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}fromkit.MediaLibraryKit;import{image}fromkit.ImageKit;import{photoOutput}fromkit.CameraKit;asyncfunctionsavePhoto(photo:photoOutput.Photo):Promisestring{// 获取照片的 Buffer 数据constimageSource:image.ImageSourceimage.createImageSource(photo.main.uri);constpixelMap:image.PixelMapawaitimageSource.createPixelMap();constbuffer:ArrayBufferawaitpixelMap.readPixelsToBuffer();// 创建媒体库实例constcontext:ContextgetContext(this);constmediaLibrary:photoAccessHelper.PhotoAccessHelperphotoAccessHelper.getPhotoAccessHelper(context);// 创建媒体文件指定类型为图片consturi:stringawaitmediaLibrary.createAsset(photoAccessHelper.PhotoType.IMAGE,jpg);constfile:fileIo.FileawaitfileIo.open(uri,fileIo.OpenMode.WRITE_ONLY);awaitfileIo.write(file.fd,buffer);awaitfileIo.close(file);returnuri;}几个关键点createAsset会先在媒体库中创建一个 Asset 记录返回一个 URI。这个 URI 可以理解为系统相册的引用点。写入数据时一定要用WRITE_ONLY模式如果传READ_WRITE会导致写入失败。写入完成必须close否则文件句柄泄露。步骤 3查询相册列表查询已保存的照片用于相册页面展示。这里使用getAssets方法配合FetchOptions进行筛选。import{photoAccessHelper}fromkit.MediaLibraryKit;asyncfunctionqueryPhotos():PromisephotoAccessHelper.PhotoAsset[]{constcontext:ContextgetContext(this);constmediaLibrary:photoAccessHelper.PhotoAccessHelperphotoAccessHelper.getPhotoAccessHelper(context);// 设置查询条件只查询图片类型按日期倒序constfetchOptions:photoAccessHelper.FetchOptions{selections:${photoAccessHelper.PhotoKeys.PHOTO_TYPE} ?,selectionArgs:[photoAccessHelper.PhotoType.IMAGE.toString()],order:date_added DESC// 按添加时间降序};constfetchResult:photoAccessHelper.FetchResultphotoAccessHelper.PhotoAssetawaitmediaLibrary.getAssets(fetchOptions);constassets:photoAccessHelper.PhotoAsset[][];// 遍历结果集for(leti0;ifetchResult.count;i){constasset:photoAccessHelper.PhotoAssetawaitfetchResult.getObjectByIndex(i);assets.push(asset);}returnassets;}这里有一个容易忽略的地方order参数的值必须严格按照PhotoKeys定义的字符串写否则查询会静默失败返回空结果集。步骤 4删除操作删除媒体文件需要先获取 Asset 对象然后调用deleteAssets。import{photoAccessHelper}fromkit.MediaLibraryKit;asyncfunctiondeletePhoto(uri:string):Promisevoid{constcontext:ContextgetContext(this);constmediaLibrary:photoAccessHelper.PhotoAccessHelperphotoAccessHelper.getPhotoAccessHelper(context);// 通过 URI 查询到具体 AssetconstfetchOptions:photoAccessHelper.FetchOptions{selections:${photoAccessHelper.PhotoKeys.URI} ?,selectionArgs:[uri]};constfetchResult:photoAccessHelper.FetchResultphotoAccessHelper.PhotoAssetawaitmediaLibrary.getAssets(fetchOptions);constasset:photoAccessHelper.PhotoAssetawaitfetchResult.getObjectByIndex(0);// 删除支持批量删除这里只删除一个awaitmediaLibrary.deleteAssets([asset]);}deleteAssets接收的是一个数组意味着可以批量删除。但需要注意删除操作会同时删除系统相册中的文件无法通过应用侧恢复。步骤 5完整相册列表页面把上面步骤串起来做一个带管理功能的页面。import{photoAccessHelper}fromkit.MediaLibraryKit;EntryComponentstruct AlbumPage{StatephotoAssets:photoAccessHelper.PhotoAsset[][];aboutToAppear():void{this.loadPhotos();}asyncloadPhotos():Promisevoid{try{constassets:photoAccessHelper.PhotoAsset[]awaitqueryPhotos();this.photoAssetsassets;}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(){awaitdeletePhoto(asset.uri);// 删除后刷新列表awaitthis.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)但监听回调需要额外管理生命周期容易内存泄漏。推荐做法是删除后延迟刷新asyncfunctiondeletePhotoAndRefresh(uri:string):Promisevoid{awaitdeletePhoto(uri);// 延迟 1 秒以保证删除生效awaitnewPromise(resolvesetTimeout(resolve,1000));awaitloadPhotos();}常见问题 2权限弹窗被拒绝后无法恢复现象用户第一次拒绝权限弹窗后后续再请求不会再弹窗导致媒体库操作全部失败。原因HarmonyOS 对敏感权限的弹窗策略是用户拒绝一次后系统不会重复弹窗需要用户手动到设置中开启。解决方案在权限被拒绝后给用户一个明确的提示引导去设置页手动开启。import{abilityAccessCtrl}fromkit.AbilityKit;asyncfunctionrequestPermission(context:Context):Promiseboolean{constatManager:abilityAccessCtrl.AtManagerabilityAccessCtrl.createAtManager();constresult:numberawaitatManager.requestPermissionsFromUser(context,[ohos.permission.READ_MEDIA,ohos.permission.WRITE_MEDIA]);if(resultabilityAccessCtrl.GrantStatus.PERMISSION_DENIED){// 引导用户去设置页AlertDialog.show({title:权限被拒绝,message:请在系统设置中手动开启媒体库读写权限,confirm:{value:去设置,action:(){// 调用跳转系统设置页面的 API}}});returnfalse;}returntrue;}最佳实践不要在build()中创建 MediaLibrary 实例。photoAccessHelper.getPhotoAccessHelper是一个同步 API放在aboutToAppear或onPageShow中初始化避免重复创建。异步操作记得try/catch。媒体库的读写可能因为存储空间不足、文件损坏等异常而失败。建议对每个关键步骤加异常处理至少日志输出便于排查。状态同步用State响应不要手动赋值。上面示例中this.photoAssets是State修饰的修改后会触发 UI 刷新。如果直接let assets this.photoAssets然后assets.push是不会刷新列表的。FAQQ查询返回的 URI 是content://格式为什么用Image组件能直接显示AHarmonyOS 的Image组件支持content://协议开头的内容 URI会自动解析并加载图片。不需要手动转换为文件路径。Q删除照片后系统相册中的应用图标也会消失吗A不会。删除文件只会删除该媒体文件本身应用在系统相册中的项目依然存在只是无法打开预览。总结MediaLibrary Kit 是 Camera Kit 的必备搭档。看似简单的增删查实际上隐藏着权限状态、查询条件、异步延迟这些坑。实际项目中建议统一封装一个媒体管理服务把查询、删除、刷新逻辑收敛到同一个DataService中避免页面直接操作媒体库实例。