一、背景引入:这玩意儿到底是啥?

咱今儿个聊的这 PhotoAccessHelper,说白了就是 HarmonyOS 给咱开发者准备的一个"图库管家"。你想想,你做个 App,总得让用户选个图片、存个照片啥的吧?难不成你还想自己写一套文件系统去怼人家的媒体库?那不得被华为爸爸给封了!

这玩意儿属于 Media Library Kit(媒体文件管理服务)的一部分,专门管图片和视频的。官方给的定义贼拉正经:“提供了管理相册和媒体文件的能力,帮助应用快速构建图片和视频的展示与播放功能。”

翻译成大白话就是:你想动用户的照片和视频,走我就对了,别自己瞎折腾!

适用场景

这玩意儿能帮你搞定这些事儿:

  • 图片选择器(让用户从图库选图)
  • 图片保存(把用户拍的/编辑的图存到图库)
  • 相册管理(创建、删除、重命名相册)
  • 媒体文件操作(读取、修改、删除图片视频)
  • 媒体文件查询(按条件搜索图库里的资源)

二、整体架构:人家是咋工作的?

咱先说说这框架的原理,其实贼简单:

你的 App → PhotoAccessHelper → 权限校验 → 媒体库数据库 → 返回结果

就这四步,没啥花里胡哨的。媒体库接收你的请求,先看看你有没有权限(没权限直接给你打回去),校验通过了就去数据库里操作,最后把结果给你。

能力范围分两种:

1. 所有应用都能用的(无需额外权限):

  • 选择/保存媒体库资源(用 Picker 和保存按钮)
  • 管理动态照片
  • 使用各种 Picker 组件(PhotoPicker、AlbumPicker、RecentPhoto 等)

2. 三方应用受限的(需要申请权限证书):

  • 获取指定媒体资源
  • 获取缩略图
  • 重命名媒体资源
  • 管理用户相册(创建、重命名、添加、删除)

避坑提醒: 受限能力需要去应用市场(AGC)申请权限证书,别想着偷偷用,人家管得可严了!

权限配置

module.json5 里配置权限(如果需要的话):

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.READ_IMAGEVIDEO",
        "reason": "$string:read_media_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.WRITE_IMAGEVIDEO",
        "reason": "$string:write_media_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

三、核心功能:全是干货!

3.1 用 Picker 选图片——贼拉方便

这功能是我最喜欢的,为啥?不用申请权限! 你直接调用就完事了。

完整组件示例

来,直接上完整的 ArkTS 组件代码,复制就能用:

// PhotoPickerExample.ets
import { fileIo } from '@kit.CoreFileKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { promptAction } from '@kit.ArkUI';

@Entry
@Component
struct PhotoPickerExample {
  // 存储选中的图片 URI
  @State selectedUris: string[] = [];
  // 存储加载后的图片数据
  @State imageDatas: ArrayBuffer[] = [];
  // 是否正在加载
  @State isLoading: boolean = false;
  // 错误信息
  @State errorMessage: string = '';

  // PhotoViewPicker 实例
  private photoViewPicker: photoAccessHelper.PhotoViewPicker = new photoAccessHelper.PhotoViewPicker();

  build() {
    Column() {
      // 标题
      Text('PhotoPicker 示例')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 20 })

      // 选择按钮
      Button('选择图片')
        .width('80%')
        .height(50)
        .fontSize(18)
        .onClick(async () => {
          await this.selectPhotos();
        })

      // 加载状态
      if (this.isLoading) {
        LoadingProgress()
          .width(50)
          .height(50)
          .margin({ top: 20 })
      }

      // 错误信息
      if (this.errorMessage) {
        Text(this.errorMessage)
          .fontSize(14)
          .fontColor('#ff4444')
          .margin({ top: 10 })
      }

      // 图片展示区
      Row() {
        ForEach(this.imageDatas, (item: ArrayBuffer, index: number) => {
          Image(item)
            .width(100)
            .height(100)
            .objectFit(ImageFit.Cover)
            .margin(5)
            .borderRadius(8)
        })
      }
      .wrap(true)
      .margin({ top: 20 })

