HarmonyOS技术精讲-Camera Kit(相机服务)第11篇:分段式拍照实践

发布时间:2026/7/17 9:55:38
HarmonyOS技术精讲-Camera Kit(相机服务)第11篇:分段式拍照实践 《HarmonyOS技术精讲-Camera Kit相机服务第11篇分段式拍照实践》为什么需要分段式拍照HarmonyOS NEXT 的相机开发里拍照速度是一个容易被低估的坑。大部分开发者第一次实现拍照功能时会直接用PhotoOutput.capture等待完整照片返回——这个操作在普通场景下没问题但如果设备正在处理高分辨率图像、HDR 合成或者多帧降噪回调时间可能长达 2-3 秒。分段式拍照Deferred Capture解决的是这个问题拍照动作和照片处理解耦。用户按下快门瞬间得到一张“快速预览图”后台继续计算完整照片。用户不用死盯着取景框等转圈体验差距很明显。这个方案适合拍照速度敏感的 App社交分享、扫码记录不适合需要实时预览照片元数据的场景专业摄影、科研分析。如果业务上要求每次拍照后立刻拿到完整照片去校验分段式反而增加了复杂度。环境说明DevEco Studio 版本DevEco Studio 6.1.0 及以上 HarmonyOS SDK 版本HarmonyOS 6.1.0(23) 及以上 目标设备手机核心实现分段式拍照完整流程1. 初始化相机和预览流第一步也是被很多人忽略的一步分段式拍照依赖DeferredPhotoOutput对象但它必须在CameraSession配置阶段就绑定。不能等用户按下快门再去创建。import{camera}fromkit.CameraKit;import{image}fromkit.ImageKit;import{common}fromkit.AbilityKit;letcameraManager:camera.CameraManager;letcameraInput:camera.CameraInput;letpreviewOutput:camera.PreviewOutput;letdeferredPhotoOutput:camera.DeferredPhotoOutput;letsession:camera.PhotoSession;asyncfunctioninitCamera(context:common.Context,surfaceId:string):Promisevoid{// 获取 CameraManagercameraManagercamera.getCameraManager(context);if(!cameraManager){console.error(CameraManager init failed);return;}// 获取可用相机constcameras:Arraycamera.CameraDevicecameraManager.getSupportedCameras();if(cameras.length0){console.error(No camera available);return;}// 创建会话 - 这步很关键后续所有Output都要在session中配置sessioncameraManager.createSession(photo)ascamera.PhotoSession;session.beginConfig();// 创建相机输入cameraInputcameraManager.createCameraInput(cameras[0]);awaitcameraInput.open();session.addInput(cameraInput);// 创建预览输出previewOutputcamera.createPreviewOutput(surfaceId);session.addOutput(previewOutput);// 创建分段式拍照输出 - 重点deferredPhotoOutputcamera.createDeferredPhotoOutput();session.addOutput(deferredPhotoOutput);awaitsession.commitConfig();awaitsession.start();}这里有一个容易踩的坑createDeferredPhotoOutput创建的是专门用于分段式拍照的 Output 对象它和普通PhotoOutput互斥——不能同时添加到同一个 Session。如果业务上还需要普通拍照必须两个 Session 切换。2. 实现快速拍照按钮用户按下快门时核心逻辑是调session.startDeferredPhoto而不是deferredPhotoOutput.capture。这一点设计有些反直觉。StatephotoCount:number0;StatelastPhotoUri:string;privatecaptureReady:booleanfalse;// 快速拍照按钮的点击回调asyncfunctiontakeQuickPhoto():Promisevoid{if(!deferredPhotoOutput||!session){console.error(Camera not initialized);return;}if(!captureReady){console.warn(Previous capture still processing);return;}captureReadyfalse;try{// 关键API: 启动分段式拍照awaitsession.startDeferredPhoto((err:BusinessError,photo:camera.DeferredPhoto){if(err){console.error(Deferred photo error:${err.message});captureReadytrue;return;}// 收到快速预览图回调console.info(Quick photo received, id:${photo.photoId});// 可以在这里展示缩略图// 注意photo.image 是 PixelMap可以直接绑定到 Image 组件handleQuickPhoto(photo);});}catch(error){console.error(Capture failed:${error});captureReadytrue;}}functionhandleQuickPhoto(photo:camera.DeferredPhoto):void{// photo.image 是快速预览图if(photo.image){// 假设 UI 有一个 State 绑定的 image 变量// this.quickPhoto photo.image;console.info(Quick photo pixelMap received);}// 注册后台处理完成的回调photo.on(photoAvailable,(err:BusinessError,result:camera.Photo){if(err){console.error(PhotoAvailable error:${err.code},${err.message});}else{// result 是最终完整的照片console.info(Full photo available:${result.mainUri});// 更新UI, 保存到相册等操作handleFullPhoto(result);}// 允许下一次拍照captureReadytrue;});}functionhandleFullPhoto(photo:camera.Photo):void{// 获取完整照片的uriconstphotoUriphoto.mainUri;console.info(Full photo saved at:${photoUri});// 更新UI状态// this.lastPhotoUri photoUri;// this.photoCount;}注意captureReady标志位的设计——很多人在这个逻辑里出现并发问题连续点击快门时、上次处理还没完成就去触发下一次导致startDeferredPhoto被多次调用Session 内部状态紊乱。实际开发里这是高频错误。3. 注册后台处理完成的通知分段式拍照的核心机制就是“先给你预览图后台算完再通知”。这个通知依赖photoAvailable事件事件触发时返回的Photo对象已经携带了完整照片的文件 URI。监听这个事件有生命周期限制如果页面在后台处理完成前被销毁事件监听也会被回收。推荐在onPageHide或组件卸载时临时保存一个标识等页面恢复后重新查询。EntryComponentstruct CameraPage{StatequickPhotoPixelMap:PixelMap|nullnull;StatefullPhotoUri:string;StatetakeEnabled:booleantrue;build(){Column(){// 预览区域XComponent({id:cameraXComponent,type:XComponentType.SURFACE,controller:newXComponentController()}).width(100%).height(70%)// 快速预览缩略图if(this.quickPhotoPixelMap){Image(this.quickPhotoPixelMap).width(80).height(80)}// 拍照按钮Button(快速拍照).width(120).height(120).borderRadius(60).backgroundColor(this.takeEnabled?#007AFF:#999).enabled(this.takeEnabled).onClick((){this.takeQuickPhoto();})// 最后一张完整照片状态if(this.fullPhotoUri){Text(完整照片已保存:${this.fullPhotoUri}).fontSize(14)}}.width(100%).height(100%)}asynctakeQuickPhoto():Promisevoid{// 复用上面的 takeQuickPhoto 函数同时更新 UI 状态this.takeEnabledfalse;// ... 完整实现}}常见问题问题1页面返回后后台照片丢失现象用户拍照后立刻返回首页后台照片处理完成后找不到 UI 实例去触发回调。原因photoAvailable回调绑定在DeferredPhoto对象上这个对象生命周期跟随当前 Page。页面销毁后回调自然失效。解决方案不要在回调里直接更新 UI改为向全局事件总线发送事件或者写入本地数据库。推荐用EventHub。// 在 onPageShow 时注册全局监听onPageShow():void{AppStorage.setOrCreate(photoAvailable,(uri:string){// 更新UIthis.fullPhotoUriuri;});}问题2连续快速拍照导致 Session 报错现象用户在 0.5 秒内连续点击拍照按钮控制台出现camera session is busy。原因startDeferredPhoto是异步操作内部有防重入机制。但并发调用没有做保护第二次调用时前一次还在执行。解决方案加一个boolean锁配合try/finally确保状态回滚。上面的takeQuickPhoto已经演示了这个方案。问题3模拟器上拍照无响应现象代码在真机上正常在模拟器上点击拍照按钮没有回调。原因分段式拍照依赖硬件 ISP 后处理能力模拟器模拟的相机 API 不完整startDeferredPhoto可能直接返回错误或永不回调。解决方案只在真机上测试此功能。代码里可以加systemCapability判断if(canIUse(SystemCapability.Multimedia.Camera.DeferredPhoto)){// 支持分段式}else{// 回退到普通拍照}最佳实践不要把session.startDeferredPhoto放在异步函数的 finally 块里很多人为了确保锁释放在 finally 中调用session.startDeferredPhoto。但 Session 状态机不允许连续调用finally 释放锁后如果用户再次点击第二次调用还在前一次处理中。正确的做法是在photoAvailable回调里释放锁。预览图拿到后尽快展示并释放内存DeferredPhoto.image是 PixelMap如果占用着持有不释放内存占用会持续增大。拿到 PixelMap 后转成 BitMap 显示然后手工释放原对象。ArkTS 的垃圾回收机制不如 Java 及时建议在onPageHide时释放所有 PixelMap 引用。控制后台处理超时官方没有暴露后台处理超时时间。实测某些设备在多帧降噪场景下完整照片生成可能超过 10 秒。如果业务上需要建议在takeQuickPhoto的同时启动一个定时器超过 5 秒后提示用户“照片正在后台处理稍后查看”。避免用户觉得 App 无响应。FAQQ分段式拍照能同时保存原始格式和 JPEG 吗A不能。DeferredPhotoOutput只支持 JPEG 编码不支持imageFormat参数修改。如果需要 RAW必须回退到普通拍照。Q为什么真机正常模拟器不生效A模拟器缺少硬件 ISP 后处理能力startDeferredPhoto的回调永远不触发。这是 API 的行为限制不是代码问题。建议用真机调试。Q拍照后页面马上返回后台照片还能存到相册吗A可以。photoAvailable回调里的Photo对象已经保存到临时目录返回不影响写入操作。但回调触发时如果当前 page 已经 onHide可以主动调用MediaLibraryAPI 来完成文件迁移。