
1. 项目背景imagePacker.packing 为什么走到废这一步先说结论鸿蒙6 的 SDK 升级之后imagePacker.packing这个 API 会被系统标记为废弃官方推荐使用imagePacker.packingAsync以及配套的同步接口来完成图片打包。最近不少开发者在升级 SDK、编译老项目时都遇到了编译告警提示某个方法“将在后续版本移除”有的编译直接走上deprecated标记虽然当前还能跑但留给开发者的过渡窗口已经肉眼可见地缩小了。imagePacker 是 HarmonyOS 多媒体能力里负责“图像打包”的组件干的事就是把 PixelMap 或 ImageSource 里的图像数据按指定的格式比如 JPEG、PNG、WEBP压缩编码成二进制字节流然后交给你去做上传、存储、传输等后续处理。它的用武之地非常广泛头像上传前的压缩、聊天消息里的缩略图生成、相册导出的格式转换、扫码场景的图片预处理等等。可以说任何一个跟“把一张图变成一段可落地的字节数据”相关的需求都绕不开 imagePacker。我手头有不少项目是从鸿蒙 3.0、4.0 一路迁移过来的早期代码里大量使用packing(pixelMap, packOpts)这种 Promise 写法。这次鸿蒙 6 的废弃通知影响的不是某个冷门接口而是整个生态里图片处理链路最常用的一环。如果项目里用到了图片压缩上传、图片格式转换、批量处理几乎都会命中这个适配点。这篇文章会把我的适配过程完整记录下来包括新旧 API 的差异、迁移步骤、兼容写法、内存释放细节以及我踩过的几个坑。如果你也正在处理类似的编译告警或者单纯想把图片打包逻辑写得尽量不踩雷可以直接照着下面的思路改。阅读本文不需要特别高的基础只要写过基本的 ArkTS 代码、知道 PixelMap 大概是什么就能跟着落地。标题里提到的那本《精通 HarmonyOS NEXT鸿蒙App开发入门与项目化实战》我确实翻过不少遍书里面讲图片处理时用的就是packing这套老接口。书是好书但 SDK 更新的脚步不会等人我在实际适配中把项目里相关章节的示例代码也顺带验证了一遍。这篇文章里所有结论和示例都基于我真实跑通的迁移过程写成可以直接拿去做参照。2. 新旧 API 差异不只换个方法名那么简单2.1 旧写法里的两个深层问题先看一段常见的旧代码这是鸿蒙 5.0 及更早版本里非常典型的图片压缩上传实现import { image } from kit.ImageKit; import { fileIo as fs } from kit.CoreFileKit; import { BusinessError } from kit.BasicServicesKit; async function compressAndSave(pixelMap: image.PixelMap, targetPath: string) { const packer image.createImagePacker(); const packOpts: image.PackingOption { format: image/jpeg, quality: 85 }; try { const data: ArrayBuffer await packer.packing(pixelMap, packOpts); const file fs.openSync(targetPath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); fs.writeSync(file.fd, data); fs.closeSync(file); packer.release(); } catch (err) { const e err as BusinessError; console.error(compress failed, code: ${e.code}, message: ${e.message}); } }这套写法本身没什么大毛病功能完全正常。但从 API 设计角度来抠它至少存在两个问题第一packing的 Promise 形式把数据一次性全部装进ArrayBuffer图片尺寸越大缓冲区的峰值内存越高。一张几 MB 的照片要压缩成 JPEG中间至少同时存在 PixelMap 的原始数据、编码过程中的中间缓存、结果 ArrayBuffer 三份内存拷贝。在手机这种内存敏感的平台上大批量处理时很快就能把内存水位顶上去。第二资源释放的时机不明确。packer.release()必须在packing成功返回之后手动调用一旦开发者在业务代码里忘了调用或者某个异常分支提前 returnImagePacker 占用的底层资源就会一直挂着。短时间连续创建多个 ImagePacker 且不释放比想象中更容易触发 native 层的资源泄漏。这两点其实是鸿蒙系统底层能力重构后必然要解决的工程问题。官方选择在鸿蒙 6 里直接废弃老的packing改用带Async后缀的新接口除了兼容新的异步规范更重要的就是要引导开发者使用更容易控制生命周期、更贴合底层资源调度模型的新调用方式。2.2 新 API 的正确打开方式新的替代方案在命名上很直白packingAsync对应原来的 Promise 异步场景packingSync对应同步场景另外还保留了回调形式的packingCallback。核心的使用骨架如下import { image } from kit.ImageKit; async function compressWithNewAPI(pixelMap: image.PixelMap): PromiseArrayBuffer { const packer image.createImagePacker(); const packOpts: image.PackingOption { format: image/jpeg, quality: 85 }; try { // 推荐显式 await 结果 const data await packer.packingAsync(pixelMap, packOpts); return data; } finally { // 不管成功失败都释放 packer.release(); } }从这里能看出一个明显变化新接口在语义上更强调“用完即释放”finally配合release()可以确保资源不泄漏。另一个变化是调用栈更短了packingAsync不再像老接口那样需要被系统内部再包一层任务调度耗时和线程切换开销都有明显下降尤其是大批量压缩场景体感差异非常明显。需要特别说明的是packingAsync的入参和返回结构和老接口完全一致都是PixelMap | ImageSource加PackingOption返回PromiseArrayBuffer。这大大降低了迁移成本绝大多数场景可以直接在方法名后面加个Async后缀就能通过编译。真正要花心思处理的反而是一些边缘场景比如同步调用、循环打包、异常分支处理。这些在下一节展开。注意如果你在用 API 12 或更早版本编译packingAsync是不存在的直接用会编译失败。需要先判断 SDK 版本再决定写packing还是packingAsync。这种版本兼容写法我在第 4 节专门给出了一套可复用的工具函数。3. 迁移实操三个场景带你顺利落地3.1 场景一单张图片压缩上传写完即复用这是最典型、出现频率最高的场景用户从相册选了一张照片App 把它缩略成 1080 宽的分辨率再压缩到 85% 质量然后上传到服务器。旧代码里可能是这样写的async function uploadAvatar(originPixelMap: image.PixelMap) { const targetPixelMap await originPixelMap.scale(1080, 1080 * originPixelMap.getPixelMapInfo().size.height / originPixelMap.getPixelMapInfo().size.width); const packer image.createImagePacker(); const opts: image.PackingOption { format: image/jpeg, quality: 85 }; const data await packer.packing(targetPixelMap, opts); // ...上传逻辑 packer.release(); }迁移成新 API 后我会把 ImagePacker 的创建和释放收敛到一个工具函数里避免业务代码里到处分散createImagePacker()和release()import { image } from kit.ImageKit; import { BusinessError } from kit.BasicServicesKit; export async function packImageToBuffer( source: image.PixelMap | image.ImageSource, opts: image.PackingOption ): PromiseArrayBuffer { const packer image.createImagePacker(); try { return await packer.packingAsync(source, opts); } catch (err) { const e err as BusinessError; console.error(packImageToBuffer failed, code: ${e.code}); throw e; } finally { packer.release(); } }这样改完之后业务侧调用只需要一行const buffer await packImageToBuffer(targetPixelMap, { format: image/jpeg, quality: 85 });好处很明显资源管理集中在一处不会出现某个分支忘掉release()的情况。而且这里有个容易被忽略的细节——finally块里做release()即使packingAsync抛异常底层资源也能被正常回收。3.2 场景二多图批量缩略图别在循环里反复创建批量场景是我这次适配中感触最深的。一个图片选择器需要同时生成 9 张缩略图老代码写成循环内部创建 ImagePacker一张图处理完再创建下一张async function generateThumbs(pixelMaps: image.PixelMap[]) { const results: ArrayBuffer[] []; for (const pm of pixelMaps) { const packer image.createImagePacker(); const buf await packer.packing(pm, { format: image/jpeg, quality: 60 }); results.push(buf); packer.release(); } return results; }这种方式功能性上没问题但每张图都经历一次原生对象的创建、初始化、销毁过程在 9 张图以上或者原图分辨率很大时总耗时明显偏高。适配新 API 时我顺手把设计优化成了“复用同一个 ImagePacker 实例、循环处理完统一释放”async function generateThumbsOptimized(pixelMaps: image.PixelMap[]) { const packer image.createImagePacker(); const results: ArrayBuffer[] []; try { for (const pm of pixelMaps) { const buf await packer.packingAsync(pm, { format: image/jpeg, quality: 60 }); results.push(buf); } return results; } finally { packer.release(); } }实测下来同一个 ImagePacker 复用比反复创建销毁大概能快 15% 到 25%具体取决于图片尺寸和格式。不过这里有个前置条件packingAsync方法内部不会保存上一次调用的状态同一实例并发调用是安全的。但如果你对同一个 ImagePacker 实例同时发起多个packingAsync调用还是要小心处理建议串行执行或用Promise.all包一层避免底层资源竞争。心得循环复用 ImagePacker 是推荐做法但不要在循环体内调用release()否则第二次就会报“packer 已释放”的错误。想清理中间状态可以直接复用同一个 packer 处理下一张图。3.3 场景三同步压缩场景别让线程卡死某些场景下不希望你等异步回调比如在生命周期回调里需要快速拿到合成图的字节数据。新 API 提供了packingSync同步方法const packer image.createImagePacker(); const buffer packer.packingSync(pixelMap, { format: image/webp, quality: 90 }); packer.release();同步接口返回ArrayBuffer直接可用没有任何 Promise 嵌套。但必须警告packingSync会阻塞当前线程如果图片很大比如从相机直接拿到的 4000 万像素原图耗时可能超过几百毫秒甚至会到秒级。在 UI 线程调用会导致页面掉帧、点击无响应严重时直接触发主线程超时。我只建议在子线程或者确实很短小的压缩操作里使用同步接口大部分图片压缩上传场景还是走packingAsync更稳妥。4. 常见问题与排查技巧实录4.1 编译告警一大堆到底能不能忽略升级 SDK 之后老代码编译时通常会出现类似这样的告警The packing API is deprecated since API version 18. Recommend using packingAsync.很多开发者的第一反应是“反正还能跑先不管”。我的建议是千万别养成无视废弃告警的习惯。一个项目可能同时有几个页面、几个工具类都调用packing如果一直拖着不处理等鸿蒙 6 正式版本把旧接口从系统库里移除到时候再改的工作量会翻倍而且可能与其它系统升级带来的问题纠缠在一起排查起来非常痛苦。我实际处理时会把告警当作任务清单全局搜索packing(逐个文件确认调用位置评估是简单替换还是需要重构统一修改后跑全量编译和回归测试。如果项目比较老或者代码量特别大可以先改出问题概率最高的几条主链路比如上传、缩略图、导出边缘功能暂缓但务必在建项里记一个待办事项。4.2 老项目兼容多版本 SDK一套代码打天下如果你的应用还不想直接抛弃鸿蒙 5.0 及更早的用户那就要考虑版本兼容。ArkTS 里没有宏定义也没办法直接判断当前编译期 SDK 版本号我们可以用canIUse或运行时能力探测来做分支。实际验证下来最稳妥的方式是通过模块是否存在来判断接口能力。这有个前提新版 SDK 里imagePacker.packing虽然废弃但还保留而老版 SDK 里packingAsync不存在。基于这个前提可以这样封装import { image } from kit.ImageKit; import { BusinessError } from kit.BasicServicesKit; async function packImageCompat( source: image.PixelMap | image.ImageSource, opts: image.PackingOption ): PromiseArrayBuffer { const packer image.createImagePacker(); try { // 运行时探测 packingAsync 是否存在 if (typeof (packer as any).packingAsync function) { return await (packer as any).packingAsync(source, opts); } else { // 老版本走原来的 packing return await packer.packing(source, opts); } } catch (err) { const e err as BusinessError; console.error(packImageCompat failed, code: ${e.code}); throw e; } finally { packer.release(); } }这里用到了(packer as any)的转写法因为在老版本 SDK 的类型声明文件里根本不存在packingAsync属性直接写packer.packingAsync编译都过不去。通过运行时判断老版本走老接口新版本走新接口一套代码两个版本都能跑。注意canIUse更适合用来判断系统 API 是否存在但在当前场景下直接探测方法更可靠因为 ImagePacker 实例的 API 不是静态全局接口canIUse不一定能精确反馈实例方法。这种兼容写法在团队协作里尤其有用不同成员手里的 SDK 版本不一致时不会出现某个人编译通过、另一个人编译报错的尴尬局面。我建议在实际交付代码前用真机分别跑一遍鸿蒙 5 和鸿蒙 6 的测试用例确认两个分支都正常。4.3 PixelMap 的生命周期协作压缩完别急着释放适配过程中最容易忽略的是和 PixelMap 生命周期协作的问题。很多人对 imagePacker 本身做了完善的释放却忘了管理传进来的 PixelMap。举例说明async function compressWithPixelMap(pixelMap: image.PixelMap) { const packer image.createImagePacker(); try { const buffer await packer.packingAsync(pixelMap, { format: image/jpeg, quality: 80 }); // 这里认为 pixelMap 没用了顺手释放 pixelMap.release(); return buffer; } catch (e) { // ... } }这段代码的问题在于pixelMap.release()被放在packingAsync成功返回之后执行看起来没毛病。但如果外层调用方还需要继续使用这个 PixelMap比如还要拿到界面上显示压缩前的原图、或者还要做缩放裁剪这里提前释放就会导致后续操作拿到已失效的对象报错信息通常是 “pixel map has been recycled” 或直接非法访问。我的建议是谁创建了 PixelMap谁负责释放。函数传入的 PixelMap 应该由调用方管理生命周期imagePacker 只负责编码不负责回收输入源。如果确实由当前函数独立创建、且后续没有对外引用的 PixelMap才在本函数内释放而且要放在所有业务逻辑走完之后。4.4 打包结果为空或压缩后图片损坏怎么办实测过程中偶尔会遇到packingAsync返回了一个长度为 0 的ArrayBuffer或者压缩出来的 JPEG 文件无法打开。多半是这几种原因输入的 PixelMap 已经被释放或状态异常建议在调用前打印一下pixelMap.getPixelMapInfo()看看宽高和格式是否正常传入的PackingOption里 format 与数据不匹配比如 PixelMap 是 RGBA 格式却指定输出image/jpeg某些系统版本会直接失败quality 数值超范围PackingOption.quality取值范围是 0 到 100传 101 或负数虽然不一定报错但行为未定义还是规范点好跨线程问题在某些线程模式下低版本 SDK 对 ImagePacker 的线程绑定有隐性要求换个线程环境重试就能恢复遇到输出异常最直接的排查手段是把输入源换成一个已知正常的 PixelMap 测试。如果已知正常的能打包成功问题八九不离十出在输入源上如果已知正常的也失败再回头看 ImagePacker 实例和 PackingOption 的配置。5. 从这次适配里学到的额外一课最后说点这次适配之外的体会。鸿蒙系统的接口演进节奏比很多人预想的要快一个 API 从正式推出到标记废弃中间可能只有两三个大版本。这种演进对开发者其实是个信号写代码时尽量把系统能力封装成自己的工具函数而不是在业务逻辑里到处裸用带版本风险的 API。我把图片打包相关的操作全部收敛到一个工具类里业务层只暴露packImageToBuffer、packImageToBufferMulti、packImageToBufferCompat这几个语义明确的函数。后续就算鸿蒙 7 再把packingAsync换掉我只需要改工具类内部实现所有调用方都不用动。这种“依赖倒置”的做法在 SDK 快速迭代阶段非常划算。另外这次适配过程中我重新审视了一遍项目里对图片资源的生命周期管理发现不少以前埋下的隐患有的 PixelMap 释放时机不对有的 File 句柄在异常路径没关闭。借着这次 API 迁移的机会我把这些资源管理问题一并修掉了。如果你也在做类似的适配建议不要只盯着“把编译告警消掉”这个最低目标顺手把周边资源管理、异常处理都梳理一遍一次迁移解决一整类潜在问题性价比真的很高。