HarmonyOS技术精讲-Camera Kit(相机服务)第18篇:媒体库集成与管理
HarmonyOS技术精讲-Camera Kit(相机服务)第18篇:媒体库集成与管理

拍完的照片究竟藏哪了?
HarmonyOS NEXT 开发里,Camera Kit 本身只管拍照和录像,不负责文件管理。这意味着你拍完一张照片,如果不去主动保存到媒体库,它就是个临时文件,系统相册里根本看不到。
很多人第一次对接这个链路时,会习惯性地调用 save() 方法保存到应用沙箱,然后发现系统相册里找不到,用户会直接干懵。这个问题在应用市场反馈里非常常见。
这里的关键在于:Camera Kit 只管拍摄,MediaLibrary Kit 才管文件的持久化存储和系统索引。 两者是上下游关系,不是同层关系。
本文通过一个带管理功能的相册页面,完整演示从拍摄到存储、从查询到删除的全链路。
环境说明
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机
核心实现
整个功能分两部分:相册页面展示和管理逻辑。下面按步骤拆解。
步骤 1:权限申请
MediaLibrary Kit 的操作需要 ohos.permission.READ_MEDIA 和 ohos.permission.WRITE_MEDIA 权限。在 module.json5 中声明:
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.CAMERA",
"reason": "用于拍照"
},
{
"name": "ohos.permission.READ_MEDIA",
"reason": "用于读取媒体文件"
},
{
"name": "ohos.permission.WRITE_MEDIA",
"reason": "用于保存媒体文件"
}
]
}
}
注意:READ_MEDIA 和 WRITE_MEDIA 是用户授权类权限,真机和模拟器上都需要弹窗确认。
步骤 2:创建媒体文件并保存
拍照后,调用 photoOutput.capture() 得到 Photo 对象。然后通过 MediaLibrary API 创建媒体 Asset 并写入数据。
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { image } from '@kit.ImageKit';
import { photoOutput } from '@kit.CameraKit';
async function savePhoto(photo: photoOutput.Photo): Promise<string> {
// 获取照片的 Buffer 数据
const imageSource: image.ImageSource = image.createImageSource(photo.main.uri);
const pixelMap: image.PixelMap = await imageSource.createPixelMap();
const buffer: ArrayBuffer = await pixelMap.readPixelsToBuffer();
// 创建媒体库实例
const context: Context = getContext(this);
const mediaLibrary: photoAccessHelper.PhotoAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
// 创建媒体文件,指定类型为图片
const uri: string = await mediaLibrary.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg');
const file: fileIo.File = await fileIo.open(uri, fileIo.OpenMode.WRITE_ONLY);
await fileIo.write(file.fd, buffer);
await fileIo.close(file);
return uri;
}
几个关键点:
createAsset会先在媒体库中创建一个 Asset 记录,返回一个 URI。这个 URI 可以理解为系统相册的引用点。- 写入数据时一定要用
WRITE_ONLY模式,如果传READ_WRITE会导致写入失败。 - 写入完成必须
close,否则文件句柄泄露。
步骤 3:查询相册列表
查询已保存的照片,用于相册页面展示。这里使用 getAssets 方法,配合 FetchOptions 进行筛选。
import { photoAccessHelper } from '@kit.MediaLibraryKit';
async function queryPhotos(): Promise<photoAccessHelper.PhotoAsset[]> {
const context: Context = getContext(this);
const mediaLibrary: photoAccessHelper.PhotoAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
// 设置查询条件:只查询图片类型,按日期倒序
const fetchOptions: photoAccessHelper.FetchOptions = {
selections: `${photoAccessHelper.PhotoKeys.PHOTO_TYPE} = ?`,
selectionArgs: [photoAccessHelper.PhotoType.IMAGE.toString()],
order: 'date_added DESC' // 按添加时间降序
};
const fetchResult: photoAccessHelper.FetchResult<photoAccessHelper.PhotoAsset> =
await mediaLibrary.getAssets(fetchOptions);
const assets: photoAccessHelper.PhotoAsset[] = [];
// 遍历结果集
for (let i = 0; i < fetchResult.count; i++) {
const asset: photoAccessHelper.PhotoAsset = await fetchResult.getObjectByIndex(i);
assets.push(asset);
}
return assets;
}
这里有一个容易忽略的地方:order 参数的值必须严格按照 PhotoKeys 定义的字符串写,否则查询会静默失败(返回空结果集)。
步骤 4:删除操作
删除媒体文件需要先获取 Asset 对象,然后调用 deleteAssets。
import { photoAccessHelper } from '@kit.MediaLibraryKit';
async function deletePhoto(uri: string): Promise<void> {
const context: Context = getContext(this);
const mediaLibrary: photoAccessHelper.PhotoAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
// 通过 URI 查询到具体 Asset
const fetchOptions: photoAccessHelper.FetchOptions = {
selections: `${photoAccessHelper.PhotoKeys.URI} = ?`,
selectionArgs: [uri]
};
const fetchResult: photoAccessHelper.FetchResult<photoAccessHelper.PhotoAsset> =
await mediaLibrary.getAssets(fetchOptions);
const asset: photoAccessHelper.PhotoAsset = await fetchResult.getObjectByIndex(0);
// 删除,支持批量删除,这里只删除一个
await mediaLibrary.deleteAssets([asset]);
}
deleteAssets 接收的是一个数组,意味着可以批量删除。但需要注意,删除操作会同时删除系统相册中的文件,无法通过应用侧恢复。
步骤 5:完整相册列表页面
把上面步骤串起来,做一个带管理功能的页面。
import { photoAccessHelper } from '@kit.MediaLibraryKit';
@Entry
@Component
struct AlbumPage {
@State photoAssets: photoAccessHelper.PhotoAsset[] = [];
aboutToAppear(): void {
this.loadPhotos();
}
async loadPhotos(): Promise<void> {
try {
const assets: photoAccessHelper.PhotoAsset[] = await queryPhotos();
this.photoAssets = assets;
} catch (error) {
console.error('查询相册失败: ' + JSON.stringify(error));
}
}
build() {
Column() {
List({ space: 10 }) {
ForEach(this.photoAssets, (asset: photoAccessHelper.PhotoAsset) => {
ListItem() {
Row() {
Image(asset.uri)
.width('40%')
.aspectRatio(1)
.objectFit(ImageFit.Cover)
.onClick(async () => {
// 点击查看大图(此处可扩展)
})
Column() {
Text(asset.title)
.fontSize(14)
Text(asset.dateAdded.toString())
.fontSize(12)
.fontColor(Color.Gray)
Button('删除')
.type(ButtonType.Normal)
.onClick(async () => {
await deletePhoto(asset.uri);
// 删除后刷新列表
await this.loadPhotos();
})
}
.padding({ left: 10 })
}
.width('100%')
}
}, (asset: photoAccessHelper.PhotoAsset) => asset.uri)
}
.width('100%')
}
.padding(10)
}
}
为什么这里用 ForEach 而不是 LazyForEach? 因为相册照片数量通常不会太长(几百张以内),用 ForEach 够用。如果业务上有大量图片(比如图库应用),换成 LazyForEach 避免一次性渲染所有列表项。
常见问题 1:删除后相册不刷新
现象: 调用 deleteAssets 成功后,再次查询依然能看到已删除的照片。
原因: 媒体库的文件删除是异步完成的。deleteAssets 返回只表示删请求已提交,不代表物理删除已完成。此时立即查询,结果可能包含尚未清理的缓存。
解决方案: 删除成功后,等待 500ms-1s 再重新查询;或者监听媒体库的变化事件(on('albumChange')),但监听回调需要额外管理生命周期,容易内存泄漏。
推荐做法是删除后延迟刷新:
async function deletePhotoAndRefresh(uri: string): Promise<void> {
await deletePhoto(uri);
// 延迟 1 秒以保证删除生效
await new Promise(resolve => setTimeout(resolve, 1000));
await loadPhotos();
}
常见问题 2:权限弹窗被拒绝后无法恢复
现象: 用户第一次拒绝权限弹窗后,后续再请求不会再弹窗,导致媒体库操作全部失败。
原因: HarmonyOS 对敏感权限的弹窗策略是:用户拒绝一次后,系统不会重复弹窗,需要用户手动到设置中开启。
解决方案: 在权限被拒绝后,给用户一个明确的提示,引导去设置页手动开启。
import { abilityAccessCtrl } from '@kit.AbilityKit';
async function requestPermission(context: Context): Promise<boolean> {
const atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
const result: number = await atManager.requestPermissionsFromUser(context, [
'ohos.permission.READ_MEDIA',
'ohos.permission.WRITE_MEDIA'
]);
if (result === abilityAccessCtrl.GrantStatus.PERMISSION_DENIED) {
// 引导用户去设置页
AlertDialog.show({
title: '权限被拒绝',
message: '请在系统设置中手动开启媒体库读写权限',
confirm: {
value: '去设置',
action: () => {
// 调用跳转系统设置页面的 API
}
}
});
return false;
}
return true;
}
最佳实践
-
不要在
build()中创建 MediaLibrary 实例。photoAccessHelper.getPhotoAccessHelper是一个同步 API,放在aboutToAppear或onPageShow中初始化,避免重复创建。 -
异步操作记得
try/catch。 媒体库的读写可能因为存储空间不足、文件损坏等异常而失败。建议对每个关键步骤加异常处理,至少日志输出便于排查。 -
状态同步用
@State响应,不要手动赋值。 上面示例中this.photoAssets是@State修饰的,修改后会触发 UI 刷新。如果直接let assets = this.photoAssets然后assets.push,是不会刷新列表的。
FAQ
Q:查询返回的 URI 是 content:// 格式,为什么用 Image 组件能直接显示?
A:HarmonyOS 的 Image 组件支持 content:// 协议开头的内容 URI,会自动解析并加载图片。不需要手动转换为文件路径。
Q:删除照片后,系统相册中的应用图标也会消失吗?
A:不会。删除文件只会删除该媒体文件本身,应用在系统相册中的项目依然存在,只是无法打开预览。
总结
MediaLibrary Kit 是 Camera Kit 的必备搭档。看似简单的增删查,实际上隐藏着权限状态、查询条件、异步延迟这些坑。实际项目中建议统一封装一个媒体管理服务,把查询、删除、刷新逻辑收敛到同一个 DataService 中,避免页面直接操作媒体库实例。
更多推荐



所有评论(0)