      // 已选数量
      Text(`已选 ${this.selectedUris.length} 张图片`)
        .fontSize(14)
        .fontColor('#666')
        .margin({ top: 10 })
    }
    .width('100%')
    .padding(20)
  }

  // 选择图片的核心方法
  async selectPhotos(): Promise<void> {
    try {
      this.isLoading = true;
      this.errorMessage = '';

      // 1. 创建选择选项
      const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
      photoSelectOptions.maxSelectNumber = 9;  // 最多选 9 张
      photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
      photoSelectOptions.isPhotoTakingSupported = true;  // 允许直接拍照

      // 2. 拉起图库选择
      const result = await this.photoViewPicker.select(photoSelectOptions);

      // 3. 检查有没有选
      if (!result.photoUris || result.photoUris.length === 0) {
        this.errorMessage = '没选图片啊老铁!';
        return;
      }

      // 4. 存到全局变量(重要!别在回调里直接操作)
      this.selectedUris = result.photoUris;

      // 5. 加载图片数据(用 setTimeout 避免阻塞 UI)
      setTimeout(async () => {
        await this.loadImages();
      }, 100);

    } catch (error) {
      this.errorMessage = `出错了:${JSON.stringify(error)}`;
      console.error('selectPhotos failed:', error);
    } finally {
      this.isLoading = false;
    }
  }

  // 加载图片数据
  async loadImages(): Promise<void> {
    this.imageDatas = [];
    
    for (const uri of this.selectedUris) {
      try {
        const imageData = await this.readImageFile(uri);
        if (imageData) {
          this.imageDatas.push(imageData);
        }
      } catch (error) {
        console.error(`加载图片失败 ${uri}:`, error);
      }
    }
  }

  // 读取单个图片文件
  async readImageFile(uri: string): Promise<ArrayBuffer | null> {
    try {
      // 第一步:打开文件拿到 fd
      const file = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY);
      
      // 第二步:读取数据
      const fileSize = fileIo.statSync(uri).size;
      const buffer = new ArrayBuffer(fileSize);
      const readLen = fileIo.readSync(file.fd, buffer);
      
      // 第三步:关闭文件
      fileIo.closeSync(file);
      
      console.info(`读取成功,大小:${readLen} 字节`);
      return buffer;
    } catch (error) {
      console.error('readImageFile failed:', error);
      return null;
    }
  }
}
配置选项详解
const photoSelectOptions = new photoAccessHelper.PhotoSelectOptions();

// 最多选择数量(1-1000)
photoSelectOptions.maxSelectNumber = 9;

// MIME 类型过滤
photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;  // 只要图片
// 或者
photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.VIDEO_TYPE;  // 只要视频
// 或者
photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_VIDEO_TYPE;  // 都要

// 是否允许直接拍照
photoSelectOptions.isPhotoTakingSupported = true;

// 是否显示最近照片(默认 true)
photoSelectOptions.isRecentPhotoSupported = true;

// 是否显示相册选择入口(默认 true)
photoSelectOptions.isAlbumSupported = true;

重点来了! 这里拿到的 URI 权限是只读的,而且你不能在回调里直接用这个 URI 打开文件!得先存到全局变量里,再用个按钮啥的去触发打开操作。这是个大坑,我见过不少人在这儿栽跟头!


3.2 读取文件数据——两步走

拿到 URI 之后,你想读文件内容,得分两步:

完整工具类封装

来,直接给你整个工具类,以后直接用:

// PhotoFileUtils.ets
import { fileIo } from '@kit.CoreFileKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit';

