HarmonyOS技术精讲-Camera Kit(相机服务)第13篇:相机启动与恢复机制
HarmonyOS技术精讲-Camera Kit(相机服务)第13篇:相机启动与恢复机制

这个机制解决什么问题
相机服务在实际开发中相较于其他多媒体能力有一个明显区别:它是一个独占式硬件资源。屏幕旋转、应用切换、横竖屏变化、系统来电等场景会导致相机资源被系统回收,此时如果应用没有正确处理恢复逻辑,用户会看到一个黑屏或直接闪退。
官方文档把Camera Kit的恢复机制描述得比较简略,只提到了onBackground和onForeground回调。但在实际项目中,这两个回调中间的Session状态管理才是最麻烦的部分——因为Camera Kit在后台时,系统可能会做以下几件事:
- 释放相机资源(CameraManager被回收)
- 断开会话连接(
createCaptureSession返回新的Session对象) - 重置所有配置(滤镜、闪光灯、缩放比全部清空)
所以这个机制的核心目标只有一个:当应用从后台切回前台时,让相机恢复到暂停前的状态。
环境说明
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机、平板
核心实现:相机生命周期管理
第一步:定义相机状态管理类
需要维护一个全局状态,记录相机当前处于哪个阶段。不能每次恢复都重新初始化全部对象,那样资源开销太大。
// CameraStateManager.ets
import { camera } from '@kit.CameraKit';
import { BusinessError } from '@kit.BasicServicesKit';
export enum CameraState {
INITIALIZED = 'initialized', // Camera对象创建完成
SESSION_CREATED = 'session_created', // 会话已创建
PREVIEW_STARTED = 'preview_started', // 预览已启动
BACKGROUND = 'background' // 应用进入后台,资源已释放
}
export class CameraStateManager {
private cameraState: CameraState = CameraState.INITIALIZED;
private cameraManager: camera.CameraManager | null = null;
private cameraInput: camera.CameraInput | null = null;
private captureSession: camera.CaptureSession | null = null;
private previewOutput: camera.PreviewOutput | null = null;
private lastCameraId: string = '';
private isInBackground: boolean = false;
get state(): CameraState {
return this.cameraState;
}
setState(state: CameraState): void {
this.cameraState = state;
}
get manager(): camera.CameraManager | null {
return this.cameraManager;
}
set manager(value: camera.CameraManager | null) {
this.cameraManager = value;
}
get input(): camera.CameraInput | null {
return this.cameraInput;
}
set input(value: camera.CameraInput | null) {
this.cameraInput = value;
}
get session(): camera.CaptureSession | null {
return this.captureSession;
}
set session(value: camera.CaptureSession | null) {
this.captureSession = value;
}
get preview(): camera.PreviewOutput | null {
return this.previewOutput;
}
set preview(value: camera.PreviewOutput | null) {
this.previewOutput = value;
}
get cameraId(): string {
return this.lastCameraId;
}
set cameraId(value: string) {
this.lastCameraId = value;
}
get background(): boolean {
return this.isInBackground;
}
set background(value: boolean) {
this.isInBackground = value;
}
// 释放所有资源
releaseAllResources(): void {
this.previewOutput?.release();
this.cameraInput?.release();
this.captureSession?.release();
this.cameraManager = null;
this.captureSession = null;
this.cameraInput = null;
this.previewOutput = null;
this.state = CameraState.INITIALIZED;
}
// 释放Session(保留CameraManager和Input)
releaseSessionOnly(): void {
if (this.captureSession) {
this.captureSession.release();
this.captureSession = null;
}
this.state = CameraState.INITIALIZED;
}
}
设计要点:
- 用枚举明确状态流转路径,避免通过布尔变量组合推导状态
releaseSessionOnly()是专门为进入后台准备的轻量级释放方案,保留CameraManager和CameraInput可以避免重新初始化硬件设备isInBackground标志位用于判断是否需要完整恢复还是部分恢复
第二步:在Page生命周期中处理暂停与恢复
// CameraPage.ets
import { camera } from '@kit.CameraKit';
import { image } from '@kit.ImageKit';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
import { CameraStateManager, CameraState } from './CameraStateManager';
@Entry
@Component
struct CameraPage {
@State previewWidth: number = 0;
@State previewHeight: number = 0;
private cameraStateMgr: CameraStateManager = new CameraStateManager();
private surfaceId: string = '';
aboutToAppear(): void {
// 注册窗口事件监听,确保能捕获系统级暂停和恢复
const win = window.getLastWindow(getContext());
win.then(windowInfo => {
windowInfo.on('windowEvent', (eventName: window.WindowEventType) => {
if (eventName === window.WindowEventType.WINDOW_HIDE) {
this.handlePause()
} else if (eventName === window.WindowEventType.WINDOW_SHOW) {
this.handleResume()
}
})
})
}
onPageShow(): void {
// 页面从后台显示时,尝试恢复相机
if (this.cameraStateMgr.background) {
this.handleResume()
}
}
onPageHide(): void {
// 页面隐藏时,暂停相机
this.handlePause()
}
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.on('windowEvent', (eventName: window.WindowEventType) => {
if (eventName === window.WindowEventType.WINDOW_HIDE) {
this.handlePause()
} else if (eventName === window.WindowEventType.WINDOW_SHOW) {
this.handleResume()
}
})
}
aboutToDisappear(): void {
// 页面销毁时释放所有资源,包括CameraManager
this.cameraStateMgr.releaseAllResources()
}
// 暂停相机(进入后台)
private handlePause(): void {
if (this.cameraStateMgr.state === CameraState.PREVIEW_STARTED) {
console.info('[CameraLifecycle] 暂停相机资源')
// 停止预览
this.cameraStateMgr.preview?.release()
// 释放Session,但保留CameraManager和CameraInput
this.cameraStateMgr.releaseSessionOnly()
this.cameraStateMgr.background = true
this.cameraStateMgr.setState(CameraState.BACKGROUND)
}
}
// 恢复相机(切回前台)
private handleResume(): void {
if (!this.cameraStateMgr.background) {
return
}
console.info('[CameraLifecycle] 恢复相机资源')
this.cameraStateMgr.background = false
this.rebuildCameraSession()
}
// 重建相机会话
private async rebuildCameraSession(): Promise<void> {
try {
// 1. 获取CameraManager
if (!this.cameraStateMgr.manager) {
this.cameraStateMgr.manager = camera.getCameraManager(getContext())
}
// 2. 获取CameraInput(保留之前选中的相机ID)
if (!this.cameraStateMgr.input) {
const camIds = this.cameraStateMgr.manager.getSupportedCameras()
if (camIds.length === 0) {
throw new Error('未找到可用摄像头')
}
const targetCamId = this.cameraStateMgr.cameraId || camIds[0]
this.cameraStateMgr.cameraInput = this.cameraStateMgr.manager.createCameraInput(targetCamId)
this.cameraStateMgr.input.open()
this.cameraStateMgr.cameraId = targetCamId
}
// 3. 创建新的CaptureSession(系统回收后必须重新创建)
this.cameraStateMgr.session = this.cameraStateMgr.manager.createCaptureSession()
// 4. 创建PreviewOutput
this.cameraStateMgr.preview = this.cameraStateMgr.manager.createPreviewOutput(this.surfaceId)
// 5. 开始会话
this.cameraStateMgr.session.beginConfig()
this.cameraStateMgr.session.addInput(this.cameraStateMgr.input)
this.cameraStateMgr.session.addOutput(this.cameraStateMgr.preview)
await this.cameraStateMgr.session.commitConfig()
await this.cameraStateMgr.session.start()
this.cameraStateMgr.setState(CameraState.PREVIEW_STARTED)
console.info('[CameraLifecycle] 相机恢复成功')
} catch (error) {
let err = error as BusinessError
console.error(`[CameraLifecycle] 恢复相机失败: ${err.code} ${err.message}`)
}
}
build() {
Stack() {
if (this.previewWidth > 0 && this.previewHeight > 0) {
XComponent({
id: 'cameraPreview',
type: XComponentType.SURFACE,
width: this.previewWidth,
height: this.previewHeight
})
}
}
.width('100%')
.height('100%')
}
}
关键决策:
- 为什么不直接用
onPageShow和onPageHide?因为页面回退栈管理下这两个回调不够精准,叠加window.WindowEventType可以覆盖更多场景 - 恢复时先判断
background标志位,避免在非后台场景下重复初始化 rebuildCameraSession必须创建新的CaptureSession对象,这是Camera Kit的内部机制——系统回收后旧Session句柄无效
常见问题
问题1:恢复后预览黑屏
现象:应用从后台切回前台,无异常抛出,但预览区域为黑屏。
原因:最常见的是XComponent的Surface ID在页面隐藏期间被系统回收。createPreviewOutput时传入的surfaceId已经失效。
解决方案:
- 在
rebuildCameraSession之前获取最新的Surface ID - 在
build()中不为XComponent绑定id时设置一个稳定ID常量,避免重建时改变ID - 监听
XComponent.onDestroy事件,Surface销毁时主动释放PreviewOutput
// 在rebuildCameraSession中
if (!this.surfaceId) {
const xcom = this.findChildById('cameraPreview') as XComponent
this.surfaceId = xcom.getXComponentSurfaceId()
}
问题2:恢复后报错“Service busy”
现象:commitConfig或start时抛出13900001错误。
原因:相机服务在后台释放资源有延迟。应用切回前台过快,Camera Manager尚未完成内部状态重置。
解决方案:添加一个200ms的延迟重试逻辑,但不要盲目加长超时,避免用户感知到卡顿。
private async rebuildWithRetry(maxRetries: number = 3): Promise<void> {
for (let i = 0; i < maxRetries; i++) {
try {
await this.rebuildCameraSession()
return
} catch (error) {
if (i === maxRetries - 1) {
throw error
}
await new Promise(resolve => setTimeout(resolve, 200 + i * 100))
}
}
}
问题3:荣耀/华为部分老设备上恢复后闪退
现象:仅在部分设备的API 12以下版本出现,camera.getCameraManager()返回null。
原因:Camera Kit在老设备上有权限预热策略限制,应用被回收后需要重新请求权限。
解决方案:在aboutToAppear和handleResume中重新检查相机权限。
private async ensureCameraPermission(): Promise<boolean> {
const permissionResult = await checkAccessToken('ohos.permission.CAMERA')
if (permissionResult !== 1) {
// 提示用户授权
return false
}
return true
}
最佳实践
-
不要在
onForeground中直接重建相机会话。系统可能在同一时刻触发多次WINDOW_SHOW事件,每次重建都会释放旧资源创建新资源,频繁操作会导致相机服务无响应。建议加一个防抖机制,或者用background标志位限制只执行一次恢复。 -
进入后台时不要释放CameraManager。CameraManager实例可以跨生命周期复用。完全释放后,恢复时需要重新建立与系统相机服务的连接,增加100-300ms的额外延迟。只释放Session和Output即可。
-
Surface ID的获取时机必须按以下顺序:
- 初始创建时在
onPlace回调中获取 - 恢复时重新获取
- 不要在
build()中每次计算Surface ID,这会导致预览区域闪烁
- 初始创建时在
XComponent({ id: 'cameraPreview', type: XComponentType.SURFACE })
.onLoad((xcom) => {
this.surfaceId = xcom.getXComponentSurfaceId()
})
FAQ
Q:主页面恢复相机成功,但再次切换到后台后恢复失败?
A:检查handlePause中是否释放了CameraInput。如果释放了CameraInput,恢复时必须重新open()。建议只释放Session和Output,保留Input。
Q:为什么返回按钮退出页面后,状态栏还显示相机图标?
A:aboutToDisappear中调用了releaseAllResources,但release()是异步操作,需要等待Promise完成。建议使用await或者Promise.all等待所有资源释放完成。
Q:权限授权逻辑需要重新走一遍吗?
A:需要。应用被系统回收后,权限授权状态可能被重置。建议在aboutToAppear和handleResume中都调用checkAccessToken。不过如果只是切后台不销毁页面,权限不会丢失。
更多推荐



所有评论(0)