HarmonyOS开发中的媒体库

举报
Jack20 发表于 2026/06/21 11:41:39 2026/06/21
【摘要】 HarmonyOS开发中的媒体库:@ohos.file.photoAccessHelper、相册管理、媒体文件查询、媒体文件增删改、媒体库权限核心要点:photoAccessHelper 是 HarmonyOS 媒体库的「大门」,它提供了对系统相册、照片、视频的完整访问能力。掌握权限申请、相册管理、媒体文件 CRUD 操作,是构建相册类、图片编辑类、社交分享类应用的必备技能。项目说明核心模...

HarmonyOS开发中的媒体库:@ohos.file.photoAccessHelper、相册管理、媒体文件查询、媒体文件增删改、媒体库权限

核心要点:photoAccessHelper 是 HarmonyOS 媒体库的「大门」,它提供了对系统相册、照片、视频的完整访问能力。掌握权限申请、相册管理、媒体文件 CRUD 操作,是构建相册类、图片编辑类、社交分享类应用的必备技能。

项目 说明
核心模块 @ohos.file.photoAccessHelper

一、背景与动机

你有没有想过,当你打开手机相册,那些照片是从哪里来的?它们并不是散落在文件系统各处的「野文件」,而是被一个叫做「媒体库」的系统服务统一管理的。

媒体库就像一个超级管家,它知道你手机上每一张照片、每一个视频的来龙去脉——什么时候拍的、在哪里拍的、文件有多大、缩略图在哪。而 photoAccessHelper 就是你和这个管家对话的「窗口」。

为什么不能直接读写文件?因为 HarmonyOS 的安全模型不允许应用随意访问用户数据。你必须通过 photoAccessHelper 这个「合法通道」,在用户授权的前提下,才能操作相册中的媒体文件。

典型的使用场景:

  1. 相册应用——浏览、管理、编辑照片和视频
  2. 图片编辑器——读取照片、保存编辑后的照片
  3. 社交分享——从相册选择照片/视频进行分享
  4. 文件管理器——查看和管理媒体文件
  5. 云同步——备份和恢复照片/视频

二、核心原理

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

    subgraph 应用层
        A[相册App]:::primary
        B[编辑器App]:::primary
        C[分享App]:::primary
    end

    subgraph photoAccessHelper API
        D[权限管理]:::warning
        E[相册管理]:::warning
        F[媒体文件CRUD]:::warning
        G[文件URI操作]:::warning
    end

    subgraph 媒体库服务
        H[MediaStore]:::purple
    end

    subgraph 存储层
        I[内部存储]:::info
        J[外部存储]:::info
        K[云存储]:::info
    end

    A --> D & E & F
    B --> D & F & G
    C --> D & F
    D --> H
    E --> H
    F --> H
    G --> H
    H --> I & J & K

2.2 核心数据模型

媒体库的核心数据模型有三个:

概念 类名 说明
媒体资源 PhotoAsset 一张照片或一个视频,包含文件路径、大小、日期等属性
相册 Album 媒体资源的集合,如「相机」「截图」「收藏」等
用户相册 UserAlbum 用户自定义的相册,如「旅行」「美食」等

2.3 权限体系

访问媒体库需要申请权限,不同操作需要不同级别的权限:

权限 级别 说明
ohos.permission.READ_IMAGEVIDEO user_granted 读取照片和视频
ohos.permission.WRITE_IMAGEVIDEO user_granted 写入(增删改)照片和视频
ohos.permission.READ_PHOTO_ACCESS system_granted 系统级读取权限(仅系统应用)

⚠️ user_granted 类型的权限需要用户在运行时授权,不能静默获取。


三、代码实战

3.1 权限申请与媒体库初始化

这是所有媒体库操作的前提——先申请权限,再获取 phAccessHelper 实例。

import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { abilityAccessCtrl, bundleManager, Permissions } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

/**
 * 媒体库权限管理器
 * 封装权限申请的完整流程
 */