export class PhotoFileUtils {
  /**
   * 读取文件数据
   * @param uri 文件 URI
   * @returns ArrayBuffer 或 null
   */
  static async readFile(uri: string): Promise<ArrayBuffer | null> {
    let file: fileIo.File | null = null;
    try {
      // 第一步:打开文件拿到 fd
      file = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY);
      console.info('file fd: ' + file.fd);

      // 第二步:获取文件大小
      const fileSize = fileIo.statSync(uri).size;
      console.info('文件大小:' + fileSize + ' 字节');

      // 第三步:读取数据
      const buffer = new ArrayBuffer(fileSize);
      const readLen = fileIo.readSync(file.fd, buffer);
      console.info('读取成功,实际读取:' + readLen + ' 字节');

      return buffer;
    } catch (error) {
      console.error('readFile failed with err: ' + JSON.stringify(error));
      return null;
    } finally {
      // 第四步:关闭文件(重要!)
      if (file) {
        try {
          fileIo.closeSync(file);
        } catch (e) {
          console.error('close file failed:', e);
        }
      }
    }
  }

  /**
   * 读取文件的一部分(适合大文件)
   * @param uri 文件 URI
   * @param offset 起始位置
   * @param length 读取长度
   * @returns ArrayBuffer 或 null
   */
  static async readFilePart(uri: string, offset: number, length: number): Promise<ArrayBuffer | null> {
    let file: fileIo.File | null = null;
    try {
      file = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY);
      
      // 定位到指定位置
      fileIo.lseekSync(file.fd, offset, fileIo.SeekMode.SET);
      
      const buffer = new ArrayBuffer(length);
      const readLen = fileIo.readSync(file.fd, buffer);
      
      return buffer.slice(0, readLen);
    } catch (error) {
      console.error('readFilePart failed:', error);
      return null;
    } finally {
      if (file) {
        fileIo.closeSync(file);
      }
    }
  }

  /**
   * 获取文件信息
   * @param uri 文件 URI
   * @returns 文件信息对象
   */
  static async getFileInfo(uri: string): Promise<{
    size: number;
    mimeType?: string;
    lastModified: number;
  } | null> {
    try {
      const stats = fileIo.statSync(uri);
      return {
        size: stats.size,
        lastModified: stats.mtime
      };
    } catch (error) {
      console.error('getFileInfo failed:', error);
      return null;
    }
  }

  /**
   * 将 ArrayBuffer 转为 base64
   * @param buffer ArrayBuffer
   * @returns base64 字符串
   */
  static arrayBufferToBase64(buffer: ArrayBuffer): string {
    let binary = '';
    const bytes = new Uint8Array(buffer);
    for (let i = 0; i < bytes.byteLength; i++) {
      binary += String.fromCharCode(bytes[i]);
    }
    return btoa(binary);
  }

  /**
   * 将 base64 转为 ArrayBuffer
   * @param base64 base64 字符串
   * @returns ArrayBuffer
   */
  static base64ToArrayBuffer(base64: string): ArrayBuffer {
    const binaryString = atob(base64);
    const bytes = new Uint8Array(binaryString.length);
    for (let i = 0; i < binaryString.length; i++) {
      bytes[i] = binaryString.charCodeAt(i);
    }
    return bytes.buffer;
  }
}
使用示例
// 在组件里直接用
import { PhotoFileUtils } from './PhotoFileUtils';

async loadImage() {
  const uri = this.selectedUris[0];
  
  // 方式一:直接读取
  const imageData = await PhotoFileUtils.readFile(uri);
  if (imageData) {
    this.imageData = imageData;
  }

  // 方式二:先获取文件信息
  const fileInfo = await PhotoFileUtils.getFileInfo(uri);
  if (fileInfo) {
    console.info(`文件大小:${fileInfo.size} 字节`);
    // 如果文件太大,可以只读一部分
    if (fileInfo.size > 1024 * 1024) {  // 大于 1MB
      const partData = await PhotoFileUtils.readFilePart(uri, 0, 1024 * 1024);
      // 处理前 1MB 数据
    }
  }
}

3.3 保存媒体资源——两种方式任你选

保存到图库这事儿,官方给了两种玩法,都不需要申请 WRITE_IMAGEVIDEO 权限

方式一:安全控件 SaveButton(推荐)

这玩意儿是个系统级的保存按钮,用户点了之后会自动弹窗授权,安全性拉满。

完整组件示例
// SaveButtonExample.ets
import { fileIo } from '@kit.CoreFileKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { common } from '@kit.AbilityKit';

interface SaveButtonOptions {
  icon: photoAccessHelper.SaveIconStyle;
  text: photoAccessHelper.SaveDescription;
  buttonType: photoAccessHelper.ButtonType;
}

@Entry
@Component
struct SaveButtonExample {
  @State isSaved: boolean = false;
  @State savedUri: string = '';
  @State errorMessage: string = '';
  
  private phAccessHelper: photoAccessHelper.PhotoAccessHelper = 
    new photoAccessHelper.PhotoAccessHelper();
  private uriString: string = '';
  private context: common.UIAbilityContext | null = null;

