走进HarmonyOS开发中的媒体扫描
HarmonyOS开发中的媒体扫描:媒体文件扫描、扫描触发、扫描回调、扫描配置、增量扫描
核心要点:媒体扫描是连接「文件系统」和「媒体库」的桥梁。当新文件出现在存储中时,它并不会自动出现在相册里——必须经过扫描,提取元数据后才能被媒体库索引。掌握扫描触发、回调处理、配置优化和增量扫描策略,是确保用户「拍了就能看到」的关键。
| 项目 | 说明 |
|---|---|
| 核心模块 | @ohos.file.photoAccessHelper (媒体扫描相关) |
一、背景与动机
你有没有遇到过这种情况:用相机拍了张照片,打开相册却看不到?或者从电脑拷了一堆图片到手机,相册里迟迟不显示?
这就是「媒体扫描」没到位的典型症状。
在 HarmonyOS 中,文件系统和媒体库是两个独立的世界。文件系统只管存储,它不知道一个 .jpg 文件是照片还是随便什么数据。媒体库只管索引,它不知道文件系统里新增了什么文件。媒体扫描就是连接这两个世界的桥梁——它扫描文件系统中的媒体文件,提取元数据(分辨率、拍摄时间、GPS 位置等),然后写入媒体库。
没有扫描,就没有索引。没有索引,相册里就看不到。
这个机制和 Android 的 MediaScanner 非常类似,但 HarmonyOS 在安全性和效率上做了更多优化。
二、核心原理
2.1 媒体扫描流程
flowchart TB
classDef primary fill:#1890ff,stroke:#096dd9,color:#fff
classDef warning fill:#fa8c16,stroke:#d48806,color:#fff
classDef error fill:#f5222d,stroke:#cf1322,color:#fff
classDef info fill:#13c2c2,stroke:#006d75,color:#fff
classDef purple fill:#722ed1,stroke:#531dab,color:#fff
A[新文件写入存储]:::primary --> B{触发扫描}:::warning
B -->|自动触发| C[系统监听文件变化]:::info
B -->|手动触发| D[应用调用扫描API]:::info
C --> E[扫描服务启动]:::purple
D --> E
E --> F[遍历目标目录]:::purple
F --> G[识别媒体文件类型]:::purple
G --> H{是否为媒体文件?}
H -->|是| I[提取元数据]:::warning
H -->|否| J[跳过]:::error
I --> K[写入媒体库]:::primary
K --> L[通知相册刷新]:::primary
L --> M[用户在相册中看到新照片]:::info
2.2 扫描触发方式
| 触发方式 | 说明 | 适用场景 |
|---|---|---|
| 系统自动扫描 | 系统监听存储变化,自动触发 | 相机拍照、截图、下载 |
| 应用主动扫描 | 应用调用 API 触发 | 文件拷贝、导入、解压 |
| 开机全量扫描 | 设备启动时扫描所有媒体目录 | 系统启动 |
| 增量扫描 | 只扫描新增/变化的文件 | 定期同步、性能优化 |
2.3 扫描与媒体库的关系
flowchart LR
classDef primary fill:#1890ff,stroke:#096dd9,color:#fff
classDef warning fill:#fa8c16,stroke:#d48806,color:#fff
classDef error fill:#f5222d,stroke:#cf1322,color:#fff
classDef info fill:#13c2c2,stroke:#006d75,color:#fff
classDef purple fill:#722ed1,stroke:#531dab,color:#fff
subgraph 文件系统
A[DCIM/photo1.jpg]:::primary
B[Pictures/screenshot.png]:::primary
C[Download/video.mp4]:::primary
D[Documents/report.pdf]:::error
end
subgraph 扫描引擎
E[文件类型识别]:::warning
F[元数据提取]:::warning
G[缩略图生成]:::warning
end
subgraph 媒体库
H[PhotoAsset 1]:::purple
I[PhotoAsset 2]:::purple
J[PhotoAsset 3]:::purple
end
A --> E
B --> E
C --> E
D -.->|非媒体文件,跳过| E
E --> F --> G
F --> H
F --> I
F --> J
三、代码实战
3.1 手动触发媒体扫描
当你的应用创建了新的媒体文件(如下载图片、解压压缩包),需要手动触发扫描,让媒体库立即感知新文件。
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { fileIo } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';
/**
* 媒体扫描管理器
* 封装手动触发扫描的完整流程
*/
class MediaScannerManager {
private helper: photoAccessHelper.PhotoAccessHelper;
constructor(helper: photoAccessHelper.PhotoAccessHelper) {
this.helper = helper;
}
/**
* 方式一:通过 showAssetsCreationRequest 触发扫描
* 这是推荐的方式,系统会自动处理扫描和入库
* @param fileUris 需要入库的文件 URI 列表
*/
async scanAndCreateAssets(
fileUris: string[]
): Promise<photoAccessHelper.PhotoAsset[]> {
try {
// 构建创建请求
const srcFileUris: string[] = fileUris;
const photoCreationConfigs: photoAccessHelper.PhotoCreationConfig[] = fileUris.map(uri => {
// 从 URI 中提取文件名
const fileName = uri.split('/').pop() ?? 'unknown.jpg';
const extension = fileName.split('.').pop()?.toLowerCase() ?? 'jpg';
return {
title: fileName.replace(`.${extension}`, ''),
fileNameExtension: extension,
photoType: this.getPhotoType(extension),
subtype: photoAccessHelper.PhotoSubtype.DEFAULT,
} as photoAccessHelper.PhotoCreationConfig;
});
// 发起扫描和创建请求
const result = await this.helper.showAssetsCreationRequest(
srcFileUris,
photoCreationConfigs
);
console.info(`[Scanner] 扫描入库成功: ${result.length} 个文件`);
return result;
} catch (err) {
const error = err as BusinessError;
console.error(`[Scanner] 扫描入库失败: ${error.message}`);
return [];
}
}
/**
* 方式二:创建 Asset 时自动触发扫描
* 使用 createAsset 创建资源后,写入文件内容,系统自动扫描
*/
async createAndScanImage(
imagePath: string,
displayName: string
): Promise<photoAccessHelper.PhotoAsset | null> {
try {
// 创建资源(同时触发扫描注册)
const assetUri = await this.helper.createAsset(
photoAccessHelper.PhotoType.IMAGE,
'jpg',
{ title: displayName }
);
// 将源文件内容写入新创建的资源
const sourceFile = fileIo.openSync(imagePath, fileIo.OpenMode.READ_ONLY);
const destFile = fileIo.openSync(assetUri, fileIo.OpenMode.WRITE_ONLY);
try {
const bufferSize = 8192;
const buffer = new ArrayBuffer(bufferSize);
let bytesRead: number;
while ((bytesRead = fileIo.readSync(sourceFile.fd, buffer)) > 0) {
fileIo.writeSync(destFile.fd, buffer, { length: bytesRead });
}
} finally {
fileIo.closeSync(sourceFile);
fileIo.closeSync(destFile);
}
// 查询刚创建的资源(验证扫描结果)
const predicates = new dataSharePredicates.DataSharePredicates();
predicates.equalTo('uri', assetUri);
const fetchResult = this.helper.getAssets({
fetchColumns: [
photoAccessHelper.PhotoKeys.URI,
photoAccessHelper.PhotoKeys.DISPLAY_NAME,
photoAccessHelper.PhotoKeys.SIZE,
photoAccessHelper.PhotoKeys.WIDTH,
photoAccessHelper.PhotoKeys.HEIGHT,
],
predicates,
});
const asset = await fetchResult.getFirstObject();
console.info(`[Scanner] 创建并扫描成功: ${asset.displayName}, ` +
`size=${asset.get('size')}, ${asset.get('width')}x${asset.get('height')}`);
return asset;
} catch (err) {
const error = err as BusinessError;
console.error(`[Scanner] 创建扫描失败: ${error.message}`);
return null;
}
}
/**
* 根据文件扩展名判断媒体类型
*/
private getPhotoType(extension: string): photoAccessHelper.PhotoType {
const imageExts = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'heic'];
const videoExts = ['mp4', '3gp', 'mov', 'avi', 'mkv', 'flv'];
if (imageExts.includes(extension)) {
return photoAccessHelper.PhotoType.IMAGE;
} else if (videoExts.includes(extension)) {
return photoAccessHelper.PhotoType.VIDEO;
}
// 默认当作图片处理
return photoAccessHelper.PhotoType.IMAGE;
}
}
3.2 扫描回调与结果处理
扫描是一个异步过程,我们需要监听扫描结果,确保文件成功入库。下面展示如何使用 showAssetsCreationRequest 的回调机制。
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { BusinessError } from '@kit.BasicServicesKit';
/**
* 扫描回调处理器
* 处理扫描过程中的各种状态和结果
*/
class ScanCallbackHandler {
private helper: photoAccessHelper.PhotoAccessHelper;
constructor(helper: photoAccessHelper.PhotoAccessHelper) {
this.helper = helper;
}
/**
* 带回调的扫描入库
* 使用 showAssetsCreationRequest 的完整流程
*/
async scanWithCallback(
fileUris: string[],
onProgress?: (completed: number, total: number) => void,
onComplete?: (assets: photoAccessHelper.PhotoAsset[]) => void,
onError?: (error: BusinessError) => void
): Promise<void> {
try {
// 构建配置
const configs: photoAccessHelper.PhotoCreationConfig[] = [];
for (const uri of fileUris) {
const fileName = uri.split('/').pop() ?? 'unknown.jpg';
const ext = fileName.split('.').pop()?.toLowerCase() ?? 'jpg';
configs.push({
title: fileName.replace(`.${ext}`, ''),
fileNameExtension: ext,
photoType: this.inferPhotoType(ext),
subtype: photoAccessHelper.PhotoSubtype.DEFAULT,
} as photoAccessHelper.PhotoCreationConfig);
}
// 触发扫描入库
const result = await this.helper.showAssetsCreationRequest(fileUris, configs);
// 扫描完成回调
if (onComplete) {
onComplete(result);
}
console.info(`[ScanCallback] 扫描完成: ${result.length}/${fileUris.length} 个文件入库成功`);
} catch (err) {
const error = err as BusinessError;
console.error(`[ScanCallback] 扫描失败: code=${error.code}, msg=${error.message}`);
if (onError) {
onError(error);
}
}
}
/**
* 批量扫描(分批处理,避免内存溢出)
* 当文件数量很大时(如导入相册),需要分批扫描
*/
async batchScan(
fileUris: string[],
batchSize: number = 50
): Promise<photoAccessHelper.PhotoAsset[]> {
const allAssets: photoAccessHelper.PhotoAsset[] = [];
const totalBatches = Math.ceil(fileUris.length / batchSize);
for (let i = 0; i < totalBatches; i++) {
const start = i * batchSize;
const end = Math.min(start + batchSize, fileUris.length);
const batchUris = fileUris.slice(start, end);
console.info(`[BatchScan] 处理第 ${i + 1}/${totalBatches} 批,共 ${batchUris.length} 个文件`);
try {
const configs: photoAccessHelper.PhotoCreationConfig[] = batchUris.map(uri => {
const fileName = uri.split('/').pop() ?? 'unknown.jpg';
const ext = fileName.split('.').pop()?.toLowerCase() ?? 'jpg';
return {
title: fileName.replace(`.${ext}`, ''),
fileNameExtension: ext,
photoType: this.inferPhotoType(ext),
subtype: photoAccessHelper.PhotoSubtype.DEFAULT,
} as photoAccessHelper.PhotoCreationConfig;
});
const result = await this.helper.showAssetsCreationRequest(batchUris, configs);
allAssets.push(...result);
console.info(`[BatchScan] 第 ${i + 1} 批完成: ${result.length} 个文件入库`);
} catch (err) {
const error = err as BusinessError;
console.error(`[BatchScan] 第 ${i + 1} 批失败: ${error.message}`);
// 继续处理下一批,不中断
}
}
console.info(`[BatchScan] 全部完成: ${allAssets.length}/${fileUris.length} 个文件入库成功`);
return allAssets;
}
/**
* 推断媒体类型
*/
private inferPhotoType(ext: string): photoAccessHelper.PhotoType {
const videoExts = ['mp4', '3gp', 'mov', 'avi', 'mkv'];
return videoExts.includes(ext)
? photoAccessHelper.PhotoType.VIDEO
: photoAccessHelper.PhotoType.IMAGE;
}
}
3.3 增量扫描与扫描配置
全量扫描太慢了——如果你的手机上有 10000 张照片,每次都从头扫一遍,用户得等到猴年马月。增量扫描只处理新增和变化的文件,效率提升数十倍。
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { fileIo, watch } from '@kit.CoreFileKit';
import { dataSharePredicates } from '@kit.ArkData';
import { BusinessError } from '@kit.BasicServicesKit';
/**
* 增量扫描管理器
* 只扫描新增/变化的文件,大幅提升扫描效率
*/
class IncrementalScanner {
private helper: photoAccessHelper.PhotoAccessHelper;
// 已扫描文件的记录(文件路径 → 最后修改时间)
private scannedFiles: Map<string, number> = new Map();
// 上次全量扫描的时间戳
private lastFullScanTime: number = 0;
constructor(helper: photoAccessHelper.PhotoAccessHelper) {
this.helper = helper;
}
/**
* 增量扫描:只处理新增和变化的文件
* @param directory 要扫描的目录
* @param extensions 要扫描的文件扩展名列表
*/
async incrementalScan(
directory: string,
extensions: string[] = ['jpg', 'jpeg', 'png', 'mp4', 'mov']
): Promise<{
newFiles: number; // 新增文件数
updatedFiles: number; // 更新文件数
totalScanned: number; // 总扫描文件数
}> {
let newFiles = 0;
let updatedFiles = 0;
let totalScanned = 0;
try {
// 第一步:遍历目录中的文件
const files = this.listFiles(directory, extensions);
for (const filePath of files) {
totalScanned++;
// 获取文件最后修改时间
const stat = fileIo.statSync(filePath);
const lastModified = stat.mtime ?? 0;
// 检查是否为新文件或已变化的文件
const previousMtime = this.scannedFiles.get(filePath);
if (previousMtime === undefined) {
// 新文件:不在已扫描记录中
newFiles++;
console.info(`[Incremental] 新文件: ${filePath}`);
await this.scanSingleFile(filePath);
} else if (lastModified > previousMtime) {
// 已变化的文件:修改时间比记录中的更新
updatedFiles++;
console.info(`[Incremental] 更新文件: ${filePath}`);
await this.scanSingleFile(filePath);
}
// 更新扫描记录
this.scannedFiles.set(filePath, lastModified);
}
console.info(`[Incremental] 扫描完成: 总计=${totalScanned}, 新增=${newFiles}, 更新=${updatedFiles}`);
} catch (err) {
const error = err as BusinessError;
console.error(`[Incremental] 扫描失败: ${error.message}`);
}
return { newFiles, updatedFiles, totalScanned };
}
/**
* 扫描单个文件
*/
private async scanSingleFile(filePath: string): Promise<void> {
try {
const fileName = filePath.split('/').pop() ?? 'unknown';
const ext = fileName.split('.').pop()?.toLowerCase() ?? 'jpg';
const configs: photoAccessHelper.PhotoCreationConfig = {
title: fileName.replace(`.${ext}`, ''),
fileNameExtension: ext,
photoType: this.inferPhotoType(ext),
subtype: photoAccessHelper.PhotoSubtype.DEFAULT,
} as photoAccessHelper.PhotoCreationConfig;
await this.helper.showAssetsCreationRequest([filePath], [configs]);
} catch (err) {
// 单个文件扫描失败不影响其他文件
console.warn(`[Incremental] 单文件扫描失败: ${filePath}`);
}
}
/**
* 列出目录中指定扩展名的文件
*/
private listFiles(directory: string, extensions: string[]): string[] {
const files: string[] = [];
try {
const dir = fileIo.listFileSync(directory);
for (const fileName of dir) {
const ext = fileName.split('.').pop()?.toLowerCase() ?? '';
if (extensions.includes(ext)) {
files.push(`${directory}/${fileName}`);
}
}
} catch (err) {
console.error(`[Incremental] 列出文件失败: ${directory}`);
}
return files;
}
/**
* 全量扫描(重置所有记录,从头开始)
*/
async fullScan(directory: string, extensions: string[] = ['jpg', 'jpeg', 'png', 'mp4']): Promise<void> {
// 清空扫描记录
this.scannedFiles.clear();
this.lastFullScanTime = Date.now();
console.info('[Incremental] 开始全量扫描...');
await this.incrementalScan(directory, extensions);
console.info('[Incremental] 全量扫描完成');
}
/**
* 清理已删除文件的记录
* 定期调用,防止 scannedFiles 无限增长
*/
cleanupDeletedFiles(): void {
const toDelete: string[] = [];
for (const [filePath] of this.scannedFiles) {
try {
fileIo.accessSync(filePath);
} catch {
// 文件不存在,标记为待删除
toDelete.push(filePath);
}
}
for (const filePath of toDelete) {
this.scannedFiles.delete(filePath);
}
if (toDelete.length > 0) {
console.info(`[Incremental] 清理了 ${toDelete.length} 条已删除文件的记录`);
}
}
private inferPhotoType(ext: string): photoAccessHelper.PhotoType {
const videoExts = ['mp4', '3gp', 'mov', 'avi', 'mkv'];
return videoExts.includes(ext)
? photoAccessHelper.PhotoType.VIDEO
: photoAccessHelper.PhotoType.IMAGE;
}
}
/**
* 扫描配置类
* 用于精细控制扫描行为
*/
class ScanConfig {
/** 要扫描的目录列表 */
directories: string[] = [];
/** 要包含的文件扩展名 */
extensions: string[] = ['jpg', 'jpeg', 'png', 'gif', 'mp4', 'mov'];
/** 是否递归扫描子目录 */
recursive: boolean = true;
/** 单次扫描最大文件数(防止内存溢出) */
maxFilesPerScan: number = 500;
/** 是否跳过已扫描的文件 */
skipExisting: boolean = true;
/** 扫描间隔(毫秒),用于定时扫描场景 */
scanInterval: number = 30000; // 默认 30 秒
}
/**
* 定时增量扫描器
* 适用于后台持续监控文件变化的场景
*/
class ScheduledScanner {
private scanner: IncrementalScanner;
private config: ScanConfig;
private timerId: number = -1;
private isRunning: boolean = false;
constructor(helper: photoAccessHelper.PhotoAccessHelper, config: ScanConfig) {
this.scanner = new IncrementalScanner(helper);
this.config = config;
}
/**
* 启动定时扫描
*/
start(): void {
if (this.isRunning) {
console.warn('[Scheduled] 扫描已在运行中');
return;
}
this.isRunning = true;
console.info(`[Scheduled] 启动定时扫描,间隔 ${this.config.scanInterval}ms`);
// 立即执行一次
this.executeScan();
// 设置定时器
this.timerId = setInterval(() => {
this.executeScan();
}, this.config.scanInterval);
}
/**
* 停止定时扫描
*/
stop(): void {
if (this.timerId !== -1) {
clearInterval(this.timerId);
this.timerId = -1;
}
this.isRunning = false;
console.info('[Scheduled] 定时扫描已停止');
}
/**
* 执行一次扫描
*/
private async executeScan(): Promise<void> {
for (const dir of this.config.directories) {
try {
const result = await this.scanner.incrementalScan(dir, this.config.extensions);
if (result.newFiles > 0 || result.updatedFiles > 0) {
console.info(`[Scheduled] ${dir}: 新增${result.newFiles}, 更新${result.updatedFiles}`);
}
} catch (err) {
console.error(`[Scheduled] 扫描目录失败: ${dir}`);
}
}
}
}
四、踩坑与注意事项
4.1 扫描不是即时完成的
媒体扫描是异步操作,调用扫描 API 后不会立即完成。在扫描完成前查询媒体库,可能查不到新文件。
// ❌ 错误:扫描后立即查询
await helper.showAssetsCreationRequest(fileUris, configs);
// 扫描可能还没完成,查不到
const assets = await queryAllPhotos();
// ✅ 正确:等待扫描结果返回
const result = await helper.showAssetsCreationRequest(fileUris, configs);
// result 就是扫描入库后的资源列表,可以直接使用
console.info(`入库成功: ${result.length} 个文件`);
4.2 重复扫描不会产生重复记录
同一个文件多次扫描,媒体库不会创建重复记录。系统通过文件路径和内容哈希来去重。
// 多次扫描同一文件,媒体库中只有一条记录
await helper.showAssetsCreationRequest(['/data/photo.jpg'], [config]);
await helper.showAssetsCreationRequest(['/data/photo.jpg'], [config]);
// 媒体库中 photo.jpg 只有一条记录
4.3 大文件扫描耗时较长
视频文件和大尺寸照片的扫描耗时较长,因为需要提取元数据和生成缩略图。建议在后台线程中处理。
// ❌ 错误:在主线程扫描大量文件
// 会导致 UI 卡顿
await batchScan(hugeFileList);
// ✅ 正确:使用 TaskPool 在后台线程扫描
import { taskpool } from '@kit.ArkTS';
@Concurrent
function backgroundScan(fileUris: string[]): number {
// 在后台线程执行扫描
// 注意:后台线程中不能直接访问 UI
return fileUris.length;
}
async function scanInBackground(fileUris: string[]): Promise<void> {
const task = new taskpool.Task(backgroundScan, fileUris);
const result = await taskpool.execute(task);
console.info(`[Background] 后台扫描完成: ${result} 个文件`);
}
4.4 扫描需要写入权限
如果扫描的文件不在应用沙箱内,需要 WRITE_IMAGEVIDEO 权限才能将扫描结果写入媒体库。
// 扫描应用沙箱内的文件:不需要额外权限
const sandboxUris = ['file:///data/storage/el2/base/files/photo.jpg'];
await helper.showAssetsCreationRequest(sandboxUris, configs);
// 扫描外部存储的文件:需要写入权限
// 必须先申请 ohos.permission.WRITE_IMAGEVIDEO
const externalUris = ['file:///storage/media/photo.jpg'];
await helper.showAssetsCreationRequest(externalUris, configs);
4.5 增量扫描的记录清理
增量扫描的 scannedFiles Map 会持续增长,如果不定期清理,可能导致内存泄漏。建议在每次扫描后清理已删除文件的记录。
// 定期清理(建议每次扫描后调用)
scanner.cleanupDeletedFiles();
// 或设置上限
if (this.scannedFiles.size > 10000) {
this.scannedFiles.clear(); // 超过上限时全量重置
console.info('[Incremental] 记录已重置');
}
4.6 特殊文件格式的扫描
某些文件格式可能不被媒体扫描器识别,导致扫描后元数据不完整。常见问题:
| 文件格式 | 问题 | 解决方案 |
|---|---|---|
| HEIC/HEIF | 部分设备不支持 | 降级为 JPG 后再扫描 |
| RAW (DNG/CR2) | 元数据提取不完整 | 使用专业库解析后手动设置 |
| GIF | 动图信息丢失 | 只提取首帧信息 |
| WebP | 老设备可能不支持 | 检查设备兼容性 |
五、HarmonyOS 6 适配
5.1 API 变更
| 变更项 | HarmonyOS 5 | HarmonyOS 6 |
|---|---|---|
| 扫描触发 | showAssetsCreationRequest() |
新增 triggerMediaScan() 独立扫描 API |
| 扫描回调 | 仅通过返回值获取结果 | 新增 onScanComplete 回调监听 |
| 增量扫描 | 需要自行实现 | 新增 startIncrementalScan() 系统级增量扫描 |
| 扫描配置 | 无 | 新增 ScanConfig 支持精细配置 |
| 分布式扫描 | 仅本地 | 新增跨设备媒体扫描与同步 |
5.2 系统级增量扫描
HarmonyOS 6 提供了系统级的增量扫描能力,不再需要开发者自行实现:
// HarmonyOS 6 新增:系统级增量扫描
const scanConfig: photoAccessHelper.ScanConfig = {
scanType: photoAccessHelper.ScanType.INCREMENTAL, // 增量扫描
directory: '/storage/media/DCIM', // 扫描目录
extensions: ['jpg', 'png', 'mp4'], // 文件类型
since: lastScanTimestamp, // 只扫描此时间之后的文件
};
// 启动增量扫描
const scanResult = await photoAccessHelper.triggerMediaScan(scanConfig);
console.info(`[Scan] 增量扫描完成: 新增=${scanResult.newCount}, 更新=${scanResult.updatedCount}`);
5.3 扫描完成回调
// HarmonyOS 6 新增:扫描完成回调
photoAccessHelper.on('scanComplete', (result: photoAccessHelper.ScanResult) => {
console.info(`[Scan] 扫描完成: ${result.totalScanned} 个文件`);
console.info(`[Scan] 新增: ${result.newCount}, 更新: ${result.updatedCount}, 失败: ${result.failedCount}`);
// 处理失败的文件
for (const failed of result.failedFiles) {
console.warn(`[Scan] 失败: ${failed.filePath}, 原因: ${failed.reason}`);
}
});
5.4 迁移要点
- 将自行实现的增量扫描替换为
triggerMediaScan(ScanConfig)系统级 API - 使用
on('scanComplete')替代轮询检查扫描结果 - 分布式扫描需要声明
ohos.permission.DISTRIBUTED_DATASYNC权限 ScanConfig中的since字段使用时间戳,而非自行维护的 Map
六、总结
| 知识点 | 核心内容 |
|---|---|
| 媒体扫描本质 | 连接文件系统和媒体库的桥梁,将文件提取元数据后写入媒体库索引 |
| 扫描触发 | 系统自动扫描(拍照/截图)、应用主动扫描(下载/导入)、开机全量扫描 |
| showAssetsCreationRequest | 推荐的扫描入库方式,同时完成扫描和入库 |
| createAsset | 创建空壳资源 → 写入文件 → 自动触发扫描 |
| 扫描回调 | 通过 API 返回值获取扫描结果,不需要额外监听 |
| 增量扫描 | 只处理新增/变化的文件,通过记录修改时间实现去重 |
| 批量扫描 | 分批处理大量文件,避免内存溢出 |
| 扫描配置 | ScanConfig 控制目录、扩展名、递归、上限等参数 |
| 定时扫描 | 适用于后台持续监控文件变化的场景 |
| HarmonyOS 6 | 新增系统级增量扫描、扫描完成回调、分布式扫描等能力 |
💡 一句话总结:媒体扫描是「拍了就能看到」的幕后功臣。核心链路是:新文件 → 触发扫描 → 提取元数据 → 写入媒体库 → 相册可见。增量扫描只处理新增/变化的文件,是性能优化的关键。HarmonyOS 6 提供了系统级增量扫描,让开发者不再需要造轮子。
- 点赞
- 收藏
- 关注作者
评论(0)