class MediaPermissionManager {
  // 需要申请的权限列表
  private readonly REQUIRED_PERMISSIONS: Permissions[] = [
    'ohos.permission.READ_IMAGEVIDEO',
    'ohos.permission.WRITE_IMAGEVIDEO',
  ];

  /**
   * 检查是否已授权
   */
  async checkPermissions(): Promise<boolean> {
    const atManager = abilityAccessCtrl.createAtManager();
    const bundleInfo = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_DEFAULT);
    const bundleName = bundleInfo.name;

    for (const permission of this.REQUIRED_PERMISSIONS) {
      const grantStatus = await atManager.checkAccessToken(bundleName, permission);
      if (grantStatus !== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
        return false;
      }
    }
    return true;
  }

  /**
   * 请求权限(弹出系统授权对话框)
   */
  async requestPermissions(context: Context): Promise<boolean> {
    const atManager = abilityAccessCtrl.createAtManager();

    try {
      const result = await atManager.requestPermissionsFromUser(
        context,
        this.REQUIRED_PERMISSIONS
      );

      // 检查所有权限是否都已授权
      for (let i = 0; i < result.authResults.length; i++) {
        if (result.authResults[i] !== 0) {
          console.warn(`[Permission] 权限被拒绝: ${this.REQUIRED_PERMISSIONS[i]}`);
          return false;
        }
      }

      console.info('[Permission] 所有权限已授权');
      return true;
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[Permission] 请求权限失败: ${error.message}`);
      return false;
    }
  }

  /**
   * 确保权限已授予(先检查,未授予则请求)
   */
  async ensurePermissions(context: Context): Promise<boolean> {
    const hasPermission = await this.checkPermissions();
    if (hasPermission) return true;
    return await this.requestPermissions(context);
  }
}

/**
 * 获取 phAccessHelper 实例
 * 这是访问媒体库的入口
 */
function getPhotoAccessHelper(context: Context): photoAccessHelper.PhotoAccessHelper {
  return photoAccessHelper.getPhotoAccessHelper(context);
}

// ========== 使用示例 ==========
const permissionManager = new MediaPermissionManager();

async function initMediaLibrary(context: Context): Promise<photoAccessHelper.PhotoAccessHelper | null> {
  const granted = await permissionManager.ensurePermissions(context);
  if (!granted) {
    console.error('[MediaLib] 权限未授予,无法访问媒体库');
    return null;
  }
  return getPhotoAccessHelper(context);
}

3.2 相册管理与媒体文件查询

查询是媒体库最常用的操作。下面展示如何查询相册列表、查询相册中的媒体文件,以及使用条件筛选。

import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { dataSharePredicates } from '@kit.ArkData';
import { BusinessError } from '@kit.BasicServicesKit';

/**
 * 相册与媒体文件查询管理器
 */
class AlbumQueryManager {
  private helper: photoAccessHelper.PhotoAccessHelper;

  constructor(helper: photoAccessHelper.PhotoAccessHelper) {
    this.helper = helper;
  }

  /**
   * 获取所有相册
   * 包括系统相册(相机、截图等)和用户相册
   */
  async getAllAlbums(): Promise<photoAccessHelper.Album[]> {
    try {
      // 获取系统相册
      const fetchOptions: photoAccessHelper.FetchOptions = {
        fetchColumns: [
          photoAccessHelper.AlbumKey.ALBUM_ID,
          photoAccessHelper.AlbumKey.ALBUM_NAME,
          photoAccessHelper.AlbumKey.ALBUM_COUNT,
          photoAccessHelper.AlbumKey.ALBUM_COVER_URI,
        ],
        predicates: new dataSharePredicates.DataSharePredicates(),
      };

      const albumFetchResult = this.helper.getAlbums(
        photoAccessHelper.AlbumType.USER,      // 用户相册
        photoAccessHelper.AlbumSubType.USER_GENERIC,  // 通用子类型
        fetchOptions
      );

      const albums: photoAccessHelper.Album[] = [];
      const album = await albumFetchResult.getFirstObject();
      albums.push(album);

      // 遍历剩余相册
      while (true) {
        try {
          const nextAlbum = await albumFetchResult.getNextObject();
          albums.push(nextAlbum);
        } catch (err) {
          // 遍历结束
          break;
        }
      }

      console.info(`[Album] 查询到 ${albums.length} 个相册`);
      return albums;
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[Album] 查询相册失败: ${error.message}`);
      return [];
    }
  }

  /**
   * 查询指定相册中的所有照片
   */
  async getPhotosInAlbum(album: photoAccessHelper.Album): Promise<photoAccessHelper.PhotoAsset[]> {
    try {
      const fetchOptions: photoAccessHelper.FetchOptions = {
        fetchColumns: [
          photoAccessHelper.PhotoKeys.URI,
          photoAccessHelper.PhotoKeys.DISPLAY_NAME,
          photoAccessHelper.PhotoKeys.DATE_ADDED,
          photoAccessHelper.PhotoKeys.DATE_MODIFIED,
          photoAccessHelper.PhotoKeys.SIZE,
          photoAccessHelper.PhotoKeys.WIDTH,
          photoAccessHelper.PhotoKeys.HEIGHT,
          photoAccessHelper.PhotoKeys.ORIENTATION,
        ],
        predicates: new dataSharePredicates.DataSharePredicates(),
      };

      const photoFetchResult = album.getAssets(fetchOptions);
      const photos: photoAccessHelper.PhotoAsset[] = [];

      const firstPhoto = await photoFetchResult.getFirstObject();
      photos.push(firstPhoto);

      while (true) {
        try {
          const nextPhoto = await photoFetchResult.getNextObject();
          photos.push(nextPhoto);
        } catch (err) {
          break;
        }
      }

      console.info(`[Album] 相册 "${album.albumName}" 中有 ${photos.length} 张照片`);
      return photos;
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[Album] 查询照片失败: ${error.message}`);
      return [];
    }
  }

  /**
   * 按条件查询媒体文件
   * 支持按类型、日期、大小等条件筛选
   */
  async queryPhotosWithFilter(options: {
    type?: 'IMAGE' | 'VIDEO' | 'ALL';  // 媒体类型
    dateFrom?: number;                  // 起始日期(时间戳)
    dateTo?: number;                    // 结束日期(时间戳)
    limit?: number;                     // 返回数量限制
  }): Promise<photoAccessHelper.PhotoAsset[]> {
    try {
      // 构建查询条件
      const predicates = new dataSharePredicates.DataSharePredicates();

      // 按媒体类型筛选
      if (options.type && options.type !== 'ALL') {
        const mediaType = options.type === 'IMAGE'
          ? photoAccessHelper.PhotoType.IMAGE
          : photoAccessHelper.PhotoType.VIDEO;
        predicates.equalTo('media_type', mediaType);
      }

      // 按日期范围筛选
      if (options.dateFrom) {
        predicates.greaterThanOrEqualTo('date_added', options.dateFrom);
      }
      if (options.dateTo) {
        predicates.lessThanOrEqualTo('date_added', options.dateTo);
      }

      // 按添加日期倒序排列(最新的在前)
      predicates.orderByDesc('date_added');

      // 数量限制
      if (options.limit) {
        predicates.limit(options.limit);
      }

      const fetchOptions: photoAccessHelper.FetchOptions = {
        fetchColumns: [
          photoAccessHelper.PhotoKeys.URI,
          photoAccessHelper.PhotoKeys.DISPLAY_NAME,
          photoAccessHelper.PhotoKeys.DATE_ADDED,
          photoAccessHelper.PhotoKeys.SIZE,
          photoAccessHelper.PhotoKeys.WIDTH,
          photoAccessHelper.PhotoKeys.HEIGHT,
        ],
        predicates,
      };

      const fetchResult = this.helper.getAssets(fetchOptions);
      const photos: photoAccessHelper.PhotoAsset[] = [];

      const first = await fetchResult.getFirstObject();
      photos.push(first);

      while (true) {
        try {
          const next = await fetchResult.getNextObject();
          photos.push(next);
        } catch (err) {
          break;
        }
      }

      console.info(`[Query] 查询到 ${photos.length} 个媒体文件`);
      return photos;
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[Query] 条件查询失败: ${error.message}`);
      return [];
    }
  }
}

