媒体资源管理器 - ArkTS实现媒体库查询与缩略图加载详解

📅 2026/8/25 21:06:01
媒体资源管理器 - ArkTS实现媒体库查询与缩略图加载详解
实例媒体资源管理器技术ohos.file.photoAccessHelper媒体库、ohos.multimedia.image图片解码、媒体权限、缩略图策略、kit.ArkDatarelationalStore 收藏一、本篇范围媒体资源管理器是「媒体库 图像处理」的综合性应用读取系统相册中的图片和视频按时间分组展示为网格缩略图点击进入大图预览页支持左右滑动双击收藏图片收藏关系写入 SQLite视频条目可以播放AVPlayer在第二篇详解。本篇拆「媒体数据服务层」核心问题媒体权限怎么申请ohos.permission.READ_IMAGEVIDEO的 user_grant 流程与 iOS 式的选图授权差异媒体库怎么查询photoAccessHelper.PhotoAccessHelper的getAssets与 FetchOptions按日期排序、分页缩略图怎么拿MediaAsset.getThumbnail与 requestId 缓存为什么不能直接解码原图收藏怎么落库SQLite 收藏表设计与系统相册的关系收藏 本地业务数据。二、媒体库权限用户可选择部分照片2.1 权限声明媒体库读取需要ohos.permission.READ_IMAGEVIDEO读图片视频和ohos.permission.WRITE_IMAGEVIDEO写本应用只读不需要。都是user_grant{ module: { requestPermissions: [ { name: ohos.permission.READ_IMAGEVIDEO, reason: $string:read_media_reason, usedScene: { abilities: [EntryAbility], when: inuse } } ] } }2.2 授权模型的演进HarmonyOS 的媒体权限经历过两个阶段阶段模型特点API 9 之前整库授权授予后读全部媒体权限粒度粗API 10含 24选图授权授权后只读用户勾选的照片未勾选不可见requestPermissionsFromUser申请READ_IMAGEVIDEO时系统弹「选择照片」界面类似 iOS 的相册授权用户勾选的照片才对你可见。这个模型的好处是隐私更严应用看不到用户没选的照片代价是「全相册应用」拿不到全部内容——getAssets只能查到已授权部分。重要工程影响用户后续在系统相册新增照片若不在授权集合内应用要提示「去系统设置扩展授权」。应用内要做「授权照片数量为 0」的引导分支。2.3 权限申请代码import{abilityAccessCtrl,common,Permissions}fromkit.AbilityKit;import{photoAccessHelper}fromkit.MediaLibraryKit;import{BusinessError}fromkit.BasicServicesKit;exportclassMediaPermission{staticasyncensure(context:common.UIAbilityContext):Promiseboolean{constatManagerabilityAccessCtrl.createAtManager();constperms:Permissions[][ohos.permission.READ_IMAGEVIDEO];try{conststatusawaitatManager.checkAccessToken(context.applicationInfo.accessTokenId,perms[0]);if(statusabilityAccessCtrl.GrantStatus.PERMISSION_GRANTED){returntrue;}constresultawaitatManager.requestPermissionsFromUser(context,perms);returnresult.authResults[0]abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;}catch(err){console.error(媒体权限申请失败:${(errasBusinessError).message});returnfalse;}}}三、媒体库查询PhotoAccessHelper3.1 创建助手与查询参数import{photoAccessHelper}fromkit.MediaLibraryKit;import{common}fromkit.AbilityKit;exportinterfaceMediaItem{uri:string;// 媒体资源 urifile://media/...displayName:string;// 文件名dateAdded:number;// 加入时间戳mediaType:number;// 1 图片 / 2 视频size:number;// 字节width:number;// 像素宽原图height:number;// 像素高favorite:boolean;// 收藏本地业务字段}exportclassMediaLibraryService{privatestatichelper:photoAccessHelper.PhotoAccessHelper|nullnull;privatestaticreadonlyPAGE_SIZE:number50;staticgetHelper(context:common.UIAbilityContext):photoAccessHelper.PhotoAccessHelper{if(!MediaLibraryService.helper){MediaLibraryService.helperphotoAccessHelper.getPhotoAccessHelper(context);}returnMediaLibraryService.helper;}/** * 分页查询媒体资源按加入时间倒序最新在前。 */staticasyncqueryAssets(context:common.UIAbilityContext,offset:number0,limit:numberMediaLibraryService.PAGE_SIZE):PromiseMediaItem[]{consthelperMediaLibraryService.getHelper(context);// 1. 构造查询选项媒体类型 排序 分页constfetchOptions:photoAccessHelper.FetchOptions{selections:${photoAccessHelper.PhotoKeys.MEDIA_TYPE}IN (?, ?),selectionArgs:[${photoAccessHelper.PhotoType.IMAGE},${photoAccessHelper.PhotoType.VIDEO}],order:${photoAccessHelper.PhotoKeys.DATE_ADDED}DESC,fetchColumns:[photoAccessHelper.PhotoKeys.URI,photoAccessHelper.PhotoKeys.DISPLAY_NAME,photoAccessHelper.PhotoKeys.DATE_ADDED,photoAccessHelper.PhotoKeys.MEDIA_TYPE,photoAccessHelper.PhotoKeys.SIZE,photoAccessHelper.PhotoKeys.WIDTH,photoAccessHelper.PhotoKeys.HEIGHT],offset:offset,limit:limit};// 2. 执行查询constresultawaithelper.getAssets(fetchOptions);// 3. 遍历结果集constitems:MediaItem[][];result.getCount();// 预热计数for(leti0;iresult.getCount();i){constassetawaitresult.getByIndex(i);items.push({uri:asset.uri,displayName:asset.displayName??,dateAdded:asset.dateAdded??0,mediaType:asset.mediaType??0,size:asset.size??0,width:asset.width??0,height:asset.height??0,favorite:false});}result.close();// 关闭结果集释放资源returnitems;}}3.2 FetchOptions 的四个字段FetchOptions是媒体库查询的「查询语言」四个字段各管一件事字段作用示例selectionsSQL 式过滤条件MEDIA_TYPE IN (?, ?)selectionArgs条件参数防注入[‘1’, ‘2’]order排序DATE_ADDED DESCfetchColumns要读哪些列性能关键URI/名称/类型/尺寸fetchColumns 是最容易忽略的性能字段不指定会返回所有列含未解码的完整元数据指定后只拉需要的列。查询 500 张照片全列 vs 7 列的性能差异是数量级的。PhotoKeys 常量photoAccessHelper.PhotoKeys.MEDIA_TYPE等是系统提供的列名常量不要手写字符串——列名变更时编译期就能发现。MEDIA_TYPE 的取值PhotoType.IMAGE 1、PhotoType.VIDEO 2。3.3 结果集遍历getAssets返回FetchResultPhotoAsset用法getCount()拿总数getByIndex(i)逐个取PhotoAsset。必须close()——不关闭会泄漏游标资源多次翻页后查询变慢甚至失败。分页加载滚动到底加载下一页就是不断调queryAssets(offset, limit)的过程offset 累加。四、缩略图加载性能的生命线4.1 为什么不能直接解码原图网格里 50 张缩略图如果每张都解码原图4000×3000 像素内存50 × 4000×3000×4 字节 ≈ 2.4GB必然 OOM时间每张原图解码几百毫秒网格加载数十秒。正确姿势是用系统提供的缩略图PhotoAsset.getThumbnail(size)返回尺寸受限的缩略图系统按需生成缓存内存和时间都控制在合理范围。4.2 缩略图服务带 requestId 缓存import{image}fromkit.ImageKit;import{photoAccessHelper}fromkit.MediaLibraryKit;exportclassThumbnailLoader{// uri - 已请求的 requestId防止重复请求同一张privatestaticpendingMap:Mapstring,numbernewMap();// 回调分发uri - 回调集合privatestaticcallbackMap:Mapstring,((pixelMap:image.PixelMap)void)[]newMap();/** * 请求缩略图异步返回 PixelMap。 * param asset PhotoAsset 资源 * param size 目标尺寸边长像素 */staticasyncloadThumbnail(asset:photoAccessHelper.PhotoAsset,size:number,onLoaded:(uri:string,pixelMap:image.PixelMap)void):Promisevoid{consturiasset.uri;// 1. 已请求中合并回调避免并发重复请求if(MediaLibraryService.pendingMap.has(uri)){constcallbacksMediaLibraryService.callbackMap.get(uri)??[];callbacks.push((pm)onLoaded(uri,pm));MediaLibraryService.callbackMap.set(uri,callbacks);return;}// 2. 标记请求中MediaLibraryService.pendingMap.set(uri,size);try{// 3. 系统缩略图接口目标边长 128pxconstthumbawaitasset.getThumbnail(size);// 4. 分发所有等待者constcallbacksMediaLibraryService.callbackMap.get(uri)??[];onLoaded(uri,thumb);callbacks.forEach((cb)cb(thumb));}catch(err){console.error(缩略图加载失败${uri}:${(errasError).message});}finally{// 5. 清理MediaLibraryService.pendingMap.delete(uri);MediaLibraryService.callbackMap.delete(uri);}}}4.3 缩略图去重的价值网格快速滚动时同一张图可能被多个GridItem同时请求item 复用导致。pendingMap保证同一 uri 只向系统请求一次其他请求者挂到回调队列等结果。没有这层去重快速滚动会打出几十个并发getThumbnail系统媒体服务直接过载。getThumbnail(size)的 size 是「边长上限」系统按比例缩放宽度优先。128px 缩略图显示在 90vp 的网格格子里高清无压力预览页才用getScaledImage或原图第二篇。五、收藏表SQLite 本地业务数据收藏是「业务数据」而非系统属性——系统相册没有收藏概念或与我们的收藏语义不同所以建本地表CREATETABLEIFNOTEXISTSmedia_favorite(uriTEXTPRIMARYKEY,-- 媒体 uri 主键favorited_atINTEGERNOTNULL,-- 收藏时间display_nameTEXTDEFAULT,media_typeINTEGERDEFAULT1);DAO 实现import{relationalStore}fromkit.ArkData;import{common}fromkit.AbilityKit;exportclassFavoriteDao{staticasyncgetStore(context:common.UIAbilityContext):PromiserelationalStore.RdbStore{conststoreawaitrelationalStore.getRdbStore(context,{name:media_gallery.db,securityLevel:relationalStore.SecurityLevel.S1});awaitstore.executeSql(CREATE TABLE IF NOT EXISTS media_favorite ( uri TEXT PRIMARY KEY, favorited_at INTEGER NOT NULL, display_name TEXT DEFAULT , media_type INTEGER DEFAULT 1 ));returnstore;}staticasyncadd(context:common.UIAbilityContext,item:MediaItem):Promisevoid{conststoreawaitFavoriteDao.getStore(context);awaitstore.executeSql(INSERT OR REPLACE INTO media_favorite (uri, favorited_at, display_name, media_type) VALUES (?, ?, ?, ?),[item.uri,Date.now(),item.displayName,item.mediaType]);}staticasyncremove(context:common.UIAbilityContext,uri:string):Promisevoid{conststoreawaitFavoriteDao.getStore(context);awaitstore.executeSql(DELETE FROM media_favorite WHERE uri ?,[uri]);}staticasyncisFavorite(context:common.UIAbilityContext,uri:string):Promiseboolean{conststoreawaitFavoriteDao.getStore(context);constprednewrelationalStore.RdbPredicates(media_favorite);pred.equalTo(uri,uri);constrsawaitstore.query(pred);constexistsrs.goToFirstRow();rs.close();returnexists;}staticasyncqueryAll(context:common.UIAbilityContext):Promisestring[]{conststoreawaitFavoriteDao.getStore(context);constprednewrelationalStore.RdbPredicates(media_favorite);pred.orderByDesc(favorited_at);constrsawaitstore.query(pred);consturis:string[][];while(rs.goToNextRow()){uris.push(rs.getString(rs.getColumnIndex(uri)));}rs.close();returnuris;}}uri 作主键媒体 urifile://media/Photo/xxx在系统内稳定唯一天然适合做主键。收藏表与系统相册的关系是「引用」我们只存 uri 引用不复制媒体文件——媒体文件由系统管理删除授权或照片被删时引用失效但表数据还在查询时按 uri 匹配不到即可忽略或做清理。六、启动装配媒体列表 收藏集合exportclassMediaBootstrapper{/** * 冷启动查询首屏媒体 加载收藏集合。 */staticasyncbootstrap(context:common.UIAbilityContext):Promise{items:MediaItem[];favorites:Setstring;hasMore:boolean;}{constgrantedawaitMediaPermission.ensure(context);if(!granted){return{items:[],favorites:newSet(),hasMore:false};}constitemsawaitMediaLibraryService.queryAssets(context,0,MediaLibraryService.PAGE_SIZE);constfavUrisawaitFavoriteDao.queryAll(context);// 给每条媒体打上收藏标记constfavSetnewSet(favUris);items.forEach((it)it.favoritefavSet.has(it.uri));return{items,favorites:favSet,hasMore:items.lengthMediaLibraryService.PAGE_SIZE};}}hasMore判断是否还有下一页查满一页就认为可能还有——分页加载的「是否继续」判定第二篇的滚动加载会用到。七、代码定位表代码块关注点改动入口module.json5 权限READ_IMAGEVIDEO user_grant增删媒体权限时改这里MediaPermission授权检查与申请调整授权分支时改这里MediaLibraryServiceFetchOptions 查询与分页改排序/过滤时动 selectionsThumbnailLoader缩略图去重缓存调整缓存策略时改 pendingMapFavoriteDao收藏表 CRUD增加收藏字段时改建表 SQLMediaBootstrapper启动装配调整首屏策略时改这里八、本篇小结媒体数据层把「系统媒体库」抽象成「可分页的 MediaItem 列表 可收藏的业务集合」权限走选图授权模型、查询走 FetchOptions 结构、缩略图走系统接口加去重缓存、收藏走 SQLite 本地表。四件事里最值钱的是缩略图去重——媒体应用的性能瓶颈九成在图片加载。关键记忆点READ_IMAGEVIDEO 是选图授权拿不到全部照片fetchColumns 必须显式声明结果集必须 close缩略图用 getThumbnail 而非原图解码收藏用 uri 主键引用系统媒体。