
从拍照到删除图片管理二三事HarmonyOS NEXT 的媒体库管理Media Library Kit是核心。很多刚接触的同学在文档里看到createAsset、deleteAssets这些 API 时觉得不就是增删改查吗。但真正上手写一个“拍照-保存-展示-删除”的完整流程时会发现权限、生命周期、UI 刷新这三个环节上官方示例跟自己项目里跑起来差别很大。这篇文章不兜圈子直接写一个完整案例。从相机拍摄一张照片保存到图库然后展示出来支持单张删除和批量删除并且删除时带进度反馈。代码全部贴出来每段都配有实际运行中容易出问题的地方。它解决的是什么问题Media Library Kit 是官方提供的媒体文件管理服务。它统一了图片、视频、音频等文件的增删改查接口替代了早期版本中各个模块各自为战的局面。之前想要操作图库需要同时处理fileIo、UserFileManager等多个模块现在大部分场景一个 Kit 搞定。适用场景App 需要读写用户照片、视频、音频文件比如相机 App、图片编辑器、文件管理器。不适用场景App 自己的沙箱文件管理比如临时缓存、下载目录等这些用FileIO操作 App 私有目录即可不要走媒体库平添权限管理负担。相比旧方案API 层更统一权限模型更严格需要ohos.permission.READ_IMAGEVIDEO和ohos.permission.WRITE_IMAGEVIDEO。但不代表所有场景都推荐如果是 App 自己生成的图片且不需要进入系统图库用沙箱目录操作更干净。环境说明DevEco Studio 版本DevEco Studio 6.1.0 及以上 HarmonyOS SDK 版本HarmonyOS 6.1.0(23) 及以上 目标设备手机核心实现图片的 CRUD 全流程1. 权限申请在entry/src/main/module.json5中声明权限{ module: { requestPermissions: [ { name: ohos.permission.READ_IMAGEVIDEO, reason: $string:permission_reason_read }, { name: ohos.permission.WRITE_IMAGEVIDEO, reason: $string:permission_reason_write }, { name: ohos.permission.CAMERA, reason: $string:permission_reason_camera } ] } }注意权限必须在module.json5中声明并且reason字段要配置对应的$string资源。很多同学漏掉reason导致安装后权限弹窗不出现。2. 拍照并保存到媒体库创建一个工具类PhotoManager.ts封装媒体库操作// PhotoManager.tsimport{photoAccessHelper}fromkit.MediaLibraryKit;import{fileIo}fromkit.CoreFileKit;import{common}fromkit.AbilityKit;exportclassPhotoManager{privatecontext:common.Context;privatehelper:photoAccessHelper.PhotoAccessHelper;constructor(context:common.Context){this.contextcontext;this.helperphotoAccessHelper.getPhotoAccessHelper(context);}// 保存图片到媒体库asyncsaveImageToGallery(uri:string):Promisestring{// 1. 创建媒体库中图片的占位文件consturiObjectawaitthis.helper.createAsset(photoAccessHelper.PhotoType.IMAGE,jpg);// 2. 打开文件并写入数据constfileawaitfileIo.open(uriObject.uri,fileIo.OpenMode.WRITE_ONLY);// uri 是相机返回的临时路径需要通过 fileIo 读取constsourceFileawaitfileIo.open(uri,fileIo.OpenMode.READ_ONLY);constbufSize1024*1024;// 1MBconstbuffernewArrayBuffer(bufSize);lettotalRead0;while(true){constreadLenawaitfileIo.read(sourceFile.fd,buffer);if(readLen0)break;awaitfileIo.write(file.fd,buffer.slice(0,readLen));totalReadreadLen;}awaitfileIo.close(sourceFile.fd);awaitfileIo.close(file.fd);returnuriObject.uri;}}说明createAsset只是创建一个空的媒体文件占位返回一个临时uri后续需要手动写入二进制数据。写入时使用块读取并写入避免一次性读入大文件导致内存溢出。这个细节在官方示例中没有强调但实际项目中必须处理。在页面中调用// 假设已有相机返回的 tempUriconstmanagernewPhotoManager(getContext(this));constsavedUriawaitmanager.saveImageToGallery(tempUri);3. 获取图片详情获取媒体库中图片的信息asyncgetPhotoDetail(uri:string):PromisephotoAccessHelper.PhotoAsset|undefined{constpredicatesdataSharePredicates.createDataSharePredicates();predicates.equalTo(photoAccessHelper.PhotoKeys.URI,uri);constfetchOptions:photoAccessHelper.FetchOptions{fetchColumns:[uri,title,date_added,size],predicates:predicates};constfetchResultawaitthis.helper.getAssets(fetchOptions);if(fetchResult.getCount()0){constassetawaitfetchResult.getObjectByPosition(0);fetchResult.close();returnasset;}fetchResult.close();returnundefined;}注意getAssets返回的FetchResult在用完后必须close()否则会导致游标泄漏。这个在官方文档有提到但很多开发者容易漏掉特别是出错路径。4. 更新图片属性修改图片的标题或位置信息asyncupdatePhotoTitle(uri:string,newTitle:string):Promisevoid{constpredicatesdataSharePredicates.createDataSharePredicates();predicates.equalTo(photoAccessHelper.PhotoKeys.URI,uri);constfetchOptions:photoAccessHelper.FetchOptions{fetchColumns:[uri,title],predicates:predicates};constfetchResultawaitthis.helper.getAssets(fetchOptions);if(fetchResult.getCount()0){fetchResult.close();return;}constassetawaitfetchResult.getObjectByPosition(0);fetchResult.close();// 更新属性constchanges:photoAccessHelper.PhotoKeys{};changes[photoAccessHelper.PhotoKeys.TITLE]newTitle;awaitasset.setAttributes(this.context,changes);}一个容易忽视的点setAttributes必须在获取到PhotoAsset对象后调用并且fetchColumns中必须包含要修改的列名否则提交不生效。5. 删除图片单张与批量单张删除asyncdeletePhoto(uri:string):Promisevoid{consturis:Arraystring[uri];awaitthis.helper.deleteAssets(uris,(progress){// progress 是删除进度回调console.info(删除进度:${progress.process}/${progress.total});});}批量删除带进度反馈到 UIasyncdeletePhotos(uris:Arraystring,progressCallback:(current:number,total:number)void):Promisevoid{awaitthis.helper.deleteAssets(uris,(progress){progressCallback(progress.process,progress.total);});}在 UI 层绑定状态StatedeleteProgress:{current:number,total:number}{current:0,total:0};asynchandleBatchDelete(){this.deleteProgress{current:0,total:this.selectedUris.length};awaitthis.manager.deletePhotos(this.selectedUris,(current,total){this.deleteProgress{current,total};});}进度回调是deleteAssets提供的特性但请注意批量删除场景下如果文件数量大比如超过 50 张这个回调并不保证每删除一个文件都触发一次实际测试中发现它可能合并成几个批次。所以 UI 上的百分比是近似值不要依赖它做精确的进度显示。踩坑记录坑1权限授权的时机现象明明在module.json5中声明了权限但saveImageToGallery调用时抛出权限错误。原因ohos.permission.READ_IMAGEVIDEO和ohos.permission.WRITE_IMAGEVIDEO是用户授权型权限需要 App 运行时主动请求。module.json5声明只是告诉了系统“我需要这个权限”并不是已经拥有。解法在调用媒体库操作前先使用abilityAccessCtrl.requestPermissionsFromUser请求权限。注意一旦用户拒绝下次不会再弹窗需要引导用户去设置中开启。import{abilityAccessCtrl}fromkit.AbilityKit;asyncrequestPermission():Promiseboolean{constatManagerabilityAccessCtrl.createAtManager();try{constresultawaitatManager.requestPermissionsFromUser(getContext(this),[ohos.permission.READ_IMAGEVIDEO,ohos.permission.WRITE_IMAGEVIDEO]);returnresult[0]abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;}catch{returnfalse;}}坑2批量删除时的进度回调不准确现象总共有 10 张图片但progress回调只触发了 3 次且total始终是 10。原因deleteAssets内部会尝试合并删除操作以提高效率。进度回调是按实际文件系统操作触发的并不严格等于每删除一个文件触发一次。解法不要用进度回调做精确的“已删除 X/总文件数”的进度条。可以将其作为“操作进行中”的提示或者改为更粗粒度的状态开始、处理中、完成。如果需要精确的文件级删除进度建议逐张调用deletePhoto但会牺牲性能。最佳实践不要在主线程中执行文件写入saveImageToGallery内部的fileIo操作是异步的但如果在Component的build()中直接调用会阻塞 UI 渲染。建议封装为async方法在Controller或ViewModel层调用UI 只负责显示状态。及时关闭FetchResult任何通过getAssets获取到的结果都要close()否则游标泄漏在模拟器上可能不明显但在真机上长时间运行会导致媒体库操作卡顿甚至崩溃。官方文档有提到这一点但实际开发中还是容易漏掉。使用PhotoType过滤图片类型createAsset和getAssets都支持传入类型参数比如只查图片不要视频。示例中写死了photoAccessHelper.PhotoType.IMAGE实际项目建议根据场景使用PhotoType枚举避免返回不相关的媒体文件。FAQQ为什么真机上权限弹窗不出现但在模拟器上正常A检查两个地方。第一module.json5中的reason字段是否配置了对应的$string资源。第二部分设备厂商对相机权限有额外限制需要在系统设置中手动开启。模拟器因为不需要真正的相机硬件权限管理往往比真机宽松。Q删除图片后图库中的缩略图没有立即更新怎么处理A这是正常现象。媒体库的变更通知是异步的缩略图的索引更新可能延迟几秒。如果需要即时刷新可以在删除成功后主动触发页面数据重新加载例如重新调用getAssets获取最新列表。QcreateAsset创建的图片是空的该怎么填充内容AcreateAsset只创建一个空文件占位需要自己写入二进制数据。参考上面的saveImageToGallery实现使用fileIo打开文件和写入。注意写入完成后要正确关闭文件描述符。Demo 入口// Index.etsEntryComponentstruct PhotoGalleryApp{Statephotos:ArrayphotoAccessHelper.PhotoAsset[];StateselectedUris:Arraystring[];build(){Column(){// 拍照按钮Button(拍照并保存).onClick(()this.captureAndSave())// 图片列表List({space:10}){ForEach(this.photos,(item:photoAccessHelper.PhotoAsset){ListItem(){Image(item.uri).width(100).height(100)}})}// 批量删除按钮Button(批量删除).onClick(()this.handleBatchDelete())// 进度显示if(this.deleteProgress.total0){Progress({value:this.deleteProgress.current,total:this.deleteProgress.total}).width(80%)}}.width(100%).height(100%)}asynccaptureAndSave(){/* 调用相机并保存 */}asynchandleBatchDelete(){/* 调用批量删除 */}}整体来看Media Library Kit 的 API 设计已经比较完善日常的图片管理需求都能覆盖。但几个关键细节——权限请求时机、文件写入方式、FetchResult 释放——是开发者容易忽略的。希望这篇文章能帮你少走一些弯路。