3.3 媒体文件增删改

媒体文件的创建、删除和修改是敏感操作,需要特别小心。下面展示完整的 CRUD 流程。

import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { fileIo } from '@kit.CoreFileKit';
import { BusinessError } from '@kit.BasicServicesKit';

/**
 * 媒体文件 CRUD 管理器
 */
class MediaFileManager {
  private helper: photoAccessHelper.PhotoAccessHelper;

  constructor(helper: photoAccessHelper.PhotoAccessHelper) {
    this.helper = helper;
  }

  // ========== 创建(保存照片到相册) ==========

  /**
   * 创建一张照片资源
   * 典型场景:拍照后保存、编辑后另存为
   * @param fileName 文件名(如 'photo_20260620.jpg')
   * @param photoType 照片类型(IMAGE 或 VIDEO)
   * @param writeFileCallback 写入文件的回调
   */
  async createPhotoAsset(
    fileName: string,
    photoType: photoAccessHelper.PhotoType,
    writeFileCallback: (fileUri: string) => Promise<void>
  ): Promise<photoAccessHelper.PhotoAsset | null> {
    try {
      // 第一步:创建媒体资源(获得一个安全 URI)
      const createOption: photoAccessHelper.CreateOptions = {
        title: fileName,  // 文件标题
        subtype: photoAccessHelper.PhotoSubtype.DEFAULT,  // 子类型
      };

      const assetUri = await this.helper.createAsset(photoType, 'jpg', createOption);
      console.info(`[Create] 创建资源成功,URI: ${assetUri}`);

      // 第二步:通过 URI 打开文件,写入数据
      const file = fileIo.openSync(assetUri, fileIo.OpenMode.WRITE_ONLY);
      try {
        // 调用回调让调用者写入文件内容
        await writeFileCallback(assetUri);
        console.info('[Create] 文件写入完成');
      } finally {
        fileIo.closeSync(file.fd);
      }

      // 第三步:验证创建结果
      const predicates = new dataSharePredicates.DataSharePredicates();
      predicates.equalTo('uri', assetUri);
      const fetchResult = this.helper.getAssets({
        fetchColumns: [photoAccessHelper.PhotoKeys.URI],
        predicates,
      });
      const asset = await fetchResult.getFirstObject();
      return asset;
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[Create] 创建照片失败: ${error.message}`);
      return null;
    }
  }

  /**
   * 从沙箱文件保存到相册
   * 典型场景:下载的图片保存到相册
   */
  async saveToGallery(sandboxPath: string, fileName: string): Promise<string | null> {
    try {
      // 创建媒体资源
      const assetUri = await this.helper.createAsset(
        photoAccessHelper.PhotoType.IMAGE,
        'jpg',
        { title: fileName }
      );

      // 将沙箱文件内容复制到媒体库
      const sourceFile = fileIo.openSync(sandboxPath, fileIo.OpenMode.READ_ONLY);
      const destFile = fileIo.openSync(assetUri, fileIo.OpenMode.WRITE_ONLY);

      try {
        const bufferSize = 4096;
        const buffer = new ArrayBuffer(bufferSize);
        let readSize = 0;

        while ((readSize = fileIo.readSync(sourceFile.fd, buffer)) > 0) {
          fileIo.writeSync(destFile.fd, buffer, { length: readSize });
        }
      } finally {
        fileIo.closeSync(sourceFile);
        fileIo.closeSync(destFile);
      }

      console.info(`[Save] 保存到相册成功: ${assetUri}`);
      return assetUri;
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[Save] 保存到相册失败: ${error.message}`);
      return null;
    }
  }

  // ========== 删除 ==========

  /**
   * 删除媒体文件
   * ⚠️ 这是不可逆操作,删除后文件无法恢复
   */
  async deletePhotoAsset(asset: photoAccessHelper.PhotoAsset): Promise<boolean> {
    try {
      await this.helper.deleteAssets([asset.uri]);
      console.info(`[Delete] 删除成功: ${asset.displayName}`);
      return true;
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[Delete] 删除失败: ${error.message}`);
      return false;
    }
  }

  /**
   * 批量删除媒体文件
   */
  async deletePhotoAssets(assets: photoAccessHelper.PhotoAsset[]): Promise<number> {
    const uris = assets.map(asset => asset.uri);
    try {
      await this.helper.deleteAssets(uris);
      console.info(`[Delete] 批量删除成功: ${uris.length} 个文件`);
      return uris.length;
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[Delete] 批量删除失败: ${error.message}`);
      return 0;
    }
  }

  // ========== 修改 ==========

  /**
   * 更新媒体文件的属性
   * 支持修改:标题、位置信息、收藏状态等
   */
  async updatePhotoAsset(
    asset: photoAccessHelper.PhotoAsset,
    updates: Record<string, string | number | boolean>
  ): Promise<boolean> {
    try {
      // 逐个设置需要更新的属性
      for (const [key, value] of Object.entries(updates)) {
        switch (key) {
          case 'title':
            asset.set('title', value as string);
            break;
          case 'isFavorite':
            asset.set('is_favorite', value as boolean);
            break;
          case 'longitude':
            asset.set('longitude', value as number);
            break;
          case 'latitude':
            asset.set('latitude', value as number);
            break;
          default:
            console.warn(`[Update] 不支持的属性: ${key}`);
        }
      }

      // 提交修改
      await this.helper.updateAsset(asset);
      console.info(`[Update] 更新成功: ${asset.displayName}`);
      return true;
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[Update] 更新失败: ${error.message}`);
      return false;
    }
  }

  /**
   * 收藏/取消收藏照片
   */
  async toggleFavorite(asset: photoAccessHelper.PhotoAsset): Promise<boolean> {
    const isFavorite = asset.get('is_favorite') as boolean;
    return await this.updatePhotoAsset(asset, { isFavorite: !isFavorite });
  }

  // ========== 用户相册管理 ==========

  /**
   * 创建用户相册
   */
  async createUserAlbum(albumName: string): Promise<photoAccessHelper.Album | null> {
    try {
      const album = await this.helper.createAlbum(albumName);
      console.info(`[Album] 创建相册成功: ${albumName}`);
      return album;
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[Album] 创建相册失败: ${error.message}`);
      return null;
    }
  }

  /**
   * 将照片添加到用户相册
   */
  async addPhotoToAlbum(
    album: photoAccessHelper.Album,
    assets: photoAccessHelper.PhotoAsset[]
  ): Promise<boolean> {
    try {
      await album.addAssets(assets);
      console.info(`[Album] 添加 ${assets.length} 张照片到相册 "${album.albumName}"`);
      return true;
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[Album] 添加到相册失败: ${error.message}`);
      return false;
    }
  }

  /**
   * 从用户相册移除照片(不会删除原文件)
   */
  async removePhotoFromAlbum(
    album: photoAccessHelper.Album,
    assets: photoAccessHelper.PhotoAsset[]
  ): Promise<boolean> {
    try {
      await album.removeAssets(assets);
      console.info(`[Album] 从相册 "${album.albumName}" 移除 ${assets.length} 张照片`);
      return true;
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[Album] 从相册移除失败: ${error.message}`);
      return false;
    }
  }

  /**
   * 删除用户相册
   */
  async deleteUserAlbum(album: photoAccessHelper.Album): Promise<boolean> {
    try {
      await this.helper.deleteAlbums([album]);
      console.info(`[Album] 删除相册成功: ${album.albumName}`);
      return true;
    } catch (err) {
      const error = err as BusinessError;
      console.error(`[Album] 删除相册失败: ${error.message}`);
      return false;
    }
  }
}

