HarmonyOS NEXT 图片/视频保存相册完整指南
一、权限体系与安全模型
HarmonyOS NEXT 采用严格的权限管理机制,保存文件到相册涉及两种不同的授权模式:
1. 动态权限申请(传统方式)
在 module.json5 中声明权限后,使用 abilityAccessCtrl 向用户弹窗申请:
json
{
"requestPermissions": [
{
"name": "ohos.permission.WRITE_IMAGEVIDEO",
"reason": "$string:permission_reason"
}
]
}
此方式需要用户明确授权,适用于需要频繁读写媒体库的场景。
2. 安全控件(推荐方式)
从 API 12 开始,官方推荐使用 SaveButton 安全控件。用户点击控件时,应用自动获得10秒的媒体库写入授权,无需额外权限申请,也不会触发权限弹窗,大幅提升用户体验。
注意:
PhotoViewPicker.save()方法在 API 12 中已被废弃,请优先使用 SaveButton 或photoAccessHelper.createAsset()方式。
二、方案一:使用 SaveButton 安全控件(最推荐)
适用场景
保存网络下载图片、应用内生成的图片或视频,无复杂路径选择需求。
核心代码示例
typescript
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { fileIo } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
async function savePhotoToGallery(context: common.UIAbilityContext, imageData: ArrayBuffer) {
let helper = photoAccessHelper.getPhotoAccessHelper(context);
try {
// 1. 创建图片资源(触发权限授权)
let uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg');
// 2. 打开文件并写入数据
let file = await fileIo.open(uri, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
await fileIo.write(file.fd, imageData);
await fileIo.close(file.fd);
promptAction.showToast({ message: '已保存至相册!' });
} catch (error) {
const err: BusinessError = error as BusinessError;
console.error(`保存失败: ${err.code}, ${err.message}`);
}
}
@Entry
@Component
struct SaveButtonDemo {
build() {
Column() {
Image($r('app.media.demo_image'))
.width('100%')
.height(400)
// 保存控件 - 点击即获得临时授权
SaveButton()
.padding({ top: 12, bottom: 12, left: 24, right: 24 })
.onClick(async (event, result) => {
if (result === SaveButtonOnClickResult.SUCCESS) {
const context = getContext(this) as common.UIAbilityContext;
// 需要准备图片数据(ArrayBuffer)
// const imageData = await fetchImageData();
// await savePhotoToGallery(context, imageData);
} else {
promptAction.showToast({ message: '授权失败' });
}
})
}
}
}
关键约束
-
10秒倒计时:从点击到调用
createAsset必须在 10 秒内完成,否则权限收回;但文件写入操作不受限 -
控件可见性:SaveButton 必须可见且可被用户识别,样式不能与普通按钮混淆
-
大文件处理:视频超过 2GB 时建议分段写入或使用 MediaStore API 处理
三、方案二:使用 PhotoViewPicker 选择保存路径
适用场景
需要用户主动选择保存文件夹的场景(如文档管理类应用)。
核心代码示例
typescript
import picker from '@ohos.file.picker';
import fs from '@ohos.file.fs';
async function saveWithPicker() {
const photoSaveOptions = new picker.PhotoSaveOptions();
photoSaveOptions.newFileNames = ['my_photo.jpg']; // 可选文件名
const photoViewPicker = new picker.PhotoViewPicker();
try {
const photoSaveResult = await photoViewPicker.save(photoSaveOptions);
const uri = photoSaveResult[0];
console.info('保存路径 URI:', uri);
// 后续通过 uri 写入数据
let file = fs.openSync(uri, fs.OpenMode.READ_WRITE);
fs.writeSync(file.fd, 'image data');
fs.closeSync(file);
} catch (err) {
console.error(`保存失败: ${err.message}`);
}
}
注意:
PhotoViewPicker.save()在 API 12 中已被标记为废弃,新项目应避免使用。
四、方案三:使用 photoAccessHelper 直接创建媒体资源
适用场景
已经拥有媒体数据(ArrayBuffer 或 PixelMap),需直接写入相册。
从网络下载并保存图片
typescript
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { http } from '@kit.NetworkKit';
import fs from '@ohos.file.fs';
async function downloadAndSave(url: string, context: Context) {
let helper = photoAccessHelper.getPhotoAccessHelper(context);
let uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg');
let file = fs.openSync(uri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
let httpRequest = http.createHttp();
httpRequest.on('dataReceive', (data: ArrayBuffer) => {
fs.writeSync(file.fd, data);
});
await httpRequest.requestInStream(url, { method: http.RequestMethod.GET });
httpRequest.on('dataEnd', () => {
fs.closeSync(file);
promptAction.showToast({ message: '下载并保存成功' });
});
}
保存组件快照(截图)
typescript
import componentSnapshot from '@ohos.arkui.componentSnapshot';
import image from '@ohos.multimedia.image';
import photoAccessHelper from '@ohos.file.photoAccessHelper';
async function saveComponentSnapshot(context: Context) {
// 1. 获取组件快照(需要给组件设置 .id('targetView'))
const pixelMap = await componentSnapshot.get('targetView');
// 2. 编码为 JPEG
const imagePacker = image.createImagePacker();
const options: image.PackingOption = {
format: 'image/jpeg',
quality: 100
};
const data = await imagePacker.packing(pixelMap, options);
// 3. 保存到相册
let helper = photoAccessHelper.getPhotoAccessHelper(context);
let uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg');
let file = await fs.open(uri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
await fs.write(file.fd, data);
await fs.close(file.fd);
}
五、视频保存的特殊考量
1. 大文件处理
视频文件通常较大(可能超过 2GB),SaveButton 的 10 秒授权窗口可能不足以完成全部写入。官方建议:
-
利用
createAsset后的写入操作不受时间限制的特性 -
使用 分段写入策略:分批次写入数据,避免内存溢出
2. 视频压缩后保存
如果需要先压缩视频再保存,可借助 videocompressor 库处理,压缩完成后同样使用 photoAccessHelper.createAsset() 写入相册。
typescript
// 压缩后保存示例(伪代码) const compressedData = await compressVideo(originalUri); let uri = await helper.createAsset(photoAccessHelper.PhotoType.VIDEO, 'mp4'); let file = await fs.open(uri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); await fs.write(file.fd, compressedData); await fs.close(file.fd);
六、方案对比与选型建议
| 方案 | 权限方式 | 用户体验 | 推荐场景 |
|---|---|---|---|
| SaveButton | 点击即授权,无弹窗 | ⭐⭐⭐⭐⭐ | 绝大多数保存场景(图片、视频、截图) |
| PhotoViewPicker | 用户选择路径 | ⭐⭐⭐ | 需要指定保存路径的文档管理类应用(已废弃,不推荐新项目) |
| 动态权限申请 | 弹窗授权,永久有效 | ⭐⭐⭐ | 需要频繁操作媒体库的老项目(新项目建议迁移至 SaveButton) |
最佳实践建议
-
新项目一律采用 SaveButton,无需处理复杂权限逻辑,用户体验最佳
-
如需后台自动保存(用户无直接点击),则必须申请
ohos.permission.WRITE_IMAGEVIDEO权限,但需注意严格遵循隐私政策 -
视频保存注意处理大文件场景,利用写入操作不限制时间的特性
-
保存前对图片进行适当压缩(如 packing 质量压缩、scale 尺寸压缩),可显著提升保存速度和用户体验
七、常见问题
Q1: SaveButton 点击后没有反应?
-
检查控件是否设置了有效的
onClick回调 -
确认控件可见且未被遮挡
-
查看日志是否有 ACL(访问控制列表)相关错误
Q2: 保存图片时提示权限不足?
-
若使用 SaveButton,确认点击到调用
createAsset在 10 秒内 -
若使用动态权限,确认
module.json5中已声明权限且用户已授权
Q3: 如何生成唯一文件名避免覆盖?
typescript
function getTimeStr(): string {
const now = new Date();
return `${now.getFullYear()}${now.getMonth()+1}${now.getDate()}_${now.getHours()}${now.getMinutes()}${now.getSeconds()}`;
}
// 使用: `photo_${getTimeStr()}.jpg`[citation:8]
Q4: 如何拦截 systemShare 的“保存到图库”事件?
无法直接拦截。如有强制前置权限校验需求,应放弃 systemShare,改用自定义分享面板,在自定义保存按钮中控制完整权限逻辑。
更多推荐

所有评论(0)