OpenHarmony 相册图片选择 + 压缩工具 ImagePickerUtil 完整实战 前言原生file.picker相册选择 API 使用繁琐每次调用都要重复写权限判断、图片读取、尺寸压缩、沙盒缓存写入逻辑大量冗余代码。本文封装一套完整 ImagePickerUtil 工具集成权限校验、单选 / 多选、图片自动压缩、图片保存到沙盒、释放 PixelMap 内存搭配前文 PermissionUtil 权限工具一行代码唤起相册适配 HSP、entry、所有 HAR 页面直接调用。一、工具完整源码 har_base/utils/image_picker_util.etsetsimport picker from ohos.file.picker; import image from ohos.multimedia.image; import fs from ohos.file.fs; import common from ohos.app.ability.common; import PermissionUtil, { PermType } from ./permission_util; import FileUtil from ./file_util; import LogUtil from ./log_util; // 图片压缩配置 export interface ImageCompressOption { maxWidth: number; maxHeight: number; quality: number; // 0-100 } // 相册返回图片实体 export interface MediaImage { uri: string; sandBoxPath: string; pixelMap?: image.PixelMap; } class ImagePickerUtil { private ctx: common.UIAbilityContext | null null; // 默认压缩规则 private readonly defaultOption: ImageCompressOption { maxWidth: 1080, maxHeight: 1920, quality: 70 }; setContext(ctx: common.UIAbilityContext) { this.ctx ctx; } // 释放图片内存防止内存泄漏 private releasePixelMap(pixelMap: image.PixelMap | undefined) { if (pixelMap) { pixelMap.release(); LogUtil.debug(ImagePicker, PixelMap资源释放完成); } } // 图片压缩并写入沙盒缓存目录 private async compressAndSave(pixelMap: image.PixelMap, opt: ImageCompressOption): Promisestring | null { try { // 缩放图片尺寸 const info pixelMap.getImageInfo(); let targetW info.width; let targetH info.height; if (targetW opt.maxWidth || targetH opt.maxHeight) { const scale Math.min(opt.maxWidth / targetW, opt.maxHeight / targetH); targetW Math.floor(targetW * scale); targetH Math.floor(targetH * scale); } const resizer image.createImageResizer(pixelMap); const resizeMap resizer.resizeSync(targetW, targetH); resizer.release(); // 生成缓存文件路径 const cacheDir FileUtil.getCacheDir(); const fileName img_${new Date().getTime()}.jpg; const savePath ${cacheDir}/${fileName}; await FileUtil.mkdir(cacheDir); // 写入文件 const packer image.createImagePacker(); const packOpt: image.PackingOption { format: image.ImageFormat.JPEG, quality: opt.quality }; const fileFd await fs.open(savePath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY); await packer.packToFile(resizeMap, fileFd.fd, packOpt); packer.release(); resizeMap.release(); fs.closeSync(fileFd); return savePath; } catch (e) { LogUtil.error(ImagePicker, 图片压缩失败, e); return null; } } // 单选相册图片 async pickSingle(compressOpt?: ImageCompressOption): PromiseMediaImage | null { // 先校验媒体读取权限 const hasPerm await PermissionUtil.requestPermission(PermType.IMAGE); if (!hasPerm) return null; if (!this.ctx) { LogUtil.error(ImagePicker, 上下文未初始化); return null; } try { // 创建相册选择器 const photoPicker new picker.PhotoViewPicker(); const res await photoPicker.select({ MIMEType: picker.PhotoViewMIMETypes.IMAGE_TYPE, maxSelectNumber: 1 }); if (res.photoUris.length 0) return null; const uri res.photoUris[0]; // 读取图片PixelMap const imgSource image.createImageSource(uri); const pixelMap imgSource.createPixelMapSync(); imgSource.release(); // 压缩保存 const opt compressOpt ?? this.defaultOption; const sandBoxPath await this.compressAndSave(pixelMap, opt); if (!sandBoxPath) { this.releasePixelMap(pixelMap); return null; } const result: MediaImage { uri, sandBoxPath, pixelMap }; return result; } catch (err) { LogUtil.error(ImagePicker, 单选图片异常, err); return null; } } // 多选相册图片 async pickMulti(maxCount: number, compressOpt?: ImageCompressOption): PromiseMediaImage[] { const hasPerm await PermissionUtil.requestPermission(PermType.IMAGE); if (!hasPerm) return []; if (!this.ctx) return []; const list: MediaImage[] []; try { const photoPicker new picker.PhotoViewPicker(); const res await photoPicker.select({ MIMEType: picker.PhotoViewMIMETypes.IMAGE_TYPE, maxSelectNumber: maxCount }); for (const uri of res.photoUris) { const imgSource image.createImageSource(uri); const pixelMap imgSource.createPixelMapSync(); imgSource.release(); const opt compressOpt ?? this.defaultOption; const sandBoxPath await this.compressAndSave(pixelMap, opt); if (sandBoxPath) { list.push({ uri, sandBoxPath, pixelMap }); } else { this.releasePixelMap(pixelMap); } } return list; } catch (err) { LogUtil.error(ImagePicker, 多选图片异常, err); return []; } } // 清理页面图片资源 clearImageResource(images: MediaImage[]) { images.forEach(item this.releasePixelMap(item.pixelMap)); } } export default new ImagePickerUtil();二、工具初始化配置1. EntryAbility 注入上下文和前文统一ets// EntryAbility onCreate ImagePickerUtil.setContext(this.context);三、页面调用示例示例 1编辑器页面单选图片HSP 分包 EditorPageetsimport ImagePickerUtil, { MediaImage } from ohos:har_base/utils/image_picker_util import DialogUtil from ohos:har_base/utils/dialog_util import ThemeUtil from ohos:har_base/utils/theme Entry Component struct EditorPage { State imgList: MediaImage[] [] // 唤起相册选图 async selectImage() { const img await ImagePickerUtil.pickSingle({ maxWidth: 720, maxHeight: 1280, quality: 60 }); if (img) { this.imgList.push(img); DialogUtil.alert({ content: 图片选择成功已自动压缩 }); } } // 页面销毁释放图片内存 aboutToDisappear() { ImagePickerUtil.clearImageResource(this.imgList); } build() { const color ThemeUtil.getColor(); const size ThemeUtil.getSize(); Column({ space: size.gapMd }) .width(100%) .height(100%) .padding(size.gapMd) .backgroundColor(color.pageBg) { Button(选择相册图片) .width(100%) .height(size.btnNormal) .backgroundColor(color.primary) .onClick(() this.selectImage()) // 图片预览 List({ space: size.gapSm }) { ForEach(this.imgList, (item: MediaImage) { ListItem() { Image(item.sandBoxPath) .width(100%) .height(200) .objectFit(ImageFit.Contain) .backgroundColor(#00000010) } }) } .layoutWeight(1) } } }示例 2个人中心头像上传entry/mine/Mine.etsetsimport ImagePickerUtil, { MediaImage } from ohos:har_base/utils/image_picker_util import ThemeUtil from ohos:har_base/utils/theme Entry Component struct MinePage { State avatarPath: string async changeAvatar() { // 头像专用压缩尺寸 const img await ImagePickerUtil.pickSingle({ maxWidth: 300, maxHeight: 300, quality: 80 }); if (img) { this.avatarPath img.sandBoxPath; } } build() { const color ThemeUtil.getColor(); const size ThemeUtil.getSize(); Column() .width(100%) .padding(size.gapLg) .backgroundColor(color.pageBg) { Image(this.avatarPath || $r(sys.media.ohos_ic_public_avatar)) .width(120) .height(120) .borderRadius(60) .margin({ bottom: size.gapLg }) .onClick(() this.changeAvatar()) Text(点击头像更换相册图片) .fontColor(color.textSecondary) } } }四、工具核心能力说明权限全自动管理内部自动调用 PermissionUtil 校验媒体相册权限未授权自动弹窗申请永久拒绝则跳转系统设置业务页面无需写任何权限判断代码。自动压缩逻辑支持自定义宽高上限、画质质量大图自动等比例缩放减少图片占用存储与内存上传接口时降低流量消耗。沙盒持久缓存选择后的图片自动写入应用私有 cache 缓存目录统一管理可通过 FileUtil.clearCache () 一键全部清理。内存自动管控封装 releasePixelMap 统一释放图像资源页面销毁调用 clearImageResource 彻底回收避免反复选图造成内存持续上涨。单选 / 多选双接口单选用于头像、单图上传多选用于笔记、朋友圈多图场景限制最大选择数量。五、常见踩坑解决方案图片选择后页面内存持续升高问题未释放 PixelMap 图像资源。 解决页面 aboutToDisappear 调用 ImagePickerUtil.clearImageResource 清空图片数组。相册唤起直接失败无弹窗问题未提前注入上下文或者权限未授予。 解决确认 EntryAbility 执行 ImagePickerUtil.setContext工具内部会自动申请媒体权限。压缩后图片模糊解决调高 quality 参数80-90适当增大 maxWidth/maxHeight 尺寸。HSP 分包调用图片工具失效解决工具存放在 har_baseHSP 已依赖 har_base导入路径ohos:har_base/utils/image_picker_util即可正常使用。缓存图片太多占用存储空间解决设置页面调用 FileUtil.clearCache () 一键清空所有压缩图片缓存。六、搭配前文架构规范总结所有图片相关操作统一使用 ImagePickerUtil禁止直接调用原生 picker/image API页面退出必须释放 PixelMap 资源遵守统一生命周期规范业务区分头像、笔记附件两套压缩配置按需调整尺寸画质缓存图片统一存放在 cacheDir不占用永久 files 目录空间。