四、踩坑与注意事项

4.1 权限必须先申请再使用

这是最常见的坑。photoAccessHelper 的几乎所有操作都需要权限,而且 WRITE_IMAGEVIDEO 权限比 READ_IMAGEVIDEO 更严格——用户可能授予读取权限但拒绝写入权限。

// ❌ 错误:没有申请权限就直接使用
const helper = photoAccessHelper.getPhotoAccessHelper(context);
const albums = await helper.getAlbums(...); // 直接崩溃!

// ✅ 正确:先申请权限
const granted = await permissionManager.ensurePermissions(context);
if (!granted) {
  // 引导用户去设置中开启权限
  return;
}
const helper = photoAccessHelper.getPhotoAccessHelper(context);

4.2 FetchResult 必须正确遍历

FetchResult 不是数组,它是一个迭代器。必须使用 getFirstObject() + getNextObject() 的方式遍历,不能直接用 for...of

// ❌ 错误:FetchResult 不是数组
const result = helper.getAssets(fetchOptions);
for (const item of result) { ... } // 编译错误!

// ✅ 正确:使用迭代器模式
const result = helper.getAssets(fetchOptions);
const items: photoAccessHelper.PhotoAsset[] = [];

try {
  const first = await result.getFirstObject();
  items.push(first);
} catch (err) {
  // 没有数据
  return items;
}

