
HarmonyOS技术精讲-Media Library Kit 之实战构建简易相册应用HarmonyOS开发中Media Library Kit媒体文件管理服务是一个绕不开的核心能力。很多人在刚开始接触时会被其复杂的权限模型和异步查询机制劝退。官方示例虽然能跑但一旦涉及到“自己创建相册”、“往相册里添加图片”、“删除图片”这种组合操作状态同步和生命周期管理的坑就全暴露出来了。这篇文章的目标很直接带你手写一个简易相册应用能看照片、能建相册、能删照片。全程不废话代码完整所有踩过的坑我都会标注出来。它解决什么问题Media Library Kit 是用来干什么的一句话它统一了设备上媒体文件图片、视频、音频的访问和管理。开发者不需要关心文件实际存在哪个目录只需要通过一套标准 API 进行查询、创建、修改和删除。适合场景自定义相册/图库应用需要管理大量媒体资源的社交或内容创作应用后台扫描、整理媒体文件的服务类应用不适合场景只需要读取少量图片建议直接用Image组件加载相对路径不需要文件级别的 CRUD 操作简单展示用PhotoAccessHelper就够了为什么用 Media Library Kit 而不是直接操作文件系统因为 HarmonyOS 对应用自有目录以外的文件访问有严格限制。直接fs.open()去读系统相册目录大概率会失败。Media Library Kit 是官方推荐且唯一稳定的途径。环境说明DevEco Studio 版本DevEco Studio 6.1.0 及以上 HarmonyOS SDK 版本HarmonyOS 6.1.0(23) 及以上 目标设备手机 / 平板核心实现1. 权限声明与申请这是第一个坑。很多人直接在module.json5里声明了权限但没有动态申请结果怎么都拿不到数据。// src/main/ets/entryability/EntryAbility.tsimport{AbilityConstant,UIAbility,Want,Permissions}fromkit.AbilityKit;import{abilityAccessCtrl,common}fromkit.AbilityKit;import{businessError}fromkit.BasicServicesKit;constPERMISSION_LIST:ArrayPermissions[ohos.permission.READ_MEDIA,ohos.permission.WRITE_MEDIA];exportdefaultclassEntryAbilityextendsUIAbility{onCreate(want:Want,launchParam:AbilityConstant.LaunchParam):void{// 这里不能直接申请权限onCreate阶段UI还没准备好}onWindowStageCreate(windowStage):void{// 入口请求权限constcontextthis.context;constbundleNamethis.context.abilityInfo.applicationInfo.bundleName;constatManagerabilityAccessCtrl.createAtManager();try{atManager.requestPermissionsFromUser(context,PERMISSION_LIST).then((data){console.info(权限授权结果:,JSON.stringify(data.authResults));// 如果全部授权才进入应用主界面}).catch((err:businessError.BusinessError){console.error(权限请求失败:${err.message});});}catch(err){console.error(权限请求异常:${JSON.stringify(err)});}}}注意事项权限必须在module.json5的requestPermissions字段中声明否则动态申请会直接报错。WRITE_MEDIA权限在 API 10 之后已经包含了READ_MEDIA的能力但建议两个都声明避免老版本兼容问题。2. 获取相册列表和资源核心接口是AssetManager和AlbumManager。很多人喜欢先拿所有资源再按相册分类但这样做性能极差。正确的做法是直接查询相册对象。// src/main/ets/model/MediaManager.tsimport{assetManagerasmediaAssetManager,AssetManager,AlbumManager}fromkit.MediaLibraryKit;import{common}fromkit.AbilityKit;import{image}fromkit.ImageKit;exportclassMediaManager{privatestaticinstance:MediaManager;privateassetManager:AssetManager|nullnull;privatealbumManager:AlbumManager|nullnull;staticgetInstance():MediaManager{if(!MediaManager.instance){MediaManager.instancenewMediaManager();}returnMediaManager.instance;}asyncinit(context:common.Context){// 获取AssetManager实例this.assetManagernewAssetManager(context);this.albumManagernewAlbumManager(context);// 这一步很多人会忽略必须先调用release否则Manager内部状态可能混乱awaitthis.assetManager?.release();awaitthis.assetManager?.init();awaitthis.albumManager?.release();awaitthis.albumManager?.init();}asyncgetAllAlbums():PromiseAlbum[]{if(!this.albumManager)thrownewError(AlbumManager 未初始化);// 查询所有相册constalbumsawaitthis.albumManager?.getAlbums();// 注意getAlbums返回的是Album对象数组但每个Album里的资源需要单独查询returnalbums??[];}asyncgetAssetsInAlbum(album:Album):PromiseAsset[]{if(!this.assetManager)thrownewError(AssetManager 未初始化);// 关键通过Album的URI构建查询条件constfetchOptions:AssetManager.FetchOptions{selections:[],uri:album.uri// 这里限制只查询该相册下的资源};constassetsawaitthis.assetManager?.getAssets(fetchOptions);returnassets??[];}}为什么这里要这么写很多人会直接用assetManager.getAssets({ selections: [] })获取所有图片然后前端过滤相册。这在图片数量少的时候没问题但一旦超过 1000 张内存占用和性能都会爆炸。通过相册 URI 过滤后端就能把数据量降下来。3. 创建新相册这个 API 比较直观但有个细节名称不能为空且不能与已有相册重名。// src/main/ets/model/MediaManager.tsexportclassMediaManager{// ... 前面代码略asynccreateAlbum(name:string):PromiseAlbum{if(!this.albumManager)thrownewError(AlbumManager 未初始化);// 检查名称有效性if(!name||name.trim().length0){thrownewError(相册名称不能为空);}try{constalbumawaitthis.albumManager?.createAlbum(name);console.info(相册创建成功:${name}, uri:${album.uri});returnalbum;}catch(err){console.error(创建相册失败:${JSON.stringify(err)});throwerr;// 交给上层处理}}asyncdeleteAlbum(album:Album):Promisevoid{if(!this.albumManager)thrownewError(AlbumManager 未初始化);// 注意删除相册不会删除里面的文件文件会回到根目录awaitthis.albumManager?.deleteAlbum(album.uri);console.info(相册删除成功:${album.uri});}}4. 删除图片删除图片同样通过AssetManager完成。这里有一个常见的坑删除后需要手动刷新 UI因为删除操作不是同步的。// src/main/ets/model/MediaManager.tsexportclassMediaManager{// ... 前面代码略asyncdeleteAsset(asset:Asset):Promisevoid{if(!this.assetManager)thrownewError(AssetManager 未初始化);try{awaitthis.assetManager?.deleteAsset(asset.uri);console.info(删除成功:${asset.uri});}catch(err){console.error(删除失败:${JSON.stringify(err)});throwerr;}}}5. UI 组件核心页面这里用 ArkUI 写一个简单的网格相册界面。重点在于状态管理和数据刷新。// src/main/ets/pages/AlbumListPage.etsimport{MediaManager}from../model/MediaManager;import{Album,Asset}fromkit.MediaLibraryKit;EntryComponentstruct AlbumListPage{privatemediaManager:MediaManagerMediaManager.getInstance();Statealbums:Album[][];StatealbumAssets:Mapstring,Asset[]newMap();StateselectedAlbum:Album|nullnull;StateisShowingGrid:booleanfalse;aboutToAppear(){this.loadAlbums();}asyncloadAlbums(){try{constcontextgetContext(this);awaitthis.mediaManager.init(contextascommon.Context);constalbumListawaitthis.mediaManager.getAllAlbums();this.albumsalbumList;// 预加载每个相册的缩略图只取前1张for(constalbumofalbumList){constassetsawaitthis.mediaManager.getAssetsInAlbum(album);this.albumAssets.set(album.uri,assets.slice(0,1));}}catch(err){console.error(加载相册失败:${JSON.stringify(err)});}}asynconDeleteAlbum(index:number){constalbumthis.albums[index];if(!album)return;try{awaitthis.mediaManager.deleteAlbum(album);// 手动从本地状态中移除this.albums.splice(index,1);this.albumAssets.delete(album.uri);// 强制刷新this.albums[...this.albums];}catch(err){console.error(删除相册失败:${JSON.stringify(err)});}}build(){Column(){if(!this.isShowingGrid){// 相册列表模式List(){ForEach(this.albums,(album:Album,index:number){ListItem(){Row(){// 缩略图占位Image(this.albumAssets.get(album.uri)?.[0]?.uri??).width(60).height(60).borderRadius(8)Text(album.displayName).fontSize(16).margin({left:12})Blank()Button(删除).onClick(()this.onDeleteAlbum(index)).backgroundColor(Color.Red)}.padding(10).onClick((){this.selectedAlbumalbum;this.isShowingGridtrue;})}})}}else{// 图片网格模式AlbumGridPage({album:this.selectedAlbum!,onBack:(){this.isShowingGridfalse;}})}}.width(100%).height(100%)}}// src/main/ets/pages/AlbumGridPage.etsComponentstruct AlbumGridPage{Propalbum:Album;privatemediaManager:MediaManagerMediaManager.getInstance();Stateassets:Asset[][];Statecallback:()void(){};aboutToAppear(){this.loadAssets();}asyncloadAssets(){try{constassetsawaitthis.mediaManager.getAssetsInAlbum(this.album);this.assetsassets;}catch(err){console.error(加载相册内资源失败:${JSON.stringify(err)});}}asynconDeleteAsset(index:number){constassetthis.assets[index];if(!asset)return;try{awaitthis.mediaManager.deleteAsset(asset);// 手动从本地数组移除并触发刷新this.assets.splice(index,1);this.assets[...this.assets];// 通知父页面刷新if(this.callback){this.callback();}}catch(err){console.error(删除图片失败:${JSON.stringify(err)});}}build(){Column(){Row(){Button(返回).onClick(()this.callback())Text(this.album.displayName).fontSize(18).fontWeight(FontWeight.Bold)}.width(100%).padding(10)Grid(){ForEach(this.assets,(asset:Asset,index:number){GridItem(){Stack(){Image(asset.uri).width(100%).height(100).objectFit(ImageFit.Cover)Button(X).width(30).height(30).position({top:0,right:0}).onClick(()this.onDeleteAsset(index))}}})}.columnsTemplate(1fr 1fr 1fr).columnsGap(5).rowsGap(5)}.width(100%).height(100%)}}常见问题 1权限授权后API 返回空结果现象明明已经在module.json5声明了权限动态请求也返回了“授权成功”但调用getAllAlbums()时返回空数组。原因这是 HarmonyOS 的一个设计问题。AssetManager和AlbumManager的init()方法内部会检查权限。如果权限是在init()之后才被授予或者init()时权限尚未完全生效Manager 内部状态就会进入一个“无权限”的模式后续所有查询都返回空。解决方案在初始化 Manager 之前先调用abilityAccessCtrl.checkAccessToken()确认权限确实生效。或者采用更稳妥的方式在aboutToAppear()之后再进行一次init()。// 更安全的初始化asyncsafeInit(context:common.Context){// 先检查权限constpermissionStatusawaitcheckPermission(context);if(!permissionStatus){console.warn(权限未完全授予跳过初始化);returnfalse;}awaitthis.init(context);returntrue;}常见问题 2删除图片后UI 没有更新现象删除了图片assets数组也做了splice操作但网格视图还是显示原来的图片。原因ArkUI 的State变更检测是基于引用变化的。如果直接修改数组splice引用没变UI 不会认为状态有变化。解决方案修改数组后一定要创建新的数组引用。推荐用this.assets [...this.assets]或者this.assets this.assets.slice()来触发变更检测。上面的代码已经用了this.assets [...this.assets]这是最稳妥的方式。最佳实践不要在 build() 中创建 Manager 实例。Manager 的init()是异步操作build()函数同步执行会导致init()无法完成。推荐在aboutToAppear()中统一初始化。使用Observed和ObjectLink管理复杂状态。如果相册列表和图片列表涉及跨组件共享建议将 MediaManager 设计为单例并通过Observed装饰状态对象这样任意地方修改都会自动触发 UI 重建。批量删除时控制并发数。删除操作本质是异步 IO如果一次性并发删除 100 张图片可能会触发系统的Too Many Requests错误。推荐使用for...of循环串行删除或者封装一个batchDelete方法每 10 张一组。资源查询时合理设置FetchOptions的offset和limit。默认不做分页如果相册里有 10000 张图片前端直接展示会卡死。务必在getAssets()时传入offset和limit做分页加载。FAQQ为什么真机正常模拟器不生效A模拟器中的媒体库机制与真机不完全一致特别是在相册创建和删除操作上。建议所有与媒体库相关的功能以真机为准。Q为什么页面返回后状态丢失A您的页面没有做状态持久化。Media Library Kit 的查询结果是临时数据页面销毁后需要重新查询。建议在aboutToAppear()中重新加载数据或使用StorageLink将状态缓存到 AppStorage。Q为什么第一次授权成功第二次失败A可能是用户手动在系统设置中关闭了权限。在入口处增加权限检查如果权限被撤销及时引导用户去设置中开启而不是静默失败。