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

在这里插入图片描述

这个机制解决什么问题

相机服务在实际开发中相较于其他多媒体能力有一个明显区别:它是一个独占式硬件资源。屏幕旋转、应用切换、横竖屏变化、系统来电等场景会导致相机资源被系统回收,此时如果应用没有正确处理恢复逻辑,用户会看到一个黑屏或直接闪退。

官方文档把Camera Kit的恢复机制描述得比较简略,只提到了onBackgroundonForeground回调。但在实际项目中,这两个回调中间的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%')
  }
}

关键决策

  • 为什么不直接用onPageShowonPageHide?因为页面回退栈管理下这两个回调不够精准,叠加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”

现象commitConfigstart时抛出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在老设备上有权限预热策略限制,应用被回收后需要重新请求权限。

解决方案:在aboutToAppearhandleResume中重新检查相机权限。

private async ensureCameraPermission(): Promise<boolean> {
  const permissionResult = await checkAccessToken('ohos.permission.CAMERA')
  if (permissionResult !== 1) {
    // 提示用户授权
    return false
  }
  return true
}

最佳实践

  1. 不要在onForeground中直接重建相机会话。系统可能在同一时刻触发多次WINDOW_SHOW事件,每次重建都会释放旧资源创建新资源,频繁操作会导致相机服务无响应。建议加一个防抖机制,或者用background标志位限制只执行一次恢复。

  2. 进入后台时不要释放CameraManager。CameraManager实例可以跨生命周期复用。完全释放后,恢复时需要重新建立与系统相机服务的连接,增加100-300ms的额外延迟。只释放Session和Output即可。

  3. 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:需要。应用被系统回收后,权限授权状态可能被重置。建议在aboutToAppearhandleResume中都调用checkAccessToken。不过如果只是切后台不销毁页面,权限不会丢失。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