while (true) {
  try {
    const next = await result.getNextObject();
    items.push(next);
  } catch (err) {
    break; // 遍历结束
  }
}

4.3 创建资源后必须写入文件

createAsset() 只是在媒体库中注册了一个空壳资源,你必须手动写入文件内容。如果只创建不写入,媒体库中会出现一个 0 字节的损坏文件。

// ❌ 错误:只创建不写入
const uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg', options);
// 忘记写入文件内容,导致出现 0 字节损坏文件

// ✅ 正确:创建后立即写入
const uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg', options);
const file = fileIo.openSync(uri, fileIo.OpenMode.WRITE_ONLY);
try {
  // 写入图片数据
  fileIo.writeSync(file.fd, imageBuffer);
} finally {
  fileIo.closeSync(file.fd);
}

4.4 删除操作不可逆

deleteAssets() 是物理删除,不是移到回收站。删除后文件无法恢复。建议在删除前弹出确认对话框。

// 安全删除:先确认再删除
async function safeDeleteAsset(
  helper: photoAccessHelper.PhotoAccessHelper,
  asset: photoAccessHelper.PhotoAsset,
  confirmed: boolean
): Promise<boolean> {
  if (!confirmed) {
    console.warn('[Delete] 用户未确认,取消删除');
    return false;
  }

  try {
    await helper.deleteAssets([asset.uri]);
    return true;
  } catch (err) {
    return false;
  }
}

