HarmonyOS技术精讲-Camera Kit(相机服务)第20篇:性能优化与常见问题

HarmonyOS技术精讲-Camera Kit(相机服务)第20篇:性能优化与常见问题
在HarmonyOS NEXT的Camera Kit开发中,很多人会把官方示例直接搬进项目,结果发现预览界面掉帧、内存暴增、权限回调不生效、设备兼容性翻车。这些问题不是API本身的问题,而是大部分人没处理好会话生命周期和资源释放的细节。
今天这篇不讲“什么是Camera Kit”,直接进入正题:帧率优化、内存泄漏、会话复用、权限回调、设备兼容性。每个问题都会给优化前后的代码对比,代码基于ArkTS,可以在真机上直接跑。
环境说明
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机
核心问题与优化
1. 预览帧率不够?别只盯着帧率设置
很多人一上来就设 CameraOutputCapability 的帧率,发现预览依然卡顿。问题通常出在两个方面:一是 ImageReceiver 的接收频率没匹配显示器刷新率;二是会话创建时没合理复用资源。
优化前代码:
// 每次打开相机都重新创建会话,并且不限制预览帧率
import { camera } from '@kit.CameraKit';
import { image } from '@kit.ImageKit';
class CameraService {
private cameraManager: camera.CameraManager | null = null;
private cameraInput: camera.CameraInput | null = null;
private previewOutput: camera.PreviewOutput | null = null;
private session: camera.CaptureSession | null = null;
async initCamera(context: Context) {
this.cameraManager = camera.getCameraManager(context);
let cameraInfo = await this.cameraManager.getSupportedCameras();
if (cameraInfo.length === 0) {
return;
}
this.cameraInput = this.cameraManager.createCameraInput(cameraInfo[0]);
await this.cameraInput.open();
this.session = this.cameraManager.createCaptureSession();
await this.session.beginConfig();
// 问题:没有限制帧率
let outputCapability = this.session.getCameraOutputCapabilities(cameraInfo[0]);
let previewProfiles = outputCapability.getSupportedPreviewOutputs();
this.previewOutput = this.session.createPreviewOutput(previewProfiles[0]);
await this.session.addInput(this.cameraInput);
await this.session.addOutput(this.previewOutput);
await this.session.commitConfig();
await this.session.start();
}
}
这段代码的问题:每次初始化都 createCaptureSession,且没有显式控制帧率,导致在高分辨率预览时掉帧明显。
优化后代码:
// 复用会话实例,并通过PreviewOutputProfile限制帧率
import { camera } from '@kit.CameraKit';
import { image } from '@kit.ImageKit';
class CameraService {
private cameraManager: camera.CameraManager | null = null;
private cameraInput: camera.CameraInput | null = null;
private previewOutput: camera.PreviewOutput | null = null;
private session: camera.CaptureSession | null = null;
private sessionInited: boolean = false;
async initCamera(context: Context) {
this.cameraManager = camera.getCameraManager(context);
let cameraInfo = await this.cameraManager.getSupportedCameras();
if (cameraInfo.length === 0) {
return;
}
// 关键点:复用会话,避免重复创建
if (!this.sessionInited) {
this.session = this.cameraManager.createCaptureSession();
this.sessionInited = true;
}
// 如果旧Input已打开,先关闭
if (this.cameraInput) {
await this.cameraInput.close();
}
this.cameraInput = this.cameraManager.createCameraInput(cameraInfo[0]);
await this.cameraInput.open();
await this.session.beginConfig();
// 限制帧率为30fps,避免过高丢帧
let outputCapability = this.session.getCameraOutputCapabilities(cameraInfo[0]);
let previewProfiles = outputCapability.getSupportedPreviewOutputs().filter(
(profile) => (profile as camera.PreviewOutputProfile).frameRateRange.min === 30
);
if (previewProfiles.length === 0) {
// 如果没有30fps配置,直接取第一个
this.previewOutput = this.session.createPreviewOutput(previewProfiles[0]);
} else {
this.previewOutput = this.session.createPreviewOutput(previewProfiles[0]);
}
await this.session.addInput(this.cameraInput);
await this.session.addOutput(this.previewOutput);
await this.session.commitConfig();
await this.session.start();
}
}
注意点:getSupportedPreviewOutputs 返回的是 PreviewOutputProfile[],其中 frameRateRange 是一个对象,包含 min 和 max 属性。这里统一限制到 min 为30,大部分设备都能稳定在这个帧率。
2. 内存泄漏?问题出在会话释放和回调解绑
这是官方文档最容易忽略的地方:页面销毁时,必须显式释放 CaptureSession、CameraInput、PreviewOutput,并且解绑所有的回调监听。否则,后台线程会继续持有这些对象,导致内存泄漏。
优化前代码:
// 只释放session,其他资源不管
releaseCamera() {
if (this.session) {
this.session.close();
this.session = null;
}
}
这种写法下,CameraInput 和 PreviewOutput 依然持有内部线程,GC无法回收。
优化后代码:
// 完整释放所有相机资源,并解绑回调
releaseCamera() {
if (this.previewOutput) {
// 解绑所有回调,否则异步回调可能引用已释放的UI上下文
this.previewOutput.off('previewOutputStart');
this.previewOutput.off('previewOutputError');
this.previewOutput.close();
this.previewOutput = null;
}
if (this.cameraInput) {
this.cameraInput.off('cameraInputStart');
this.cameraInput.off('cameraInputError');
this.cameraInput.close();
this.cameraInput = null;
}
if (this.session) {
// 先停止再关闭,避免残留任务
this.session.stop();
this.session.close();
this.session = null;
}
this.cameraManager = null;
this.sessionInited = false;
}
这里特别说明:off('previewOutputStart') 等回调解绑不能省。因为 PreviewOutput 在子线程中上报事件,如果回调里引用了UI组件(比如更新 @State 变量),而页面已经销毁,ArkUI的响应式系统会抛出运行时错误,并且导致对象无法完全释放。
3. 权限回调不响应?顺序和生命周期没对齐
很多人写权限申请回调时,直接写在 onPageShow 里,结果相机初始化完成时权限还没返回。或者权限回调没绑定 UIAbilityContext,导致回调函数无法正确执行。
优化前代码:
// 权限申请与相机初始化分离,可能导致时序问题
import { abilityAccessCtrl, common } from '@kit.AbilityKit';
async function requestAndInitCamera(context: common.UIAbilityContext) {
let atManager = abilityAccessCtrl.createAtManager();
// 权限回调可能晚于相机初始化
atManager.requestPermissionsFromUser(context, ['ohos.permission.CAMERA']).then((data) => {
if (data.authResults[0] === 0) {
// 这里才初始化相机
}
});
// 但下面的代码会立刻执行,导致相机初始化在没有权限时失败
let cameraManager = camera.getCameraManager(context);
}
优化后代码:
// 权限申请完成后,再初始化相机
import { abilityAccessCtrl, common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
class CameraService {
async requestAndInitCamera(context: common.UIAbilityContext): Promise<void> {
let atManager = abilityAccessCtrl.createAtManager();
try {
let data = await atManager.requestPermissionsFromUser(context, ['ohos.permission.CAMERA']);
if (data.authResults[0] !== 0) {
console.error('Camera permission denied');
return;
}
// 权限确认后再初始化
await this.initCamera(context);
} catch (error) {
let bizError = error as BusinessError;
console.error(`Camera permission request failed: ${bizError.message}`);
}
}
}
注意:requestPermissionsFromUser 返回的是一个 Promise,使用 await 可以保证顺序。但如果用户拒绝权限,authResults 值不为0,此时直接返回,不会执行后续的相机逻辑。
4. 设备兼容性问题:不同机型的Session支持不同
某些低端机型不支持某些分辨率或帧率配置,直接使用 getSupportedPreviewOutputs() 返回的第一个Profile,会导致崩溃或预览黑屏。
优化后代码:
// 安全获取第一个可用Profile,并降级处理
async initCamera(context: Context) {
this.cameraManager = camera.getCameraManager(context);
let cameraInfo = await this.cameraManager.getSupportedCameras();
if (cameraInfo.length === 0) {
return;
}
// 先尝试使用第一个相机
let cameraId = cameraInfo[0];
this.cameraInput = this.cameraManager.createCameraInput(cameraId);
await this.cameraInput.open();
this.session = this.cameraManager.createCaptureSession();
await this.session.beginConfig();
let outputCapability = this.session.getCameraOutputCapabilities(cameraId);
let previewProfiles = outputCapability.getSupportedPreviewOutputs();
// 关键:如果设备不支持MJPEG编码,它会返回空数组,此时需要降级
if (previewProfiles.length === 0) {
console.warn('No supported preview profiles, using default config');
// 这里可以尝试手动创建默认Profile,或提示设备不支持
return;
}
// 优先选择1280x720,大多数设备都支持
let targetProfile = previewProfiles.find(
(profile) => {
return profile.size.width === 1280 && profile.size.height === 720;
}
) || previewProfiles[0];
this.previewOutput = this.session.createPreviewOutput(targetProfile);
await this.session.addInput(this.cameraInput);
await this.session.addOutput(this.previewOutput);
await this.session.commitConfig();
await this.session.start();
}
常见问题FAQ
Q:为什么有些设备上预览是黑的,但日志没有报错?
A:预览黑屏最常见的原因是 PreviewOutput 没有正确绑定到 ImageReceiver。检查 ImageReceiver 是否在创建 PreviewOutput 之前初始化,并且 ImageReceiver 的尺寸与 PreviewOutputProfile 的尺寸一致。此外,某些低端设备不支持 SURFACE_FORMAT_YCBCR_422_I,需要改为 SURFACE_FORMAT_RGBA_8888。
Q:为什么页面返回后再进入,相机黑屏或报错“session already started”?
A:这是因为页面 aboutToDisappear 没有调用 releaseCamera()。当再次进入页面时,initCamera 试图创建一个新的 CaptureSession,但旧的会话可能没有被彻底关闭。解决方案:在 aboutToDisappear 里调用完整释放方法,并在 aboutToAppear 里重新 initCamera。
Q:为什么权限回调里 authResults 总是返回-1?
A:常见原因是 requestPermissionsFromUser 的 context 参数不是正确的 UIAbilityContext。在组件中,可以通过 getContext() 获取,确保它是一个 common.UIAbilityContext 实例。另一种情况是用户在系统设置中已经永久拒绝了相机权限,此时 requestPermissionsFromUser 不会再弹出弹窗,而是直接返回拒绝结果。如果权限被永久拒绝,需要通过 abilityAccessCtrl 检查权限状态并引导用户去设置页面开启。
最佳实践
- 不要在build()中创建CameraManager。 每次build都会触发组件重建,而CameraManager的创建涉及底层服务初始化,非常昂贵。建议放在
aboutToAppear或@State变量变化的回调中,且只创建一次。 - Session复用:如果应用需要频繁切换摄像头(比如前后摄切换),不要每次重新创建
CaptureSession。 使用beginConfig()、removeInput、removeOutput、addInput、addOutput、commitConfig()的方式,可以避免资源泄露和性能开销。 - 所有回调都使用lambda表达式绑定当前组件实例,并在释放时解绑。 这样可以避免回调引用过期对象导致的ArkUI报错“Component has been destroyed”。
更多推荐

所有评论(0)