news 2026/7/5 2:16:49

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
《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:可能是用户手动在系统设置中关闭了权限。在入口处增加权限检查,如果权限被撤销,及时引导用户去设置中开启,而不是静默失败。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/5 2:13:33

网络安全与网络协议知识点汇总 + 选填题库

一、核心精简知识点汇总&#xff08;一&#xff09;SSH 安全远程协议&#xff08;TCP 22 端口&#xff09;连接四阶段&#xff1a;TCP 建立连接 → 协议版本协商&#xff08;明文&#xff09;→ 密钥交换&#xff08;生成加密隧道&#xff09;→ 用户身份认证关键特点&#xff…

作者头像 李华
网站建设 2026/7/5 2:13:11

微信登录 + 微信支付 业务逻辑分步详解

前置说明 两套能力都依赖微信开放平台&#xff0c;区分两种账号&#xff1a; 微信开放平台&#xff08;网站 / APP 登录、APP 支付&#xff09;&#xff1a;open.weixin.qq.com 移动应用&#xff1a;APP 微信一键登录、APP 内微信支付网站应用&#xff1a;PC 网页微信扫码登录…

作者头像 李华
网站建设 2026/7/5 2:12:51

自动扩缩容:3 种策略的适用场景

为什么需要自动扩缩容 API 服务的流量不是恒定的: 工作日 vs 周末(白天高、夜间低)营销活动(突发 5-10 倍)日常波动(20%) 固定容量的问题: 容量过小:流量高峰打爆,服务不可用容量过大:闲时浪费,白付钱 自动扩缩容:跟着流量走,既不爆也不浪费。 3 种策略 策略 1:反应式扩…

作者头像 李华
网站建设 2026/7/5 2:05:41

记录arm64内核调试环境搭建qemu_arm64_linux_01

先准备busybox busyboxcd ~ wget https://busybox.net/downloads/busybox-1.36.1.tar.bz2 tar -xvf busybox-1.36.1.tar.bz2 cd busybox-1.36.1# 配置 BusyBox make ARCHarm CROSS_COMPILEarm-linux-gnueabihf- defconfig # 启用静态编译&#xff08;关键&#xff1a;无需动态…

作者头像 李华