4.5 URI 的有效期

通过 PhotoAsset.uri 获取的 URI 不是永久有效的。在以下情况下 URI 可能失效:

  1. 设备重启后
  2. 媒体库重新扫描后
  3. 文件被其他应用删除后

建议在需要使用 URI 时实时查询,不要长期缓存。

// ❌ 错误:长期缓存 URI
const cachedUri = asset.uri;
// ... 几天后 ...
const file = fileIo.openSync(cachedUri); // 可能已失效!

// ✅ 正确:实时查询
const assets = await queryManager.queryPhotosWithFilter({ type: 'IMAGE' });
const currentUri = assets[0].uri; // 实时获取

4.6 fetchColumns 的性能影响

fetchColumns 决定了查询返回哪些字段。查询的字段越多,耗时越长。建议只查询需要的字段。

// ❌ 错误:查询所有字段(性能差)
const fetchOptions: photoAccessHelper.FetchOptions = {
  fetchColumns: [], // 空数组 = 查询所有字段
  predicates: new dataSharePredicates.DataSharePredicates(),
};

// ✅ 正确:只查询需要的字段
const fetchOptions: photoAccessHelper.FetchOptions = {
  fetchColumns: [
    photoAccessHelper.PhotoKeys.URI,
    photoAccessHelper.PhotoKeys.DISPLAY_NAME,
  ],
  predicates: new dataSharePredicates.DataSharePredicates(),
};

