HarmonyOS趣味相机实战第12篇CameraKit错误分层、相机占用恢复与用户可重试状态机摘要CameraKit 相机项目最难排查的故障往往都被用户描述成一句“相机打不开”。实际原因可能完全不同相机权限被拒绝、设备没有可用镜头、XComponent Surface 尚未创建、旧 PhotoSession 没释放、相机被其他应用占用、photoAvailable没在超时窗口内返回甚至只是页面拿到了旧任务的迟到 PixelMap。如果所有错误都显示“请重试”用户无法恢复开发者也无法从日志定位。本文继续基于D:/APP/1quweixiangjiHarmonyOS ArkTS 趣味相机项目把相机故障拆成权限、设备、Surface、预览、拍照和释放六层复盘现有服务如何把系统异常转换成业务状态并给出可直接落地的错误码映射、恢复动作、任务代际和真机回归方案。工程背景与源码定位文件主要错误边界entry/src/main/ets/service/CameraPermissionService.ets运行时权限 granted、denied、errorentry/src/main/ets/service/CameraDeviceService.ets摄像头发现、方向不可用和默认镜头降级entry/src/main/ets/service/CameraPreviewService.etsSurface、CameraInput、PhotoSession、输出和拍照回调entry/src/main/ets/pages/Index.ets页面状态、用户提示、重试入口和结果所有权entry/src/main/module.json5ohos.permission.CAMERA声明entry/src/test/LocalUnit.test.ets当前水印快照测试可扩展错误路由纯函数测试环境与版本边界项目当前值说明工程路径D:/APP/1quweixiangji本文按当前源码分析应用版本1.0.2来自AppScope/app.json5target SDK6.0.2(22)SDK 升级后要回归错误码和会话行为应用模型HarmonyOS Stage 模型EntryAbility加载单页工作台相机能力kit.CameraKit设备、输入、会话、预览和拍照日志能力kit.PerformanceAnalysisKit使用 hilog 记录可公开诊断信息图像对象ohos.multimedia.imagePixelMap、ImageSource 与照片组件本地构建命令cd D:\APP\1quweixiangji$env:JAVA_HOMED:\Program Files\Huawei\DevEco Studio\jbr$env:Path$env:JAVA_HOME\bin;$env:PathD:\Program Files\Huawei\DevEco Studio\tools\hvigor\bin\hvigorw.bat--mode module-p moduleentrydefault-p productdefault assembleHap--no-daemon一、先定义错误发生在哪一层建议把相机链路看成六层闸门权限 - 设备 - Surface - 预览会话 - 拍照回调 - 资源释放每一层的责任不同层级成功条件失败后是否应该继续权限用户授予 CAMERA否不能发现或打开相机设备找到至少一个可用镜头否展示设备不可用SurfaceXComponent 返回有效 Surface ID等待不应提前启动预览预览PhotoSession 成功 start否释放半成品并允许重试拍照本次收到真实 photoAvailable 与 PixelMap预览可保持允许再次拍照释放所有对象 stop/release/close失败也要继续释放其他对象只有先确定层级恢复动作才不会错。二、权限错误拒绝和 API 异常不是一回事权限服务定义exporttypeCameraPermissionStatusidle|granted|denied|error;exportinterfaceCameraPermissionResult{status:CameraPermissionStatus;message:string;}申请逻辑constresultawaitatManager.requestPermissionsFromUser(context,[ohos.permission.CAMERA]);constgranted:booleanresult.authResults.length0result.authResults[0]0;拒绝返回return{status:denied,message:相机权限未开启请开启后使用真实取景};API 异常返回catch(error){hilog.error(DOMAIN,TAG,request camera permission failed: %{public}s,JSON.stringify(error));return{status:error,message:相机权限申请失败请稍后重试};}两种状态的恢复不同状态用户动作denied再次阅读权限用途前往系统设置或重新授权error稍后重试必要时重启应用页面在非 granted 时必须把cameraAvailable设为 false不能继续走设备发现。三、设备错误不要把没有镜头误判成权限问题设备服务先读取constmanager:camera.CameraManagercamera.getCameraManager(context);constsupportedDevices:camera.CameraDevice[]manager.getSupportedCameras();然后分别探测前后方向privatestatichasCamera(manager:camera.CameraManager,position:camera.CameraPosition):boolean{try{constdevice:camera.CameraDevicemanager.getCameraDevice(position,camera.CameraType.CAMERA_TYPE_DEFAULT);returndevice!undefined;}catch(error){hilog.warn(DOMAIN,TAG,camera position unavailable: %{public}s,JSON.stringify(error));returnfalse;}}返回状态包含设备总数、前后镜头和最终方向。这样可以区分权限已开但模拟器没有真实相机。只有后置没有前置。用户偏好前置但当前设备只能降级到后置。CameraManager 查询本身抛异常。没有设备时提示应是“未检测到可用摄像头”而不是让用户反复授权。四、Surface 错误尚未就绪应该等待预览服务入口先检查if(surfaceId.length0){return{status:error,message:预览画面尚未就绪};}页面则通过 XComponent 获取privateonPreviewSurfaceReady():void{this.previewSurfaceIdthis.previewController.getXComponentSurfaceId();if(this.isCameraGranted()this.cameraAvailable){this.startCameraPreview();}}严格来说“Surface 尚未就绪”通常是等待状态不是不可恢复错误。页面应该使用三重闸门if(!this.isCameraGranted()||!this.cameraAvailable||this.previewSurfaceId.length0){return;}权限、设备和 Surface 谁最后完成谁触发统一的tryStartPreview()避免重复启动。五、预览失败必须释放半成品资源预览服务启动前先调用awaitCameraPreviewService.stopPreview();之后依次创建 CameraManager、Device、CameraInput、PreviewOutput、PhotoOutput 和 PhotoSession。任何一步失败catch 中执行catch(error){hilog.error(DOMAIN,TAG,start preview failed: %{public}s,JSON.stringify(error));awaitCameraPreviewService.stopPreview();return{status:error,message:相机预览启动失败请检查权限或相机占用};}这里的核心不是提示文案而是再次调用统一释放。否则可能留下已经 open 的 CameraInput 或只配置了一半的 PhotoSession下一次重试就更容易报相机冲突。六、把 CameraKit 错误码映射成恢复动作相机输入和会话相关场景需要重点关注以下错误信号错误码方向用户提示程序恢复7400102操作不允许当前相机状态不可用检查调用顺序释放后重建7400107相机冲突相机可能被占用释放本应用旧会话提示关闭其他相机应用7400108安全原因禁用相机被系统安全策略禁用引导检查系统设置不循环重试7400201相机服务致命错误相机服务异常完整释放限次重建仍失败则停止可以定义业务错误exporttypeCameraErrorKindpermissionDenied|deviceUnavailable|surfaceNotReady|cameraConflict|securityDisabled|serviceFatal|captureTimeout|unknown;错误对象只负责分类页面根据 kind 选择按钮和文案。七、避免无限自动重试相机冲突和服务异常不适合无上限重建。推荐constMAX_PREVIEW_RETRY:number2;privatepreviewRetryCount:number0;恢复策略第一次失败完整释放并等待短暂退避。第二次失败允许再重建一次。超过上限停止自动重试显示“重新启用相机”按钮。用户主动点击后把计数清零开始新一轮。安全禁用和权限拒绝不应该自动重试因为外部状态没有改变。八、用会话代际淘汰旧启动结果镜头切换、页面退出和 Surface 重建可能同时触发异步启动。建议privatepreviewGeneration:number0;privateasyncstartCameraPreview():Promisevoid{constgeneration:numberthis.previewGeneration;this.previewStatusstarting;constresult:CameraPreviewStateawaitCameraPreviewService.startPreview(context,this.previewSurfaceId,this.activeCameraPosition);if(generation!this.previewGeneration){return;}this.previewStatusresult.status;this.previewStatusTextresult.message;}页面退出或 Surface 销毁时递增 generation。旧启动即使晚到也不能覆盖新状态。九、拍照错误不应该摧毁可用预览服务在photoOutput为空时返回return{success:false,message:相机拍照输出尚未就绪,realPhotoReceived:false,photoEventCount:CameraPreviewService.photoEventCount,visionTargetCount:0};页面调用后继续检查if(!captureState.realPhotoReceived||captureState.photoPixelMapundefined){this.isCapturingfalse;return;}拍照失败时预览会话可能仍然可用。正确行为通常是不创建CapturedPhoto。不进入结果弹层。保持预览运行。恢复快门按钮。给用户一次明确重试机会。除非 CameraKit 返回会话级或服务级错误否则不要因为单次拍照超时就重建整个预览。十、区分 capture 调用成功和真实照片到达PhotoOutput.capture()没抛异常只能说明触发调用被接受。项目继续等待本次photoAvailableconstrealPhotoReceived:booleanawaitCameraPreviewService.waitForPhotoAvailable(captureStartedAt,350);若没有真实回调返回message:未获取到真实照片请重试页面不能只看success还要看realPhotoReceived和photoPixelMap。这避免把旧预览画面或空记录保存到相册。十一、迟到回调要验证任务归属当前服务使用captureStartedAt区分本次照片if(CameraPreviewService.lastPhotoAvailableAtcaptureStartedAt){returnPromise.resolve(true);}页面层还应增加 capture generation。若用户已经退出页面、重新拍摄或切换镜头迟到结果要立即释放if(generation!this.captureGeneration||!this.pageActive){if(captureState.photoPixelMap!undefined){awaitcaptureState.photoPixelMap.release();}return;}“不再展示”不代表“可以不释放”。PixelMap 所有权已经交给页面失效结果仍由页面收口。十二、释放错误要逐项隔离统一释放中通常包含取消视觉采样 - 移除 photoAvailable/metadata 回调 - session.stop() - session.release() - metadataOutput.release() - photoOutput.release() - previewOutput.release() - cameraInput.close()某个 release 失败不能阻断后续对象释放。推荐每一步独立 try/catch并记录对象类型。释放入口要保持幂等多次调用不会因为静态引用仍指向旧对象而重复操作。十三、页面提示必须带下一步动作对用户有用的错误提示至少包含“发生了什么”和“现在能做什么”错误不推荐文案推荐文案权限拒绝相机失败相机权限未开启请授权后重试无设备相机失败未检测到可用摄像头相机冲突请重试相机可能被占用请关闭其他相机应用后重试安全禁用请重试相机被系统禁用请检查安全设置拍照超时拍照成功未获取到真实照片请保持预览后重试服务异常未知错误相机服务异常已停止自动重试错误文案不要暴露完整异常栈、文件路径或系统内部对象。十四、日志字段设计建议每次预览与拍照记录hilog.info(DOMAIN,TAG,preview generation%{public}d position%{public}s status%{public}s,generation,this.activeCameraPosition,result.status);推荐字段字段用途previewGeneration区分预览重建轮次captureGeneration区分拍照轮次activeCameraPosition定位前置或后置差异previewStatus判断失败阶段surfaceReady判断 Surface 闸门photoEventCount判断回调是否到达errorCode映射 CameraKit 恢复动作不要记录照片内容、PixelMap 字节、水印地点、用户备注或人体关键点。十五、建议补充的纯函数测试错误码路由可以抽成纯函数exportfunctionmapCameraError(code:number):CameraErrorKind{if(code7400107){returncameraConflict;}if(code7400108){returnsecurityDisabled;}if(code7400201){returnserviceFatal;}returnunknown;}测试矩阵expect(mapCameraError(7400107)).assertEqual(cameraConflict);expect(mapCameraError(7400108)).assertEqual(securityDisabled);expect(mapCameraError(7400201)).assertEqual(serviceFatal);expect(mapCameraError(-1)).assertEqual(unknown);还可以测试哪些错误允许自动重试kind自动重试permissionDenied否deviceUnavailable否surfaceNotReady等待 Surface不计为失败cameraConflict限次重试securityDisabled否serviceFatal完整释放后限次重试captureTimeout保持预览允许用户再次拍照十六、真机故障注入流程权限路径首次启动拒绝权限。确认不发现设备、不启动预览。再次授权后确认能恢复。相机占用让另一相机应用持有摄像头。返回趣味相机启动预览。确认识别冲突、限次重试并给出明确提示。Surface 生命周期预览运行时销毁 XComponent。确认旧会话释放。Surface 重建后只启动一次新会话。拍照超时连续拍摄并快速切后台。验证迟到回调不会创建旧照片。迟到 PixelMap 被释放。服务错误记录错误码、镜头方向和 generation。完整释放后限次重建。超过重试上限后停止循环。十七、常见问题排查现象可能原因排查方式反复授权仍黑屏实际是设备或 Surface 层错误分层检查权限、设备、Surface切镜头后旧文案覆盖新状态异步启动结果过期增加 previewGeneration预览失败后一直冲突半成品资源未释放catch 中统一 stopPreview点击拍照无结果photoAvailable 未在窗口内返回查 captureStartedAt 与事件计数拍照超时却生成照片页面只检查 success同时检查 realPhotoReceived 和 PixelMap页面退出后弹出旧照片迟到结果未校验代际检查 captureGeneration内存持续增长失效 PixelMap 未释放所有取消路径显式 releaserelease 抛错后仍占相机一步失败阻断后续释放每个对象独立 try/catch相机冲突无限重试没有重试上限设置 MAX_PREVIEW_RETRY安全禁用仍循环打开错误码路由错误7400108 不自动重试用户不知道如何恢复文案只有“失败”文案加入下一步动作日志泄露用户内容记录了照片或水印正文只记录状态、代际和错误码十八、上线前验收清单权限 denied 与 permission API error 分开处理。未授权时不进入设备发现。无设备时不提示用户重复授权。Surface 未就绪时等待不启动 PhotoSession。预览启动失败时释放全部半成品资源。7400102、7400107、7400108、7400201有业务映射。自动重试有次数上限。权限拒绝和安全禁用不自动重试。预览异步结果按 generation 校验。拍照失败不默认销毁仍可用的预览。页面检查 realPhotoReceived 和 photoPixelMap。迟到 PixelMap 在失效路径释放。每个系统资源独立 stop/release/close。释放入口可重复调用。错误文案包含可执行的恢复动作。日志包含镜头、状态、代际和错误码。日志不记录照片、水印或人体数据。拒绝权限、相机占用、切后台、Surface 重建和拍照超时完成真机验证。SDK 升级后重新确认错误码和 CameraKit 生命周期行为。总结相机稳定性不是给每个 catch 加一句“请重试”而是先判断错误属于权限、设备、Surface、预览、拍照还是释放层再为这一层选择正确恢复动作。1quweixiangji当前已经通过服务层状态对象、预览失败统一释放、真实照片确认和 PixelMap 所有权控制打好了基础。进一步完善时应加入错误码业务映射、预览与拍照 generation、限次重试、迟到结果释放和带下一步动作的用户文案。这样“相机打不开”就能被拆成可观测、可恢复、可测试的状态机相机冲突不会无限循环拍照超时不会生成假记录页面退出也不会接收旧任务结果。