HarmonyOS技术精讲-Camera Kit(相机服务)第4篇:拍照并保存照片

📅 2026/7/14 23:16:01
HarmonyOS技术精讲-Camera Kit(相机服务)第4篇:拍照并保存照片
HarmonyOS技术精讲-Camera Kit相机服务第4篇拍照并保存照片拍照是相机应用最核心的功能没有之一。上一篇我们完成了预览这次就是把“看”变成“拍”。为什么把拍照单独拿出来写因为拍照本身不是难点难点在于按下快门后图片怎么存、存到哪里、怎么保证不丢帧、不被系统回收。这些问题在官方示例里往往一笔带过但实际项目里全是要命的细节。这篇教程的目标很简单在上一篇预览的基础上增加一个拍照按钮按下后拍一张照片并保存到媒体库让用户在相册里直接看到。拍照功能解决什么问题拍照功能在 Camera Kit 中的定位是“截取当前预览帧”生成一张静态图片。这个功能的核心应用场景很明确用户想要记录当前画面需要将图片持久化保存希望图片能被其他应用如相册访问它不适合的场景连拍需要单独实现帧回调录像过程中抓拍需要额外处理生成原始 RAW 格式需要单独配置和预览相比拍照引入了两个关键新角色PhotoOutput负责拍照输出配置和触发MediaLibrary / phAccessHelper负责将图片写入媒体库环境说明DevEco Studio 版本DevEco Studio 6.1.0 及以上 HarmonyOS SDK 版本HarmonyOS 6.1.0(23) 及以上 目标设备手机核心实现从预览到拍照第一步配置 PhotoOutput在上一篇的代码基础上我们需要创建一个PhotoOutput实例。关键点PhotoOutput 必须在 CameraManager 创建会话之前完成配置。// PhotoOutputConfig.etsimport{camera}fromkit.CameraKit;import{image}fromkit.ImageKit;exportfunctioncreatePhotoOutput(cameraManager:camera.CameraManager):camera.PhotoOutput|undefined{// 创建图片接收器用于接收拍照后的图片数据letreceiver:image.ImageReceiverimage.createImageReceiver({width:1920,height:1080,format:image.ImageFormat.JPEG,// 使用 JPEG 格式兼容性好capacity:1// 缓存1帧即可不需要多帧});// 获取 ImageReceiver 的 surfaceIdletphotoSurfaceId:stringreceiver.getReceivingSurfaceId();// 通过 surfaceId 创建 PhotoOutputletphotoOutput:camera.PhotoOutput|undefinedcameraManager.createPhotoOutput(photoSurfaceId);if(photoOutputundefined){console.error(创建 PhotoOutput 失败);returnundefined;}// 注册拍照完成回调photoOutput.on(photoCaptureStateChange,(captureState:camera.PhotoCaptureState){if(captureState.statecamera.PhotoCaptureStateId.PHOTO_CAPTURE_STATE_ID_DONE){console.info(拍照完成);}});returnphotoOutput;}这段代码要注意几个点image.createImageReceiver第一个参数是配置对象width和height决定输出图片的分辨率。这里用 1920x1080 是一个折中方案太高会导致内存开销大太低画质差format建议用 JPEG虽然 PNG 画质更好但文件体积大而且大多数场景下 JPEG 完全可以满足需求capacity设置为 1 就够了拍照是单次操作不需要缓存多帧photoCaptureStateChange回调可以用来做拍照完成后的 UI 反馈比如震动或提示第二步触发拍照拍照入口是一个按钮点击后调用photoOutput.capture()。// PhotoCaptureButton.etsimport{camera}fromkit.CameraKit;Componentexportstruct PhotoCaptureButton{privatephotoOutput:camera.PhotoOutput|undefined;build(){Column(){Button(拍照).width(80).height(80).borderRadius(40).backgroundColor(#FF0000).onClick((){this.takePicture();})}.width(100%).height(100%).justifyContent(FlexAlign.End)}takePicture(){if(this.photoOutputundefined){console.warn(PhotoOutput 未初始化);return;}// 拍照前检查设备是否处于拍照状态if(this.photoOutput.isSupportedPhotoType(camera.PhotoCaptureType.PHOTO_CAPTURE_TYPE_NORMAL)){this.photoOutput.capture();}else{console.error(当前设备不支持普通拍照模式);}}}这里有个容易被忽略的点不是所有设备都支持PHOTO_CAPTURE_TYPE_NORMAL有些专业相机模式可能只支持 RAW 输出。所以isSupportedPhotoType这个检查很有必要。第三步保存图片到媒体库这是最复杂也最容易出错的部分。当photoCaptureStateChange回调触发后我们需要从ImageReceiver中取出图片数据然后写入媒体库。// PhotoSaver.etsimport{image}fromkit.ImageKit;import{mediaLibrary}fromkit.MediaLibraryKit;import{photoAccessHelper}fromkit.MediaLibraryKit;import{dataSharePredicates}fromkit.ArkData;exportclassPhotoSaver{privatereceiver:image.ImageReceiver;constructor(receiver:image.ImageReceiver){this.receiverreceiver;}asyncsaveToGallery(uri?:string):Promisevoid{try{// 1. 从 ImageReceiver 中获取最新的图片letimageData:image.Imageawaitthis.receiver.readNextImage();if(imageDataundefined){console.error(获取图片数据失败);return;}// 2. 将图片转换为 ArrayBufferletimageBuffer:ArrayBufferimageData.getComponent(4).buffer;// 4 对应 JPEG 组件if(imageBuffer.byteLength0){console.error(图片数据为空);return;}// 3. 获取媒体库访问权限letcontextgetContext(this);letphAccessHelperphotoAccessHelper.getPhotoAccessHelper(context);// 4. 创建媒体库 URIletphotoUriawaitphAccessHelper.createAsset(photoAccessHelper.PhotoType.PHOTO_TYPE_NORMAL,jpg);if(photoUriundefined){console.error(创建媒体库资源失败);return;}// 5. 写入图片数据letfileAssetawaitphAccessHelper.getAssetByUri(photoUri);letfdawaitfileAsset.open(Rw);awaitfileAsset.write(imageBuffer,0,imageBuffer.byteLength,fd);awaitfileAsset.close(fd);// 6. 释放图片资源imageData.release();console.info(图片保存成功URI: photoUri);}catch(error){console.error(保存图片失败: JSON.stringify(error));}}}这个saveToGallery方法里有几个关键点需要解释imageData.getComponent(4)在 ImageReceiver 中JPEG 格式对应的组件索引是 4。这个数字不是随便选的是 HarmonyOS 定义好的编码映射getContext(this)必须在 UI 线程或具有上下文的组件中调用否则无法获取媒体库权限phAccessHelper.createAsset会在媒体库中创建一个新文件返回 URI。注意这个操作是异步的fileAsset.open(Rw)以读写模式打开文件才能写入数据imageData.release()释放原生资源非常重要。如果不调用会导致内存泄漏连续拍几十张后应用直接闪退第四步整合到主页面现在把上面的三个模块整合到主页面中。// Index.etsimport{camera}fromkit.CameraKit;import{image}fromkit.ImageKit;import{PhotoSaver}from./PhotoSaver;import{PhotoCaptureButton}from./PhotoCaptureButton;import{createPhotoOutput}from./PhotoOutputConfig;EntryComponentstruct Index{StatecameraManager:camera.CameraManager|undefinedundefined;StatephotoOutput:camera.PhotoOutput|undefinedundefined;privatereceiver:image.ImageReceiver|undefinedundefined;privatephotoSaver:PhotoSaver|undefinedundefined;aboutToAppear(){this.initCamera();}asyncinitCamera(){// 获取 CameraManager省略上下文获取逻辑this.cameraManagerawaitcamera.getCameraManager(getContext(this));// 创建 PhotoOutputletresultcreatePhotoOutput(this.cameraManager);if(result!undefined){this.photoOutputresult.photoOutput;this.receiverresult.receiver;this.photoSavernewPhotoSaver(result.receiver);// 注册拍照完成后的自动保存this.photoOutput.on(photoCaptureStateChange,(captureState:camera.PhotoCaptureState){if(captureState.statecamera.PhotoCaptureStateId.PHOTO_CAPTURE_STATE_ID_DONE){this.photoSaver?.saveToGallery();}});}}build(){Stack(){// 预览组件省略CameraPreview()// 拍照按钮PhotoCaptureButton({photoOutput:this.photoOutput})}.width(100%).height(100%)}}整合后整个流程是这样的页面加载 → 初始化 CameraManager → 创建 PhotoOutput用户点击拍照按钮 → 触发capture()系统完成拍照 → 回调photoCaptureStateChange回调中自动调用saveToGallery()→ 保存到媒体库常见问题问题 1拍照后图片没有出现在相册现象代码执行成功但打开系统相册看不到刚刚拍的照片。原因createAsset创建的资产默认可能没有更新媒体库索引或者保存的路径不对。解决方案在写入完成后使用phAccessHelper.notifyChange()通知媒体库刷新awaitphAccessHelper.notifyChange(photoUri);问题 2按拍照按钮后 App 闪退现象点击按钮后应用无响应或直接退出。原因最常见的原因是ImageReceiver的capacity为 1但多次快速点击拍照导致图片数据被覆盖引发内存访问冲突。解决方案在takePicture中增加状态锁防止重复触发privateisCapturing:booleanfalse;takePicture(){if(this.isCapturing){return;}this.isCapturingtrue;this.photoOutput?.capture();// 拍照完成或失败后解锁setTimeout((){this.isCapturingfalse;},500);}问题 3真机正常模拟器保存失败现象在模拟器上执行createAsset时返回 undefined。原因部分模拟器没有模拟真实的媒体库环境导致访问受限。解决方案真机调试是必须的。模拟器只能验证拍照回调是否触发保存逻辑必须在真机上测试。最佳实践不要在回调中执行耗时操作photoCaptureStateChange回调运行在相机内部线程如果直接在里面执行文件写入会阻塞相机管线导致后续操作延迟。建议用setTimeout或async调度到其他线程。优先使用 JPEG 格式JPEG 的压缩比和画质平衡最好而且媒体库原生支持不需要额外转换。PNG 虽然无损但文件体积大 5-10 倍对存储和性能都不友好。除非有特殊需求如透明背景否则用 JPEG。注意 ImageReceiver 的生命周期ImageReceiver在aboutToDisappear中必须释放否则会残留管道资源。image.receiver.release()记得调用。不要在主线程等待图片写入saveToGallery中的文件读写操作是异步的但如果用await可能会阻塞 UI 线程。建议将写入逻辑放到独立的TaskPool或Worker中执行。如果你也遇到过拍照后图片无法保存、闪退或权限问题可以重点检查ImageReceiver的释放、capacity设置和notifyChange是否调用。官方文档对拍照回调的描述比较简略实际项目中这些细节比 API 本身更重要。