走进HarmonyOS开发中的媒体扫描

举报
Jack20 发表于 2026/06/21 11:42:55 2026/06/21
【摘要】 HarmonyOS开发中的媒体扫描:媒体文件扫描、扫描触发、扫描回调、扫描配置、增量扫描核心要点:媒体扫描是连接「文件系统」和「媒体库」的桥梁。当新文件出现在存储中时,它并不会自动出现在相册里——必须经过扫描,提取元数据后才能被媒体库索引。掌握扫描触发、回调处理、配置优化和增量扫描策略,是确保用户「拍了就能看到」的关键。项目说明核心模块@ohos.file.photoAccessHelpe...

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 迁移要点

  1. 将自行实现的增量扫描替换为 triggerMediaScan(ScanConfig) 系统级 API
  2. 使用 on('scanComplete') 替代轮询检查扫描结果
  3. 分布式扫描需要声明 ohos.permission.DISTRIBUTED_DATASYNC 权限
  4. ScanConfig 中的 since 字段使用时间戳,而非自行维护的 Map

六、总结

知识点 核心内容
媒体扫描本质 连接文件系统和媒体库的桥梁,将文件提取元数据后写入媒体库索引
扫描触发 系统自动扫描(拍照/截图)、应用主动扫描(下载/导入)、开机全量扫描
showAssetsCreationRequest 推荐的扫描入库方式,同时完成扫描和入库
createAsset 创建空壳资源 → 写入文件 → 自动触发扫描
扫描回调 通过 API 返回值获取扫描结果,不需要额外监听
增量扫描 只处理新增/变化的文件,通过记录修改时间实现去重
批量扫描 分批处理大量文件,避免内存溢出
扫描配置 ScanConfig 控制目录、扩展名、递归、上限等参数
定时扫描 适用于后台持续监控文件变化的场景
HarmonyOS 6 新增系统级增量扫描、扫描完成回调、分布式扫描等能力

💡 一句话总结:媒体扫描是「拍了就能看到」的幕后功臣。核心链路是:新文件 → 触发扫描 → 提取元数据 → 写入媒体库 → 相册可见。增量扫描只处理新增/变化的文件,是性能优化的关键。HarmonyOS 6 提供了系统级增量扫描,让开发者不再需要造轮子。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。