《HarmonyOS技术精讲-Media Library Kit》之最佳实践与常见问题

📅 2026/7/6 6:00:24 👁️ 阅读次数 📝 编程学习
《HarmonyOS技术精讲-Media Library Kit》之最佳实践与常见问题

从“读取不了相册”到“自动刷新列表”:Media Library Kit 的真实坑与解法

很多人在 HarmonyOS NEXT 开发里第一次接触Media Library Kit时,会发现官方示例能跑通,能读到图片、视频。但一放到实际项目里,问题就来了:权限弹出之后点“禁止”怎么办?用户在相册里删了一张图,应用列表为什么不刷新?多删几张,列表直接白屏加报错MEDIA_LIBRARY_ERROR。这些都不是编的,是反复出现的问题。

本文就针对这几个高频痛点,提供一个能直接运行的、自动监听并刷新媒体列表的 Demo,并把权限适配、状态同步、内存释放这几个节点的正确做法拆开讲清楚。

它解决什么问题,适合什么场景

Media Library Kit是 HarmonyOS 提供的统一媒体文件管理服务。它封装了对相册、音频、视频的读写操作,并且提供了onMediaChangeonAlbumChange这类的监听接口。

适合的场景:

  • 相册/文件管理类 App
  • 应用内预览媒体文件
  • 媒体选择器

不适合的场景:

  • 大量非媒体文件的目录遍历(这种情况直接走文件系统更合适)
  • 纯流媒体播放场景(不需要管文件管理)

跟直接操作文件系统相比,Media Library Kit 的最大优势是提供了“资源变更通知”,这是 saveFile + 定时扫描做不到的。

环境说明

DevEco Studio 版本:DevEco Studio 6.1.0 及以上 HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上 目标设备:手机

核心实现

这一段实现一个页面:进入页面后申请权限,权限通过后读取媒体文件并展示列表,同时注册监听器,当相册有新文件或文件被删除时,自动刷新列表。

1. 权限模型

HarmonyOS NEXT 的权限模型有一个关键变化:不再像老版本那样在 config.json 里声明一次就行。NEXT 要求运行时动态申请,并且用户可以选择“仅本次允许”或者“禁止”。App 不能假设权限一定会被通过。

所以第一步,代码里必须处理权限被拒的情况。

