OpenHarmony 原生 Camera 相机拍摄实现(API Version23 + 适配版) 摘要Camera 模块为系统原生相机能力支持实时预览、拍照保存图片、录像等功能适用于扫码、头像拍摄、单据上传等业务场景。API Version23 重构相机生命周期、设备释放、帧数据回调、存储写入逻辑修复低版本相机占用无法释放、多次打开黑屏、拍摄图片保存失败、切换前后摄像头崩溃、后台驻留相机耗电等兼容缺陷。旧项目升级 API23 后常出现页面退出相机未释放导致下次打开黑屏、拍摄无文件、预览画面拉伸变形、无相机权限直接闪退等问题。本文基于 DevEco Studio 适配 OpenHarmony API23 及以上版本封装相机通用工具类实现实时预览、拍照保存至沙盒、前后摄像头切换三大核心功能搭配拍摄页面完整实战代码输出权限校验、设备资源释放、图片持久化规范汇总版本升级兼容故障解决方案为鸿蒙相机拍摄业务开发提供标准化实操模板。关键词OpenHarmonyArkUIAPI Version23camera相机预览拍照媒体存储资源释放一、引言1.1 相机开发背景头像拍摄、证件上传、扫码识别、视频录制等场景都需要调用硬件相机。系统 camera 模块直接操作硬件摄像头相比相册选择能实时取景拍摄交互更贴合拍照类业务。相机属于独占硬件资源页面销毁必须主动释放设备否则会持续占用硬件导致其他应用无法调用相机。 OpenHarmony API Version23 针对 camera 底层硬件调度引擎完成全面升级核心变更点标准化 CameraDevice 完整生命周期创建→打开预览→拍摄→关闭释放缺失步骤直接报错新增硬件占用强回收机制页面销毁未释放相机则系统强制断开硬件统一拍摄图片沙盒写入逻辑修复图片丢失、路径无效、无写入权限问题重构 Surface 预览绑定规则修复预览画面拉伸、黑屏、画面倒置强化 CAMERA 动态权限强校验未授权调用直接抛出可捕获异常区分前后摄像头设备 ID切换摄像头自动销毁重建预览流无画面卡顿。大量 API9~11 旧项目升级后二次进入相机页面黑屏、拍摄图片找不到、切换摄像头闪退根源是未完整释放 CameraDevice 硬件资源因此掌握 API23 标准相机生命周期与资源释放规范是拍摄类功能必备技能。1.2 开发环境与测试场景开发工具DevEco Studio 5.0 及以上 适配系统OpenHarmony API Version23、HarmonyOS NEXT 导入模块ohos.multimedia.camera、ohos.file.fs、ohos.file.common 前置依赖动态 CAMERA 权限、存储写入权限 测试场景实时相机预览、拍摄照片保存本地、前后摄像头切换、页面退出释放相机、无权限拦截提示二、API23 camera 相机核心 API 与版本变更说明2.1 核心对象生命周期顺序CameraManager相机管理器获取设备列表CameraDevice摄像头硬件实例核心独占资源用完必须 closePreviewOutput预览输出流绑定 XComponent 渲染画面CaptureOutput拍照输出流处理拍摄图片保存2.2 关键核心方法etsimport camera from ohos.multimedia.camera // 获取相机管理器 const camMgr camera.getCameraManager(getContext(this)) // 获取全部摄像头设备ID列表 const deviceIds camMgr.getSupportedCameraDeviceIds() // 打开摄像头硬件 const cameraDev await camMgr.openCamera(deviceId) // 创建预览输出 const preview await camMgr.createPreviewOutput(surfaceId, cameraDev) // 创建拍照输出 const capture await camMgr.createCaptureOutput(fileFd, cameraDev) // 开始预览 preview.start() // 执行拍照 await capture.capture() // 停止预览、关闭硬件必须执行 preview.stop() cameraDev.close()2.3 XComponent 预览容器相机画面依赖 XComponent 组件承载通过 onSurfaceCreated 获取 surfaceId 绑定预览流。2.4 API23 废弃与强制约束废弃页面销毁不执行 cameraDev.close () 写法硬件长期占用造成黑屏禁止多页面同时持有 CameraDevice 实例同一时间仅允许一个页面使用相机拍摄图片必须传入文件文件描述符 fd不支持直接传入绝对路径必须前置动态申请 ohos.permission.CAMERA 权限无权限直接捕获异常切换摄像头必须先关闭当前 CameraDevice再重新创建新设备实例后台切页必须停止预览并关闭相机避免持续耗电、硬件占用。三、API23 标准基础示例代码3.1 基础相机打开与预览流程etsimport camera from ohos.multimedia.camera import common from ohos.app.ability.common let cameraDev: camera.CameraDevice | null null let previewOut: camera.PreviewOutput | null null async function openCamera(ctx: common.UIAbilityContext, surfaceId: string, deviceId: string) { const camMgr camera.getCameraManager(ctx) // 打开硬件摄像头 cameraDev await camMgr.openCamera(deviceId) // 创建预览输出绑定XComponent画布 previewOut await camMgr.createPreviewOutput(surfaceId, cameraDev) // 启动预览画面 await previewOut.start() } // 释放全部相机资源页面退出必调用 async function releaseCamera() { if (previewOut) { await previewOut.stop() previewOut null } if (cameraDev) { await cameraDev.close() cameraDev null } }3.2 基础拍照并保存沙盒文件etsimport fs from ohos.file.fs import camera from ohos.multimedia.camera async function takePhoto(ctx: common.UIAbilityContext) { const camMgr camera.getCameraManager(ctx) // 创建沙盒图片文件 const filePath ctx.filesDir /photo_ Date.now() .jpg const fd fs.openSync(filePath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY) // 创建拍照输出流 const captureOut await camMgr.createCaptureOutput(fd, cameraDev!) // 执行拍摄 await captureOut.capture() // 关闭文件描述符 fs.closeSync(fd) return filePath }四、完整实战案例相机工具类 拍摄页面4.1 实战一相机工具封装 utils/camera_util.etsetsimport camera from ohos.multimedia.camera import fs from ohos.file.fs import common from ohos.app.ability.common import promptAction from ohos.promptAction class CameraUtil { private static instance: CameraUtil private ctx: common.UIAbilityContext | null null private cameraDev: camera.CameraDevice | null null private previewOut: camera.PreviewOutput | null null private curDeviceId: string static getInstance(): CameraUtil { if (!CameraUtil.instance) { CameraUtil.instance new CameraUtil() } return CameraUtil.instance } setContext(context: common.UIAbilityContext) { this.ctx context } // 获取前后摄像头ID0后置1前置 getCameraIds(): string[] { if (!this.ctx) return [] const mgr camera.getCameraManager(this.ctx) return mgr.getSupportedCameraDeviceIds() } // 初始化相机预览 async initCamera(surfaceId: string, deviceId: string): Promiseboolean { if (!this.ctx) return false // 先释放已有相机 await this.release() try { const mgr camera.getCameraManager(this.ctx) this.curDeviceId deviceId this.cameraDev await mgr.openCamera(deviceId) this.previewOut await mgr.createPreviewOutput(surfaceId, this.cameraDev) await this.previewOut.start() return true } catch (err) { promptAction.showToast({ message: 相机打开失败请检查权限 }) console.error(initCamera error, err) return false } } // 执行拍照返回图片沙盒路径 async takePicture(): Promisestring { if (!this.ctx || !this.cameraDev) return try { const mgr camera.getCameraManager(this.ctx) const fileName cam_ Date.now() .jpg const fullPath this.ctx.filesDir / fileName const fd fs.openSync(fullPath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY) const captureOut await mgr.createCaptureOutput(fd, this.cameraDev) await captureOut.capture() fs.closeSync(fd) promptAction.showToast({ message: 拍摄完成 }) return fullPath } catch (err) { promptAction.showToast({ message: 拍摄失败 }) console.error(takePicture error, err) return } } // 释放相机全部硬件资源页面销毁强制调用 async release() { if (this.previewOut) { try { await this.previewOut.stop() } catch (e) {} this.previewOut null } if (this.cameraDev) { try { await this.cameraDev.close() } catch (e) {} this.cameraDev null } } } export default CameraUtil.getInstance()4.2 实战二相机拍摄页面XComponent 预览 切换摄像头 拍照etsimport CameraUtil from ../utils/camera_util import PermissionUtil, { PERMISSION } from ../utils/permission_util import ImagePickerUtil from ../utils/image_picker Entry Component struct CameraPage { State surfaceId: string State photoPath: ResourceStr deviceList: string[] [] State currentIndex: number 0 async aboutToAppear() { // 绑定上下文并校验相机权限 PermissionUtil.setContext(getContext(this)) const hasCamPerm await PermissionUtil.requestSingle(PERMISSION.CAMERA, 相机) if (!hasCamPerm) return CameraUtil.setContext(getContext(this)) this.deviceList CameraUtil.getCameraIds() } // XComponent画布创建完成初始化相机预览 onSurfaceReady(surface: string) { this.surfaceId surface if (this.deviceList.length 0) { CameraUtil.initCamera(this.surfaceId, this.deviceList[this.currentIndex]) } } // 切换前后摄像头 async switchCamera() { this.currentIndex this.currentIndex 0 ? 1 : 0 await CameraUtil.initCamera(this.surfaceId, this.deviceList[this.currentIndex]) } // 执行拍照 async shotPhoto() { const path await CameraUtil.takePicture() if (path) this.photoPath path } build() { Column() { // 相机预览画布 XComponent() .width(100%) .layoutWeight(1) .onSurfaceCreated((surface) this.onSurfaceReady(surface)) // 底部操作按钮栏 Row({ space: 30 }) { Button(切换摄像头).width(140).height(48).onClick(() this.switchCamera()) Button(拍照).width(140).height(48).backgroundColor(#f56c6c).onClick(() this.shotPhoto()) } .width(100%) .height(80) .justifyContent(FlexAlign.Center) // 拍摄预览缩略图 if (this.photoPath) { Image(this.photoPath) .width(80) .height(80) .position({ x: 10, y: -90 }) .borderRadius(8) } } .width(100%) .height(100%) } async aboutToDisappear() { // 页面退出强制释放相机硬件资源API23硬性要求 await CameraUtil.release() } }五、API23 相机适配与硬件性能优化规范5.1 硬件资源释放强制规范页面aboutToDisappear必须调用工具 release () 停止预览、关闭 CameraDevice切换摄像头先关闭当前设备再创建新预览流避免硬件冲突应用切后台 onBackground 主动释放相机前台返回再重新初始化。5.2 权限与存储规范打开相机前动态申请 CAMERA 权限无权限阻断相机初始化流程拍摄图片保存至应用沙盒 filesDir 目录无需额外存储权限拍照文件必须使用 fs 文件描述符 fd 传入 CaptureOutput禁止直接传路径。5.3 预览画面适配规范XComponent 铺满页面宽高避免画面拉伸、黑边设备 ID 0 默认后置主摄1 前置自拍自动区分设备列表初始化失败捕获异常弹窗提示不直接闪退应用。5.4 性能耗电规范长时间停留相机页面不频繁重复创建 / 销毁预览页面退出、切后台立刻关闭相机硬件停止图像解码节省电量不循环连续调用拍照增加按钮防抖锁防止重复拍摄。六、API23 升级高频兼容问题与解决方案问题 1第一次打开相机正常返回再次进入页面黑屏无画面 解决页面销毁执行 release 完整关闭 CameraDevice 硬件下次进入重新初始化。问题 2点击拍照无图片沙盒目录找不到照片文件 解决使用 fs.openSync 获取文件描述符 fd 传入拍摄输出不能直接传文件路径字符串。问题 3切换前后摄像头画面卡顿、直接闪退 解决切换前先释放当前预览与摄像头设备再新建预览流。问题 4无相机权限进入页面直接崩溃 解决页面加载前置动态权限校验未授予权限直接阻断相机初始化逻辑。问题 5切后台后再切回相机画面冻结不动 解决onBackground 释放相机onForeground 重新调用 initCamera 重建预览。问题 6多次打开相机后系统提示相机被其他应用占用 解决保证所有退出分支都执行 release无遗漏硬件关闭逻辑。七、总结camera 原生相机模块是鸿蒙硬件拍摄标准实现方案API Version23 严格标准化摄像头硬件生命周期强制要求页面销毁释放 CameraDevice 硬件资源解决旧版硬件长期占用、黑屏、拍摄图片丢失、切换摄像头崩溃等核心问题。项目封装独立相机工具类统一管理预览、拍照、设备释放逻辑搭配 XComponent 预览画布、动态权限校验完整实现前后摄像头切换、拍照保存本地核心业务。