《HarmonyOS技术精讲-Media Library Kit》之最佳实践与常见问题

从“读取不了相册”到“自动刷新列表”: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 } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
@Entry
@Component
struct MediaListPage {
@State mediaList: Array<MediaData> = []; // 媒体文件列表
private context = getContext(this) as common.UIAbilityContext;
aboutToAppear() {
this.requestPermission();
}
async requestPermission(): Promise<boolean> {
const permissions: Array<Permissions> = ['ohos.permission.READ_MEDIA'];
// 额外提示:API 版本不同,权限声明方式可能不同,建议统一动态申请
const bundleInfo = bundleManager.getBundleInfoForSelfSync(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_PERMISSION);
const grantStatus: Array<number> = abilityAccessCtrl.createAtManager().checkAccessSync(
bundleInfo.appInfo.accessTokenId.toString(),
permissions[0]
);
if (grantStatus[0] === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
// 已经授权,直接初始化
this.initializeMediaFetch();
return true;
} else {
// 需要请求授权
try {
await abilityAccessCtrl.createAtManager().requestPermissionsFromUser(
this.context,
permissions
);
this.initializeMediaFetch();
return true;
} catch (err) {
let bizErr = err as BusinessError;
if (bizErr.code === common.UIAbilityErrorCode.UIKIT_INNER_ERROR) {
// 用户拒绝授权
console.error('Permission denied');
}
return false;
}
}
}
}
为什么这样写:checkAccessSync 同步检查当前权限状态,可以避免在没有权限时直接调用 Media Library API 抛出异常。requestPermissionsFromUser 的 catch 分支必须处理,否则应用会直接 crash。
2. 注册 onMediaChange 监听
onMediaChange 用于监听所有媒体资源的变更(新增、删除、修改)。onAlbumChange 用于监听相册级别的变更(重命名相册、删除相册等)。
大部分场景只需要 onMediaChange。
import { photoAccessHelper } from '@kit.MediaLibraryKit';
class MediaData {
uri: string = '';
displayName: string = '';
dateAdded: number = 0;
// 根据实际需要扩展字段
}
@Component
struct MediaListPage {
// ... 省略 aboveToAppear 等上下文
private helper: photoAccessHelper.PhotoAccessHelper | null = null;
private mediaListener: photoAccessHelper.MediaChangeCallback | null = null;
initializeMediaFetch() {
this.helper = photoAccessHelper.getPhotoAccessHelper(this.context);
this.fetchMediaList();
this.registerMediaChange();
}
registerMediaChange() {
this.mediaListener = {
onMediaChange: (uris: Array<string>) => {
// 注意:这个回调可能在子线程触发,不要直接在回调里 setState
console.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 获取相册中所有图片和视频。
async fetchMediaList() {
if (!this.helper) {
return;
}
try {
// 获取系统相册
const albumUri = photoAccessHelper.PhotoType.IMAGE | photoAccessHelper.PhotoType.VIDEO;
const fetchResult = await this.helper.getAssets({});
if (!fetchResult) {
return;
}
const totalCount = fetchResult.getCount();
if (totalCount === 0) {
this.mediaList = [];
return;
}
// 一次性获取所有媒体(谨慎使用,大量文件可能 OOM)
const assets: Array<photoAccessHelper.PhotoAsset> = [];
for (let i = 0; i < totalCount; i++) {
const asset = fetchResult.getObjectByIndex(i);
if (asset) {
assets.push(asset);
}
}
// 映射为 UI 数据
this.mediaList = assets.map(item => {
return {
uri: item.uri,
displayName: item.displayName,
dateAdded: item.dateAdded ?? 0,
};
});
fetchResult.close(); // 重要:释放资源
} catch (err) {
let mediaErr = err as BusinessError;
if (mediaErr.code === photoAccessHelper.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.helper && this.mediaListener) {
this.helper.unRegisterChange(this.mediaListener, (err) => {
if (err) {
console.error('unregister failed');
}
});
}
this.helper = null;
this.mediaListener = null;
}
常见问题 1:onMediaChange 触发后页面卡死
现象:在相册里批量删除 20 张图片后,App 直接白屏或者 ANR。
原因:onMediaChange 回调里执行了 fetchMediaList(),而 fetchMediaList 里用了 getAssets 获取全部资源。删除大量文件时,onMediaChange 可能被连续触发多次,导致重复的全量查询,阻塞主线程。
解决方案:加防抖。
private mediaChangeThrottleTimer: number | null = null;
registerMediaChange() {
this.mediaListener = {
onMediaChange: (uris) => {
if (this.mediaChangeThrottleTimer) {
clearTimeout(this.mediaChangeThrottleTimer);
}
this.mediaChangeThrottleTimer = setTimeout(() => {
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 的返回处理
const granted = await this.requestPermission();
if (!granted) {
// 设置一个标记,UI 层判断后展示授权引导界面
this.isPermissionDenied = true;
}
最佳实践
-
不要在 build() 中初始化 Media Library 或注册监听
ArkUI 的 build() 会被频繁调用,每次创建 helper 是资源浪费,而且重复注册会导致多个回调同时运行。在aboutToAppear中只做一次初始化。 -
养成解注册和 close FetchResult 的习惯
这两个步骤写在一起,不容易遗漏。可以封装一个统一的生命周期管理方法。 -
优先使用 onMediaChange 来触发列表刷新,而不是轮询
轮询既要耗电、又存在更新滞后。onMediaChange可以做到实时响应。
FAQ
Q:为什么真机正常,模拟器不生效?
A:模拟器没有真实的相册资源,getAssets 可能直接返回空列表。另外模拟器不支持 onMediaChange 监听,只在真机上有效。
Q:为什么页面返回后状态丢失?
A:aboutToDisappear 中解除了监听,但再次进入页面时 aboutToAppear 重新执行了申请权限的逻辑,如果权限状态未变,checkAccessSync 返回已授权,然后重新初始化,用户可以接收。
Q:为什么第一次授权成功,第二次进入却弹出权限框?
A:用户可能在系统设置中手动关闭了相册权限。需要在 aboutToAppear 或 onPageShow 中重新检查一次权限状态,如果被拒,再次申请。
更多推荐

所有评论(0)