HarmonyOS趣味相机实战第21篇:JPEG解码、PixelMap所有权转移与结果弹层闭环

📅 2026/7/19 16:27:06
HarmonyOS趣味相机实战第21篇:JPEG解码、PixelMap所有权转移与结果弹层闭环
HarmonyOS趣味相机实战第21篇JPEG解码、PixelMap所有权转移与结果弹层闭环摘要PhotoOutput.capture()返回成功只表示拍照请求已经提交不代表页面已经拿到可显示的照片。真实图像会通过photoAvailable异步回调到达再经过 JPEG Component、ImageSource 和 PixelMap 解码。任一资源释放过早都会导致结果弹层黑图释放过晚又会造成 native 内存持续增长。本文基于D:/APP/1quweixiangji趣味相机工程复盘CameraPreviewService.ets与Index.ets的拍照资源链路。我们明确camera.Photo、image.ImageSource和image.PixelMap的所有者使用keepPixelMap完成跨层转移并让“重拍、保存到相册、转文档、页面退出”都进入可验证的释放闭环。工程背景与源码定位文件责任本文关注点entry/src/main/ets/service/CameraPreviewService.etsPhotoOutput 回调与 JPEG 解码资源创建和转移entry/src/main/ets/pages/Index.ets拍照、结果弹层、保存和关闭PixelMap 最终所有者entry/src/main/ets/service/PhotoAlbumService.ets保存照片元数据不长期持有 PixelMapentry/src/main/ets/service/PhotoDocumentService.ets创建图片文档索引继承业务快照entry/src/main/ets/model/DecorationModels.etsCapturedPhoto与CapturedDocument元数据和图像资源分离entry/src/main/ets/service/CoreVisionHumanService.ets对拍照 PixelMap 分析分析不拥有最终资源环境与资源边界项目当前值说明应用版本1.0.4当前工程target SDK6.0.2(22)构建配置图像回调photoAvailable异步到达编码组件JPEGComponentType.JPEG结果图像PixelMap页面弹层显示拍照到达超时项目常量控制请求与回调分离UI 显示策略ImageFit.Cover结果弹层 250 高度一、区分“请求成功”和“照片到达”拍照入口constsetting:camera.PhotoCaptureSetting{quality:captureQualityLevel(qualityPreference),mirror:activeCameraPositionfront};awaitphotoOutput.capture(setting);这一步之后仍要等待constrealPhotoReceived:booleanawaitwaitForPhotoAvailable(captureStartedAt,PHOTO_AVAILABLE_TIMEOUT_MS);因此返回模型包含exportinterfaceCameraCaptureState{success:boolean;message:string;realPhotoReceived:boolean;photoEventCount:number;visionTargetCount:number;photoPixelMap?:image.PixelMap;}success和realPhotoReceived不能合并。调用成功但回调超时需要提示用户重试而不是生成模拟成功数据。二、回调先记录时序再启动解码privatestaticbindPhotoAvailable(photoOutput:camera.PhotoOutput):void{constcallback:AsyncCallbackcamera.Photo(error,photo):void{if(error){hilog.warn(DOMAIN,TAG,photo available failed);return;}lastPhotoAvailableAtDate.now();photoEventCount1;constpromiseanalyzeCapturedPhoto(photo,shouldAnalyzeTargetsOnCapture);lastPhotoVisionPromisepromise;promise.then(result{lastPhotoVisionTargetsresult.targets;lastPhotoVisionAtDate.now();if(result.targets.length0){notifyTargets(result.targets);}}).catch((){hilog.warn(DOMAIN,TAG,photo vision analysis failed);});};photoAvailableCallbackcallback;photoOutput.on(photoAvailable,callback);}先更新时间戳waitForPhotoAvailable()才能知道回调已经发生解码和识别则通过 Promise 单独等待。三、时间戳用于区分本次拍照constcaptureStartedAt:numberDate.now();等待函数privatestaticasyncwaitForPhotoAvailable(captureStartedAt:number,timeout:number):Promiseboolean{if(lastPhotoAvailableAtcaptureStartedAt){returntrue;}constdeadlineDate.now()timeout;while(Date.now()deadline){awaitdelay(80);if(lastPhotoAvailableAtcaptureStartedAt){returntrue;}}returnlastPhotoAvailableAtcaptureStartedAt;}如果只判断事件计数大于 0上一张照片的回调会让新拍照误判为已完成。时间戳提供最小任务归属边界。更严格的实现可维护单调 captureId并让回调结果进入任务队列避免系统时间变化影响判断。四、JPEG解码链路privatestaticasyncanalyzeCapturedPhoto(photo:camera.Photo,analyzeTargets:booleantrue):PromiseCameraPhotoVisionResult{letpixelMap:image.PixelMap|nullnull;letsource:image.ImageSource|nullnull;letkeepPixelMap:booleanfalse;try{constmainImage:image.Imagephoto.main;constcomponent:image.ComponentawaitmainImage.getComponent(image.ComponentType.JPEG);sourceimage.createImageSource(component.byteBuffer);pixelMapawaitsource.createPixelMap();// ...}finally{// ...}}资源关系camera.Photo - photo.main: image.Image - JPEG Component.byteBuffer - ImageSource - PixelMap后一个对象创建成功不代表前一个可以永远不释放。函数必须为每一层定义释放路径。五、keepPixelMap表达所有权转移识别关闭时if(!analyzeTargets){keepPixelMaptrue;return{targets:[],pixelMap};}识别开启时consttargetsawaitCoreVisionHumanService.analyzePixelMap(pixelMap,previewRotation,activeCameraPositionfront);keepPixelMaptrue;return{targets:toCameraTargets(targets),pixelMap};keepPixelMap true表示函数把 PixelMap 的释放责任转交给调用方。它不是“不释放”而是“由下一位所有者释放”。六、finally释放未转移资源finally{if(pixelMap!null!keepPixelMap){try{awaitpixelMap.release();}catch(releaseError){hilog.warn(DOMAIN,TAG,release pixelMap failed);}}if(source!null){try{awaitsource.release();}catch(releaseError){hilog.warn(DOMAIN,TAG,release image source failed);}}try{awaitphoto.release();}catch(releaseError){hilog.warn(DOMAIN,TAG,release captured photo failed);}}无论成功、识别失败还是解码异常ImageSource 和 Photo 都会释放。PixelMap 只有在成功返回给页面时保留。七、为什么ImageSource可以先释放createPixelMap()成功后PixelMap 成为独立图像对象。页面显示的是 PixelMap不需要持续保留 ImageSource。因此函数在返回前释放 source可以缩短资源持有时间。但必须以实际 SDK 行为和真机测试为准。验收时连续打开结果弹层确认 source 释放后 PixelMap 仍稳定显示。八、等待识别Promise而不是猜固定时间privatestaticasyncwaitForPhotoVision(captureStartedAt:number,timeout:number):PromiseCameraPhotoVisionResult{constdeadlineDate.now()timeout;while(Date.now()deadline){if(lastPhotoVisionPromise!null){try{returnawaitlastPhotoVisionPromise;}catch(error){return{targets:[]};}}if(lastPhotoVisionAtcaptureStartedAt){return{targets:lastPhotoVisionTargets};}awaitdelay(40);}return{targets:lastPhotoVisionTargets};}固定sleep(1000)在快设备上浪费时间在慢设备上又可能不够。保存实际 Promise 能让调用方等待真实任务完成。九、超时返回时要处理迟到PixelMap一个隐蔽风险等待函数超时返回后解码 Promise 仍可能稍后完成并产生 PixelMap。如果没有接收者该 PixelMap 可能泄漏。可以让任务带 captureId 与取消状态interfaceCaptureTask{id:number;active:boolean;promise:PromiseCameraPhotoVisionResult;}asyncfunctioncompleteTask(task:CaptureTask,result:CameraPhotoVisionResult):PromiseCameraPhotoVisionResult{if(!task.activeresult.pixelMap){awaitresult.pixelMap.release();return{targets:[]};}returnresult;}超时时把activefalse迟到任务负责清理自己的返回资源。十、页面接收前先释放旧预览页面字段StatepreviewPhotoPixelMap:image.PixelMap|nullnull;新拍照结果到达前应处理旧对象privateasyncreplacePreviewPixelMap(next:image.PixelMap|undefined):Promisevoid{constpreviousthis.previewPhotoPixelMap;this.previewPhotoPixelMapnext??null;if(previous!nullprevious!next){awaitprevious.release();}}先更新状态还是先释放要结合 ArkUI 渲染时序验证。更保守的方式是先从 UI 移除旧对象等待一帧或状态生效后释放再赋新对象。十一、结果弹层显示真实PixelMapif(this.previewPhotoPixelMap!null){Image(this.previewPhotoPixelMap).width(100%).height(250).objectFit(ImageFit.Cover).borderRadius(22)}else{Column().width(100%).height(250).backgroundColor(#1B1B24)}UI 对 null 有明确占位避免解码失败时组件崩溃。占位不能伪装成成功照片状态文案应提示“未获取到照片请重试”。十二、CapturedPhoto只保存业务快照exportinterfaceCapturedPhoto{id:string;title:string;createdAt:string;resolutionLabel?:string;captureSource:CaptureSource;captureSummary:string;status:preview|saved;watermark?:WatermarkSnapshot;}PixelMap 没有放入模型也没有 JSON 序列化到 Preferences。这是正确边界模型用于业务元数据PixelMap 是进程内 native 资源。如果未来保存真实图片应编码到文件或媒体库并在模型中保存 URI而不是长期保存 PixelMap。十三、重拍分支用户点击“重拍”时关闭结果弹层 - 页面从Image移除PixelMap - release PixelMap - 清空previewPhoto - 保留或恢复相机预览项目已有privateasyncclosePreview():Promisevoid{awaitthis.releasePreviewPixelMap();this.previewPhotonull;}关闭按钮与重拍按钮应复用同一释放函数避免两个分支行为不一致。十四、保存到相册分支当前PhotoAlbumService保存元数据constsavedPhotoPhotoAlbumService.savePhoto(photo);constnextPhotos[savedPhoto].concat(cachedPhotos);cachedPhotosnextPhotos.slice(0,60);awaitflushPhotos();保存成功后仍应释放结果 PixelMap因为本地相册卡片目前根据元数据生成缩略视图而不是持有该 PixelMap。如果未来写入真实媒体文件正确顺序PixelMap/JPEG写入临时文件 - fsync或确认写入完成 - 原子重命名或媒体库提交 - Preferences写入URI元数据 - flush - 释放PixelMap文件失败时不能把元数据标记为 saved。十五、转文档分支文档服务从CapturedPhoto生成constdocument:CapturedDocument{id:doc_${Date.now()}_${photo.id},photoId:photo.id,title:${photo.title}文档,pageCount:1,documentType:imageDocument,status:ready,summary,captureSummary:safeCaptureSummary(photo.captureSummary),watermark:cloneWatermark(photo.watermark)};当前是“图片文档索引”不依赖 PixelMap 持久化。创建文档后可以释放预览 PixelMap。若后续生成 PDF 或增强扫描图转码函数应明确是借用 PixelMap 还是接管它不能双方都释放。十六、用类型表达借用与接管ArkTS 没有编译期所有权系统可以用接口约定interfaceBorrowedPixelMap{value:image.PixelMap;owner:previewDialog;}interfaceOwnedPixelMapResult{value:image.PixelMap;releaseBy:caller;}或者通过类封装一次性释放classPixelMapLease{privatereleased:booleanfalse;constructor(readonlyvalue:image.PixelMap){}asyncrelease():Promisevoid{if(this.released){return;}this.releasedtrue;awaitthis.value.release();}}幂等 Lease 可以降低关闭按钮、页面退出和 Surface 销毁多路径重复释放的风险。十七、页面退出必须兜底asyncaboutToDisappear():Promisevoid{awaitthis.releasePreviewPixelMap();awaitCameraPreviewService.stopPreview();}即使用户没有点击弹层按钮页面销毁仍释放。Ability 进入后台时也应根据产品策略关闭弹层资源至少停止相机会话。十八、资源状态机idle - capturing - photoAvailable - decoding - previewOwned - saving | converting | retaking - released每个状态只允许一个 PixelMap 所有者状态所有者decodingCameraPreviewServicepreviewOwnedIndex结果弹层saving保存函数借用页面仍拥有converting转换函数借用页面仍拥有released无若保存函数要异步编码图像关闭弹层不能同时释放需要 Busy 状态或复制独立数据。十九、测试设计使用可注入的资源代理interfaceReleasable{release():Promisevoid;}classFakeResourceimplementsReleasable{releaseCount:number0;asyncrelease():Promisevoid{this.releaseCount1;}}断言JPEG 解码失败时 Photo 释放一次。createPixelMap 失败时 ImageSource 释放一次。成功返回时服务不释放 PixelMap。页面关闭时 PixelMap 释放一次。页面关闭和 aboutToDisappear 连续调用仍只释放一次。超时后的迟到任务会释放无人接收的 PixelMap。新预览替换旧预览时旧对象释放。保存失败不会把照片状态改成 saved。二十、真机压力验收连续拍照并重拍50次 - 观察native内存是否回落 连续拍照并保存30次 - 检查相册条目与结果一致 拍照后立即退后台 - 返回后无黑图和相机占用 拍照后立即切换Tab - 迟到回调不覆盖新页面 前置/后置交替拍照 - mirror和结果方向正确还要故障注入JPEG Component 获取失败。ImageSource 创建失败。PixelMap 创建失败。CoreVision 分析超时。用户快速双击拍照。页面在解码中销毁。二十一、常见问题排查现象可能原因排查方式capture成功但弹层黑图把请求完成当照片到达检查photoAvailable和realPhotoReceived连续重拍内存上涨PixelMap未在关闭分支释放检查所有弹层出口偶发显示上一张照片时间戳/任务ID归属错误按captureStartedAt过滤识别超时后仍涨内存迟到Promise无人接收取消后释放返回PixelMap图片显示一瞬后消失服务过早release PixelMap明确所有权转移保存后相册有记录但无文件只保存元数据增加媒体URI和原子写入关闭与页面退出偶发异常重复release非幂等使用PixelMapLease转文档时图片被释放借用和接管约定不清类型化资源协议二十二、上线前验收清单区分 capture 请求成功和 photoAvailable 到达。每次拍照使用时间戳或任务 ID 归属回调。JPEG Component、ImageSource、PixelMap 链路清晰。Photo 和 ImageSource 在 finally 中释放。PixelMap 只有成功转移时不由服务释放。页面接管后负责关闭、重拍和退出释放。新预览替换旧预览会释放旧资源。迟到任务的无人接收 PixelMap 会释放。CapturedPhoto 不序列化 PixelMap。保存真实文件时先写媒体再提交元数据。转文档明确借用或接管关系。多出口释放具备幂等保护。真机连续拍照后 native 内存稳定。错误日志不包含照片字节和用户图像。总结HarmonyOS 拍照链路的难点不是把 JPEG 解码成 PixelMap而是让异步回调和 native 资源拥有明确归属。camera.Photo与 ImageSource 在服务内释放成功返回的 PixelMap 转交页面页面则在重拍、保存、转文档和退出时完成最终释放。配合任务时间戳、真实 Promise 等待、迟到结果清理和幂等 Lease结果弹层才能既显示真实照片又不会在连续拍照与快速切页中泄漏。业务元数据与图像资源分离后后续接入媒体库 URI 或 PDF 转换也能保持清晰边界。