PhotoAccessHelper 避坑指南:鸿蒙图库操作,看这一篇就够了!
PhotoAccessHelper 这玩意儿,说白了就是华为给咱开发者铺的一条"正规军"道路。你想动用户的照片和视频,走它就对了,安全、稳定、还不用自己处理一堆权限问题。选图片用 PhotoViewPicker,无需权限保存图片用 SaveButton 或 showAssetsCreationDialog,也无需权限受限能力(比如直接访问指定资源)需要去 AGC 申请权限证书文件操作完一定要关闭大
一、背景引入:这玩意儿到底是啥?
咱今儿个聊的这 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 标签配置了 label 和 icon,不然弹窗显示不了应用名称!
{
"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 这玩意儿,说白了就是华为给咱开发者铺的一条"正规军"道路。你想动用户的照片和视频,走它就对了,安全、稳定、还不用自己处理一堆权限问题。
核心要点就三条:
- 选图片用 PhotoViewPicker,无需权限
- 保存图片用 SaveButton 或 showAssetsCreationDialog,也无需权限
- 受限能力(比如直接访问指定资源)需要去 AGC 申请权限证书
记住这些最佳实践:
- 文件操作完一定要关闭
- 大文件分块读取
- 监听记得取消
- 沙箱路径要搞对
记住这几点,你在 HarmonyOS 上玩媒体文件就能横着走了!
下期预告
下回咱聊聊 HarmonyOS 的相机开发,教你怎么用 Camera Kit 拍出花来!想学的老铁们,记得关注 B 站 “莓创 - 陈杨”,干货不断,更新不停!
咱们下期见!
更多推荐



所有评论(0)