  // 保存按钮配置
  saveButtonOptions: SaveButtonOptions = {
    icon: photoAccessHelper.SaveIconStyle.FULL_FILLED,
    text: photoAccessHelper.SaveDescription.SAVE_IMAGE,
    buttonType: photoAccessHelper.ButtonType.Capsule
  };

  aboutToAppear() {
    // 获取 context
    this.context = getContext(this) as common.UIAbilityContext;
  }

  build() {
    Column() {
      Text('SaveButton 保存示例')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 20 })

      // 系统保存按钮
      photoAccessHelper.SaveButton({
        onSave: async () => {
          await this.handleSave();
        },
        options: this.saveButtonOptions
      })
      .width(200)
      .height(50)
      .margin({ bottom: 20 })

      // 状态显示
      if (this.isSaved) {
        Text('保存成功!')
          .fontSize(16)
          .fontColor('#00aa00')
          .margin({ top: 10 })
        
        Text(`URI: ${this.savedUri}`)
          .fontSize(12)
          .fontColor('#666')
          .margin({ top: 5 })
      }

      if (this.errorMessage) {
        Text(` ${this.errorMessage}`)
          .fontSize(14)
          .fontColor('#ff4444')
          .margin({ top: 10 })
      }
    }
    .width('100%')
    .padding(20)
  }

  async handleSave(): Promise<void> {
    if (!this.context) {
      this.errorMessage = 'Context 还没准备好!';
      return;
    }

    try {
      this.errorMessage = '';

      // 1. 准备要保存的文件(这里假设你有个文件 URI)
      const fileUri = this.getTempFileUri();

      // 2. 注册监听
      this.phAccessHelper.registerChange(
        photoAccessHelper.DefaultChangeUri.DEFAULT_PHOTO_URI,
        true,
        this.onCallback
      );

      // 3. 创建资源请求
      const assetChangeRequest = photoAccessHelper.MediaAssetChangeRequest
        .createImageAssetRequest(this.context, fileUri);
      
      await this.phAccessHelper.applyChanges(assetChangeRequest);

      // 4. 拿到保存后的 URI
      this.uriString = assetChangeRequest.getAsset().uri;
      console.info('保存后的 URI: ' + this.uriString);

    } catch (error) {
      this.errorMessage = `保存失败:${JSON.stringify(error)}`;
      console.error('handleSave failed:', error);
    }
  }

  // 回调处理
  onCallback = (changeData: photoAccessHelper.ChangeData): void => {
    console.info('收到变更通知:' + JSON.stringify(changeData));
    
    for (let i = 0; i < changeData.uris.length; i++) {
      if (changeData.uris[i] === this.uriString &&
          changeData.type === photoAccessHelper.NotifyType.NOTIFY_ADD) {
        
        console.info('保存成功确认!');
        this.isSaved = true;
        this.savedUri = this.uriString;

        // 重要:取消监听
        this.phAccessHelper.unRegisterChange(
          photoAccessHelper.DefaultChangeUri.DEFAULT_PHOTO_URI
        );
      }
    }
  }

  // 获取临时文件 URI(示例)
  getTempFileUri(): string {
    // 实际使用时,这里应该是你生成的图片文件路径
    const context = getContext(this) as common.UIAbilityContext;
    const cacheDir = context.cacheDir;
    return `${cacheDir}/temp_image.jpg`;
  }
}
SaveButton 配置选项
// 图标样式
enum SaveIconStyle {
  FULL_FILLED = 0,      // 实心图标
  OUTLINED = 1          // 空心图标
}

// 按钮文字
enum SaveDescription {
  SAVE_IMAGE = 0,       // "保存图片"
  SAVE_VIDEO = 1,       // "保存视频"
  SAVE_TO_ALBUM = 2     // "保存到相册"
}

// 按钮类型
enum ButtonType {
  Capsule = 0,          // 胶囊形
  Square = 1            // 方形
}
方式二:弹窗授权 showAssetsCreationDialog

这方式更直接,直接弹个窗让用户授权,然后拿到目标 URI 自己写文件。

完整示例
// ShowAssetsDialogExample.ets
import { fileIo } from '@kit.CoreFileKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { common } from '@kit.AbilityKit';
import { image } from '@kit.ImageKit';

@Entry
@Component
struct ShowAssetsDialogExample {
  @State isSaved: boolean = false;
  @State savedUri: string = '';
  @State errorMessage: string = '';
  
