
从“读取不了相册”到“自动刷新列表”Media Library Kit 的真实坑与解法很多人在 HarmonyOS NEXT 开发里第一次接触Media Library Kit时会发现官方示例能跑通能读到图片、视频。但一放到实际项目里问题就来了权限弹出之后点“禁止”怎么办用户在相册里删了一张图应用列表为什么不刷新多删几张列表直接白屏加报错MEDIA_LIBRARY_ERROR。这些都不是编的是反复出现的问题。本文就针对这几个高频痛点提供一个能直接运行的、自动监听并刷新媒体列表的 Demo并把权限适配、状态同步、内存释放这几个节点的正确做法拆开讲清楚。它解决什么问题适合什么场景Media Library Kit是 HarmonyOS 提供的统一媒体文件管理服务。它封装了对相册、音频、视频的读写操作并且提供了onMediaChange、onAlbumChange这类的监听接口。适合的场景相册/文件管理类 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}fromkit.AbilityKit;import{BusinessError}fromkit.BasicServicesKit;EntryComponentstruct MediaListPage{StatemediaList:ArrayMediaData[];// 媒体文件列表privatecontextgetContext(this)ascommon.UIAbilityContext;aboutToAppear(){this.requestPermission();}asyncrequestPermission():Promiseboolean{constpermissions:ArrayPermissions[ohos.permission.READ_MEDIA];// 额外提示API 版本不同权限声明方式可能不同建议统一动态申请constbundleInfobundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_PERMISSION);constgrantStatus:ArraynumberabilityAccessCtrl.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){letbizErrerrasBusinessError;if(bizErr.codecommon.UIAbilityErrorCode.UIKIT_INNER_ERROR){// 用户拒绝授权console.error(Permission denied);}returnfalse;}}}}为什么这样写checkAccessSync同步检查当前权限状态可以避免在没有权限时直接调用 Media Library API 抛出异常。requestPermissionsFromUser的 catch 分支必须处理否则应用会直接 crash。2. 注册 onMediaChange 监听onMediaChange用于监听所有媒体资源的变更新增、删除、修改。onAlbumChange用于监听相册级别的变更重命名相册、删除相册等。大部分场景只需要onMediaChange。import{photoAccessHelper}fromkit.MediaLibraryKit;classMediaData{uri:string;displayName:string;dateAdded:number0;// 根据实际需要扩展字段}Componentstruct MediaListPage{// ... 省略 aboveToAppear 等上下文privatehelper:photoAccessHelper.PhotoAccessHelper|nullnull;privatemediaListener:photoAccessHelper.MediaChangeCallback|nullnull;initializeMediaFetch(){this.helperphotoAccessHelper.getPhotoAccessHelper(this.context);this.fetchMediaList();this.registerMediaChange();}registerMediaChange(){this.mediaListener{onMediaChange:(uris:Arraystring){// 注意这个回调可能在子线程触发不要直接在回调里 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{// 获取系统相册constalbumUriphotoAccessHelper.PhotoType.IMAGE|photoAccessHelper.PhotoType.VIDEO;constfetchResultawaitthis.helper.getAssets({});if(!fetchResult){return;}consttotalCountfetchResult.getCount();if(totalCount0){this.mediaList[];return;}// 一次性获取所有媒体谨慎使用大量文件可能 OOMconstassets:ArrayphotoAccessHelper.PhotoAsset[];for(leti0;itotalCount;i){constassetfetchResult.getObjectByIndex(i);if(asset){assets.push(asset);}}// 映射为 UI 数据this.mediaListassets.map(item{return{uri:item.uri,displayName:item.displayName,dateAdded:item.dateAdded??0,};});fetchResult.close();// 重要释放资源}catch(err){letmediaErrerrasBusinessError;if(mediaErr.codephotoAccessHelper.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.helperthis.mediaListener){this.helper.unRegisterChange(this.mediaListener,(err){if(err){console.error(unregister failed);}});}this.helpernull;this.mediaListenernull;}常见问题 1onMediaChange 触发后页面卡死现象在相册里批量删除 20 张图片后App 直接白屏或者 ANR。原因onMediaChange回调里执行了fetchMediaList()而fetchMediaList里用了getAssets获取全部资源。删除大量文件时onMediaChange可能被连续触发多次导致重复的全量查询阻塞主线程。解决方案加防抖。privatemediaChangeThrottleTimer:number|nullnull;registerMediaChange(){this.mediaListener{onMediaChange:(uris){if(this.mediaChangeThrottleTimer){clearTimeout(this.mediaChangeThrottleTimer);}this.mediaChangeThrottleTimersetTimeout((){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 的返回处理constgrantedawaitthis.requestPermission();if(!granted){// 设置一个标记UI 层判断后展示授权引导界面this.isPermissionDeniedtrue;}最佳实践不要在 build() 中初始化 Media Library 或注册监听ArkUI 的 build() 会被频繁调用每次创建 helper 是资源浪费而且重复注册会导致多个回调同时运行。在aboutToAppear中只做一次初始化。养成解注册和 close FetchResult 的习惯这两个步骤写在一起不容易遗漏。可以封装一个统一的生命周期管理方法。优先使用 onMediaChange 来触发列表刷新而不是轮询轮询既要耗电、又存在更新滞后。onMediaChange可以做到实时响应。FAQQ为什么真机正常模拟器不生效A模拟器没有真实的相册资源getAssets可能直接返回空列表。另外模拟器不支持onMediaChange监听只在真机上有效。Q为什么页面返回后状态丢失AaboutToDisappear中解除了监听但再次进入页面时aboutToAppear重新执行了申请权限的逻辑如果权限状态未变checkAccessSync返回已授权然后重新初始化用户可以接收。Q为什么第一次授权成功第二次进入却弹出权限框A用户可能在系统设置中手动关闭了相册权限。需要在aboutToAppear或onPageShow中重新检查一次权限状态如果被拒再次申请。