五、HarmonyOS 6 适配

5.1 API 变更

变更项 HarmonyOS 5 HarmonyOS 6
创建资源 createAsset() 新增 createAssetWithUri() 支持指定保存路径
安全访问 新增「安全相册」API,支持隐私照片加密存储
批量操作 逐个操作 新增 batchUpdateAssets() 批量更新
云端资源 仅本地 新增云端媒体资源查询与下载
变更通知 新增 registerChange() 监听媒体库变化

5.2 媒体库变化监听

HarmonyOS 6 新增了媒体库变化监听能力,可以实时感知照片的增删改:

// HarmonyOS 6 新增:监听媒体库变化
helper.registerChange(
  assetUri,  // 监听指定资源,传空字符串则监听所有变化
  false,     // 是否递归监听子目录
  (changeData: photoAccessHelper.ChangeData) => {
    switch (changeData.type) {
      case photoAccessHelper.NotifyType.INSERT:
        console.info('[Change] 新增媒体文件');
        break;
      case photoAccessHelper.NotifyType.DELETE:
        console.info('[Change] 删除媒体文件');
        break;
      case photoAccessHelper.NotifyType.UPDATE:
        console.info('[Change] 更新媒体文件');
        break;
    }
  }
);

// 取消监听
helper.unregisterChange(assetUri);

5.3 安全相册

HarmonyOS 6 引入了「安全相册」概念,用户可以将私密照片存入加密相册:

// HarmonyOS 6 新增:创建安全相册
const secureAlbum = await helper.createSecureAlbum('私密相册', userPassword);

// 将照片移入安全相册
await secureAlbum.addAssets(assets);

// 访问安全相册需要验证密码
const albums = await helper.getSecureAlbums(userPassword);

5.4 迁移要点

  1. createAsset() 改为 createAssetWithUri() 以获得更灵活的保存路径控制
  2. 使用 registerChange() 替代轮询查询,实时感知媒体库变化
  3. 安全相册功能需要声明 ohos.permission.SECURE_ALBUM 权限
  4. 云端资源操作需要网络权限和华为账号授权

六、总结

知识点 核心内容
photoAccessHelper 媒体库的访问入口,通过 getPhotoAccessHelper(context) 获取
权限体系 READ_IMAGEVIDEO(读)+ WRITE_IMAGEVIDEO(写),均为 user_granted
相册管理 getAlbums() 查询相册、createAlbum() 创建相册、deleteAlbums() 删除相册
媒体查询 getAssets() + FetchOptions + DataSharePredicates 条件筛选
FetchResult 迭代器模式,getFirstObject() + getNextObject() 遍历
创建资源 createAsset() 注册空壳 → openSync() 打开文件 → writeSync() 写入数据
删除资源 deleteAssets() 物理删除,不可逆,建议删除前确认
修改资源 asset.set() 设置属性 → helper.updateAsset() 提交修改
URI 有效期 URI 不是永久有效,建议实时查询而非长期缓存
HarmonyOS 6 新增安全相册、变化监听、批量操作、云端资源等能力

💡 一句话总结:photoAccessHelper 是媒体库的「合法通道」,权限申请是前提,FetchResult 遍历是基本功,创建资源后必须写入文件内容,删除操作不可逆需谨慎,URI 不要长期缓存。掌握这些,你就能安全高效地操作用户的照片和视频了。

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

评论(0

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

全部回复

上滑加载中

设置昵称

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

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

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