  private phAccessHelper: photoAccessHelper.PhotoAccessHelper = 
    new photoAccessHelper.PhotoAccessHelper();

  build() {
    Column() {
      Text('showAssetsCreationDialog 示例')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 20 })

      Button('保存图片到图库')
        .width('80%')
        .height(50)
        .fontSize(18)
        .onClick(async () => {
          await this.saveToGallery();
        })

      if (this.isSaved) {
        Text('保存成功!')
          .fontSize(16)
          .fontColor('#00aa00')
          .margin({ top: 20 })
      }

      if (this.errorMessage) {
        Text(` ${this.errorMessage}`)
          .fontSize(14)
          .fontColor('#ff4444')
          .margin({ top: 10 })
      }
    }
    .width('100%')
    .padding(20)
  }

  async saveToGallery(): Promise<void> {
    try {
      this.errorMessage = '';

      // 1. 准备源文件(必须是应用沙箱路径)
      const srcFileUri = await this.createTempImage();
      console.info('源文件 URI: ' + srcFileUri);

      // 2. 配置保存选项
      const photoCreationConfigs: Array<photoAccessHelper.PhotoCreationConfig> = [{
        title: '我的照片',  // 可选,图片标题
        fileNameExtension: 'jpg',  // 文件扩展名
        photoType: photoAccessHelper.PhotoType.IMAGE,  // 图片类型
        subtype: photoAccessHelper.PhotoSubtype.DEFAULT,  // 子类型,可选
      }];

      // 3. 调起授权弹窗,拿到目标 URI
      const desFileUris: Array<string> = await this.phAccessHelper
        .showAssetsCreationDialog([srcFileUri], photoCreationConfigs);

      console.info('目标 URI: ' + desFileUris[0]);

      // 4. 文件已经自动复制了,不需要手动操作
      // (新版本的 API 会自动处理文件复制)

      this.isSaved = true;
      this.savedUri = desFileUris[0];

    } catch (error) {
      this.errorMessage = `保存失败:${JSON.stringify(error)}`;
      console.error('saveToGallery failed:', error);
    }
  }

  // 创建临时图片(示例)
  async createTempImage(): Promise<string> {
    const context = getContext(this) as common.UIAbilityContext;
    const cacheDir = context.cacheDir;
    const tempPath = `${cacheDir}/temp_${Date.now()}.jpg`;

    // 这里可以用 image 模块生成图片,或者从网络下载
    // 简单示例:创建一个空白文件
    const file = fileIo.openSync(tempPath, 
      fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY);
    fileIo.closeSync(file);

    return tempPath;
  }
}

注意: 调用这个接口前,确保你的 module.json5 文件里的 abilities 标签配置了 labelicon,不然弹窗显示不了应用名称!

{
  "abilities": [{
    "name": "EntryAbility",
    "label": "$string:app_name",  // 必须有
    "icon": "$media:icon"         // 必须有
  }]
}

3.4 查询媒体文件——高级玩法

想按条件搜索图库里的资源?用 getAssets 方法:

// 查询最近的照片
async getRecentPhotos(): Promise<void> {
  const context = getContext(this) as common.UIAbilityContext;
  const phAccessHelper = new photoAccessHelper.PhotoAccessHelper();

  // 创建查询条件
  const options = new photoAccessHelper.PhotoSelectOptions();
  options.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
  
  // 按时间倒序
  const orderOption = new photoAccessHelper.OrderOption();
  orderOption.orderKey = photoAccessHelper.MediaKey.DATE_MODIFIED;
  orderOption.order = photoAccessHelper.Order.DESC;

  try {
    // 查询最近 10 张照片
    const assets = await phAccessHelper.getAssets(context, options, orderOption, 0, 10);
    
    console.info(`找到 ${assets.length} 张照片`);
    
    for (const asset of assets) {
      console.info(`URI: ${asset.uri}`);
      console.info(`修改时间:${asset.dateModified}`);
    }
  } catch (error) {
    console.error('getAssets failed:', error);
  }
}

四、避坑指南:血泪教训!

坑 1:Picker 回调里直接操作 URI

错误做法:

const result = await photoViewPicker.select(options);
//  别在这儿直接打开文件!
const file = fileIo.openSync(result.photoUris[0], ...);

正确做法:

const result = await photoViewPicker.select(options);
// 先存到全局变量
this.selectedUris = result.photoUris;
// 然后用按钮或其他事件触发读取

