当前位置: 首页 > news >正文

《HarmonyOS技术精讲-Media Library Kit》之实战:构建简易相册应用

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}from'@kit.AbilityKit';import{abilityAccessCtrl,common}from'@kit.AbilityKit';import{businessError}from'@kit.BasicServicesKit';constPERMISSION_LIST:Array<Permissions>=['ohos.permission.READ_MEDIA','ohos.permission.WRITE_MEDIA'];exportdefaultclassEntryAbilityextendsUIAbility{onCreate(want:Want,launchParam:AbilityConstant.LaunchParam):void{// 这里不能直接申请权限,onCreate阶段UI还没准备好}onWindowStageCreate(windowStage):void{// 入口:请求权限constcontext=this.context;constbundleName=this.context.abilityInfo.applicationInfo.bundleName;constatManager=abilityAccessCtrl.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.json5requestPermissions字段中声明,否则动态申请会直接报错。
  • WRITE_MEDIA权限在 API 10 之后已经包含了READ_MEDIA的能力,但建议两个都声明,避免老版本兼容问题。

2. 获取相册列表和资源

核心接口是AssetManagerAlbumManager。很多人喜欢先拿所有资源再按相册分类,但这样做性能极差。正确的做法是直接查询相册对象。

// src/main/ets/model/MediaManager.tsimport{assetManagerasmediaAssetManager,AssetManager,AlbumManager}from'@kit.MediaLibraryKit';import{common}from'@kit.AbilityKit';import{image}from'@kit.ImageKit';exportclassMediaManager{privatestaticinstance:MediaManager;privateassetManager:AssetManager|null=null;privatealbumManager:AlbumManager|null=null;staticgetInstance():MediaManager{if(!MediaManager.instance){MediaManager.instance=newMediaManager();}returnMediaManager.instance;}asyncinit(context:common.Context){// 获取AssetManager实例this.assetManager=newAssetManager(context);this.albumManager=newAlbumManager(context);// 这一步很多人会忽略:必须先调用release,否则Manager内部状态可能混乱awaitthis.assetManager?.release();awaitthis.assetManager?.init();awaitthis.albumManager?.release();awaitthis.albumManager?.init();}asyncgetAllAlbums():Promise<Album[]>{if(!this.albumManager)thrownewError('AlbumManager 未初始化');// 查询所有相册constalbums=awaitthis.albumManager?.getAlbums();// 注意:getAlbums返回的是Album对象数组,但每个Album里的资源需要单独查询returnalbums??[];}asyncgetAssetsInAlbum(album:Album):Promise<Asset[]>{if(!this.assetManager)thrownewError('AssetManager 未初始化');// 关键:通过Album的URI构建查询条件constfetchOptions:AssetManager.FetchOptions={selections:[],uri:album.uri// 这里限制只查询该相册下的资源};constassets=awaitthis.assetManager?.getAssets(fetchOptions);returnassets??[];}}

为什么这里要这么写?很多人会直接用assetManager.getAssets({ selections: [] })获取所有图片,然后前端过滤相册。这在图片数量少的时候没问题,但一旦超过 1000 张,内存占用和性能都会爆炸。通过相册 URI 过滤,后端就能把数据量降下来。

3. 创建新相册

这个 API 比较直观,但有个细节:名称不能为空,且不能与已有相册重名。

// src/main/ets/model/MediaManager.tsexportclassMediaManager{// ... 前面代码略asynccreateAlbum(name:string):Promise<Album>{if(!this.albumManager)thrownewError('AlbumManager 未初始化');// 检查名称有效性if(!name||name.trim().length===0){thrownewError('相册名称不能为空');}try{constalbum=awaitthis.albumManager?.createAlbum(name);console.info(`相册创建成功:${name}, uri:${album.uri}`);returnalbum;}catch(err){console.error(`创建相册失败:${JSON.stringify(err)}`);throwerr;// 交给上层处理}}asyncdeleteAlbum(album:Album):Promise<void>{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):Promise<void>{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}from'@kit.MediaLibraryKit';@Entry@Componentstruct AlbumListPage{privatemediaManager:MediaManager=MediaManager.getInstance();@Statealbums:Album[]=[];@StatealbumAssets:Map<string,Asset[]>=newMap();@StateselectedAlbum:Album|null=null;@StateisShowingGrid:boolean=false;aboutToAppear(){this.loadAlbums();}asyncloadAlbums(){try{constcontext=getContext(this);awaitthis.mediaManager.init(contextascommon.Context);constalbumList=awaitthis.mediaManager.getAllAlbums();this.albums=albumList;// 预加载每个相册的缩略图(只取前1张)for(constalbumofalbumList){constassets=awaitthis.mediaManager.getAssetsInAlbum(album);this.albumAssets.set(album.uri,assets.slice(0,1));}}catch(err){console.error(`加载相册失败:${JSON.stringify(err)}`);}}asynconDeleteAlbum(index:number){constalbum=this.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.selectedAlbum=album;this.isShowingGrid=true;})}})}}else{// 图片网格模式AlbumGridPage({album:this.selectedAlbum!,onBack:()=>{this.isShowingGrid=false;}})}}.width('100%').height('100%')}}
// src/main/ets/pages/AlbumGridPage.ets@Componentstruct AlbumGridPage{@Propalbum:Album;privatemediaManager:MediaManager=MediaManager.getInstance();@Stateassets:Asset[]=[];@Statecallback:()=>void=()=>{};aboutToAppear(){this.loadAssets();}asyncloadAssets(){try{constassets=awaitthis.mediaManager.getAssetsInAlbum(this.album);this.assets=assets;}catch(err){console.error(`加载相册内资源失败:${JSON.stringify(err)}`);}}asynconDeleteAsset(index:number){constasset=this.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 的一个设计问题。AssetManagerAlbumManagerinit()方法内部会检查权限。如果权限是在init()之后才被授予,或者init()时权限尚未完全生效,Manager 内部状态就会进入一个“无权限”的模式,后续所有查询都返回空。

解决方案:在初始化 Manager 之前,先调用abilityAccessCtrl.checkAccessToken()确认权限确实生效。或者采用更稳妥的方式:在aboutToAppear()之后再进行一次init()

// 更安全的初始化asyncsafeInit(context:common.Context){// 先检查权限constpermissionStatus=awaitcheckPermission(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],这是最稳妥的方式。

最佳实践

  1. 不要在 build() 中创建 Manager 实例。Manager 的init()是异步操作,build()函数同步执行,会导致init()无法完成。推荐在aboutToAppear()中统一初始化。

  2. 使用@Observed@ObjectLink管理复杂状态。如果相册列表和图片列表涉及跨组件共享,建议将 MediaManager 设计为单例,并通过@Observed装饰状态对象,这样任意地方修改都会自动触发 UI 重建。

  3. 批量删除时,控制并发数。删除操作本质是异步 IO,如果一次性并发删除 100 张图片,可能会触发系统的Too Many Requests错误。推荐使用for...of循环串行删除,或者封装一个batchDelete方法,每 10 张一组。

  4. 资源查询时,合理设置FetchOptionsoffsetlimit默认不做分页,如果相册里有 10000 张图片,前端直接展示会卡死。务必在getAssets()时传入offsetlimit做分页加载。

FAQ

Q:为什么真机正常,模拟器不生效?
A:模拟器中的媒体库机制与真机不完全一致,特别是在相册创建和删除操作上。建议所有与媒体库相关的功能以真机为准。

Q:为什么页面返回后状态丢失?
A:您的页面没有做状态持久化。Media Library Kit 的查询结果是临时数据,页面销毁后需要重新查询。建议在aboutToAppear()中重新加载数据,或使用@StorageLink将状态缓存到 AppStorage。

Q:为什么第一次授权成功,第二次失败?
A:可能是用户手动在系统设置中关闭了权限。在入口处增加权限检查,如果权限被撤销,及时引导用户去设置中开启,而不是静默失败。

http://www.cnnetsun.cn/news/3149459.html

相关文章:

  • 网络安全与网络协议知识点汇总 + 选填题库
  • 微信登录 + 微信支付 业务逻辑分步详解
  • 自动扩缩容:3 种策略的适用场景
  • qt的元对象系统(具备反射能力)有哪些部件
  • 把 HLS 字幕玩出花:zwPlayer 如何让 M3U8 视频支持全文搜索、翻译与码率自适应
  • 记录arm64内核调试环境搭建qemu_arm64_linux_01
  • Rust AI 工具配置层级:命令参数、环境变量和配置文件别打架
  • 扒源码 | Cube Sandbox 的微虚机、容器镜像、可写层,是怎么串到一起的
  • 2026年储能船型开关生产商盘点:谁在领跑市场?
  • win11下Multipass修改默认MULTIPASS_STORAGE位置后,持续报错waiting for daemon的问题
  • 5分钟掌握ppInk:Windows屏幕标注终极指南,让远程协作效率翻倍
  • 【Java课程设计/毕业设计】农家乐客房排班运维管理系统的设计与实现 乡村民宿文旅服务智能化管理平台【附源码、数据库、万字文档】
  • 【Java毕业设计】基于前后端分离的民宿农家乐综合管理系统的设计与实现 农家乐客房住宿预约与订单管理系统(源码+文档+远程调试,全bao定制等)
  • 基于单片机人脸识别电子密码锁智能门禁指纹识别语音提醒防盗成品112(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_
  • 手中有机, 心中不慌 (5 只 二手 Android 手机)
  • 干货教程:APK反编译神器安卓修改大师,一步步教你如何美化和修改安卓应用
  • Java计算机毕设之美容会员储值充值积分管理系统的设计与实现 美业技师业绩提成统计管理系统(完整前后端代码+说明文档+LW,调试定制等)
  • LED灯珠颜色亮度工业自动化测量
  • 工业机器人送料机械手设计实战指南
  • 从电商项目课程设计,搞懂 JWT 鉴权和 Redis 缓存到底在解决什么问题
  • 面试官问:“模型一本正经胡说时,logprobs 抓得到吗?“
  • 你往 AI 里装的那些 skill,打开看过一眼吗?
  • 【图像分类】实战ResNet——从零构建到CIFAR-10分类(Pytorch)
  • Agent记忆系统设计与实现
  • 别把知识图谱做成高级文档库——定制化做企业级知识图谱
  • 【面板数据模型实战】从理论到Stata/R/Python实现与选择
  • 【机器人】基于缓冲的不确定性感知沃罗诺伊单元多机器人碰撞规避附Matlab代码
  • Rmarkdown动态文档创作与数据科学报告实战指南
  • 【HarmonyOS NEXT】error: failed to install bundle. code:9568322...
  • 多接地配电系统的基于PMU的系统状态估计附Matlab代码