import{abilityAccessCtrl,bundleManager,common,Permissions}from'@kit.AbilityKit';import{BusinessError}from'@kit.BasicServicesKit';@Entry@Componentstruct MediaListPage{@StatemediaList:Array<MediaData>=[];// 媒体文件列表privatecontext=getContext(this)ascommon.UIAbilityContext;aboutToAppear(){this.requestPermission();}asyncrequestPermission():Promise<boolean>{constpermissions:Array<Permissions>=['ohos.permission.READ_MEDIA'];// 额外提示:API 版本不同,权限声明方式可能不同,建议统一动态申请constbundleInfo=bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_PERMISSION);constgrantStatus:Array<number>=abilityAccessCtrl.createAtManager().checkAccessSync(bundleInfo.appInfo.accessTokenId.toString(),permissions[0]);if(grantStatus[0]===abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED){// 已经授权,直接初始化this.initializeMediaFetch();returntrue;}else{// 需要请求授权try{awaitabilityAccessCtrl.createAtManager().requestPermissionsFromUser(this.context,permissions);this.initializeMediaFetch();returntrue;}catch(err){letbizErr=errasBusinessError;if(bizErr.code===common.UIAbilityErrorCode.UIKIT_INNER_ERROR){// 用户拒绝授权console.error('Permission denied');}returnfalse;}}}}

为什么这样写checkAccessSync同步检查当前权限状态,可以避免在没有权限时直接调用 Media Library API 抛出异常。requestPermissionsFromUser的 catch 分支必须处理,否则应用会直接 crash。

2. 注册 onMediaChange 监听

onMediaChange用于监听所有媒体资源的变更(新增、删除、修改)。
onAlbumChange用于监听相册级别的变更(重命名相册、删除相册等)。

大部分场景只需要onMediaChange

import{photoAccessHelper}from'@kit.MediaLibraryKit';classMediaData{uri:string='';displayName:string='';dateAdded:number=0;// 根据实际需要扩展字段}@Componentstruct MediaListPage{// ... 省略 aboveToAppear 等上下文privatehelper:photoAccessHelper.PhotoAccessHelper|null=null;privatemediaListener:photoAccessHelper.MediaChangeCallback|null=null;initializeMediaFetch(){this.helper=photoAccessHelper.getPhotoAccessHelper(this.context);this.fetchMediaList();this.registerMediaChange();}registerMediaChange(){this.mediaListener={onMediaChange:(uris:Array<string>)=>{// 注意:这个回调可能在子线程触发,不要直接在回调里 setStateconsole.info('Media changed, uris:',JSON.stringify(uris));this.fetchMediaList();}};// 注册监听,返回 listenerId 用于后续解注册this.helper?.registerChange(this.mediaListener,(err)=>{if(err){console.error('registerChange failed',err.code);}else{console.info('registerChange success');}});}}

这里有一个容易被忽略的问题onMediaChange回调不会携带完整的文件 URI,只给一个变更的 URI 列表。所以最稳妥的做法是重新 fetch 全量数据。如果列表很大,可以考虑用onAlbumChange进一步细化。

3. 实现 fetchMediaList

使用photoAccessHelper获取相册中所有图片和视频。

asyncfetchMediaList(){if(!this.helper){return;}try{// 获取系统相册constalbumUri=photoAccessHelper.PhotoType.IMAGE|photoAccessHelper.PhotoType.VIDEO;constfetchResult=awaitthis.helper.getAssets({});if(!fetchResult){return;}consttotalCount=fetchResult.getCount();if(totalCount===0){this.mediaList=[];return;}// 一次性获取所有媒体(谨慎使用,大量文件可能 OOM)constassets:Array<photoAccessHelper.PhotoAsset>=[];for(leti=0;i<totalCount;i++){constasset=fetchResult.getObjectByIndex(i);if(asset){assets.push(asset);}}// 映射为 UI 数据this.mediaList=assets.map(item=>{return{uri:item.uri,displayName:item.displayName,dateAdded:item.dateAdded??0,};});fetchResult.close();// 重要:释放资源}catch(err){letmediaErr=errasBusinessError;if(mediaErr.code===photoAccessHelper.PhotoAccessHelperErrorCode.MEDIA_LIBRARY_ERROR){// 处理 Media Library 错误(通常是权限、资源耗尽等)console.error('Media Library error');}else{console.error('fetchMediaList error',mediaErr.code);}}}

代码里有一个fetchResult.close(),这是很多人会忘掉的点。getAssets返回的FetchResult对象内部持有媒体库资源引用,如果不释放,会导致内存泄漏,严重时会造成应用 OOM。官方文档虽然提到了这个 API,但没有强调不 close 的风险

4. 在 aboutToDisappear 中解注册

如果不解注册,页面销毁后监听回调仍然在运行,会触发已经被销毁页面的状态更新,造成异常。

aboutToDisappear(){if(this.helper&&this.mediaListener){this.helper.unRegisterChange(this.mediaListener,(err)=>{if(err){console.error('unregister failed');}});}this.helper=null;this.mediaListener=null;}

常见问题 1:onMediaChange 触发后页面卡死

现象:在相册里批量删除 20 张图片后,App 直接白屏或者 ANR。

原因onMediaChange回调里执行了fetchMediaList(),而fetchMediaList里用了getAssets获取全部资源。删除大量文件时,onMediaChange可能被连续触发多次,导致重复的全量查询,阻塞主线程。

解决方案:加防抖。

privatemediaChangeThrottleTimer:number|null=null;registerMediaChange(){this.mediaListener={onMediaChange:(uris)=>{if(this.mediaChangeThrottleTimer){clearTimeout(this.mediaChangeThrottleTimer);}this.mediaChangeThrottleTimer=setTimeout(()=>{this.fetchMediaList();},300);// 300ms 内合并多次变更}};}

常见问题 2:权限被拒后直接调 API 抛出 MEDIA_LIBRARY_ERROR

现象:用户点击“禁止”后,getPhotoAccessHelper没报错,但getAssets抛出了MEDIA_LIBRARY_ERROR。代码没处理这个异常,页面 crash。

原因getPhotoAccessHelper的创建不需要权限,但后续的读写操作需要。如果权限被拒,所有操作都会返回MEDIA_LIBRARY_ERROR。这个错误码是通用错误,还需要进一步检查code才确定是权限问题(photoAccessHelper.PhotoAccessHelperErrorCode.MEDIA_LIBRARY_ERROR是通用错误,实际会带子错误码,建议统一走BusinessError)。

解决方案:在requestPermission失败后,不要初始化 Media Library,直接展示“需要授权”页面。不要尝试偷偷调用 API。

// 改进 requestPermission 的返回处理constgranted=awaitthis.requestPermission();if(!granted){// 设置一个标记,UI 层判断后展示授权引导界面this.isPermissionDenied=true;}

最佳实践

  1. 不要在 build() 中初始化 Media Library 或注册监听
    ArkUI 的 build() 会被频繁调用,每次创建 helper 是资源浪费,而且重复注册会导致多个回调同时运行。在aboutToAppear中只做一次初始化。

  2. 养成解注册和 close FetchResult 的习惯
    这两个步骤写在一起,不容易遗漏。可以封装一个统一的生命周期管理方法。

  3. 优先使用 onMediaChange 来触发列表刷新,而不是轮询
    轮询既要耗电、又存在更新滞后。onMediaChange可以做到实时响应。

FAQ

Q:为什么真机正常,模拟器不生效?
A:模拟器没有真实的相册资源,getAssets可能直接返回空列表。另外模拟器不支持onMediaChange监听,只在真机上有效。

Q:为什么页面返回后状态丢失?
A:aboutToDisappear中解除了监听,但再次进入页面时aboutToAppear重新执行了申请权限的逻辑,如果权限状态未变,checkAccessSync返回已授权,然后重新初始化,用户可以接收。

Q:为什么第一次授权成功,第二次进入却弹出权限框?
A:用户可能在系统设置中手动关闭了相册权限。需要在aboutToAppearonPageShow中重新检查一次权限状态,如果被拒,再次申请。