坑 2:保存后不取消监听

用 SaveButton 保存完,记得调用 unRegisterChange 取消监听,不然内存泄漏找你麻烦!

// 正确做法
onCallback = (changeData: photoAccessHelper.ChangeData) => {
  if (/* 保存成功条件 */) {
    // 处理业务逻辑
    // 取消监听
    phAccessHelper.unRegisterChange(
      photoAccessHelper.DefaultChangeUri.DEFAULT_PHOTO_URI
    );
  }
}

坑 3:EXIF 信息获取不到

出于隐私保护,EXIF 里的地理位置和拍摄参数被去隐私化了。你要是真需要,得申请 ohos.permission.MEDIA_LOCATION 权限,但这权限可不好申请!

// 需要额外权限
{
  "name": "ohos.permission.MEDIA_LOCATION",
  "reason": "$string:media_location_reason",
  "usedScene": {
    "abilities": ["EntryAbility"],
    "when": "inuse"
  }
}

坑 4:音频文件别用 PhotoAccessHelper

人家只管图片和视频!你要处理音频,请用 AudioViewPicker,别硬刚!

//  错误
import { photoAccessHelper } from '@kit.MediaLibraryKit';
// 想选音频文件

// 正确
import { audioAccessHelper } from '@kit.MediaLibraryKit';
// 或者用 AudioViewPicker

坑 5:沙箱路径问题

保存文件时,源文件 URI 必须是应用沙箱路径,别想着用其他路径,人家不认!

// 正确的沙箱路径
const context = getContext(this) as common.UIAbilityContext;

// 缓存目录
const cacheDir = context.cacheDir;  // /data/storage/el2/base/haps/entry/cache

// 文件目录
const fileDir = context.fileDir;  // /data/storage/el2/base/haps/entry/files

// 临时文件
const tempPath = `${cacheDir}/temp_${Date.now()}.jpg`;

坑 6:文件不关闭导致资源泄漏

//  错误:忘记关闭文件
const file = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY);
const buffer = new ArrayBuffer(fileSize);
fileIo.readSync(file.fd, buffer);
// 没关闭!

// 正确:用 try-finally 确保关闭
let file = null;
try {
  file = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY);
  const buffer = new ArrayBuffer(fileSize);
  fileIo.readSync(file.fd, buffer);
} finally {
  if (file) {
    fileIo.closeSync(file);
  }
}

坑 7:大文件一次性读取导致 OOM

//  错误:大文件直接读
const fileSize = fileIo.statSync(uri).size;  // 可能是 100MB+
const buffer = new ArrayBuffer(fileSize);  // 内存爆炸!

// 正确:分块读取
async function readInChunks(uri: string, chunkSize: number = 1024 * 1024) {
  const file = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY);
  const fileSize = fileIo.statSync(uri).size;
  const chunks: ArrayBuffer[] = [];
  
  let offset = 0;
  while (offset < fileSize) {
    const readSize = Math.min(chunkSize, fileSize - offset);
    fileIo.lseekSync(file.fd, offset, fileIo.SeekMode.SET);
    const buffer = new ArrayBuffer(readSize);
    fileIo.readSync(file.fd, buffer);
    chunks.push(buffer);
    offset += readSize;
  }
  
  fileIo.closeSync(file);
  return chunks;
}

五、总结

PhotoAccessHelper 这玩意儿,说白了就是华为给咱开发者铺的一条"正规军"道路。你想动用户的照片和视频,走它就对了,安全、稳定、还不用自己处理一堆权限问题。

核心要点就三条:

  1. 选图片用 PhotoViewPicker,无需权限
  2. 保存图片用 SaveButton 或 showAssetsCreationDialog,也无需权限
  3. 受限能力(比如直接访问指定资源)需要去 AGC 申请权限证书

记住这些最佳实践:

  • 文件操作完一定要关闭
  • 大文件分块读取
  • 监听记得取消
  • 沙箱路径要搞对

记住这几点,你在 HarmonyOS 上玩媒体文件就能横着走了!


下期预告

下回咱聊聊 HarmonyOS 的相机开发,教你怎么用 Camera Kit 拍出花来!想学的老铁们,记得关注 B 站 “莓创 - 陈杨”,干货不断,更新不停!

咱们下期见!

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