39 识别性能优化与内存管理

引言

"拍照识别文字"的体验好不好,一半看识别准不准,一半看快不快、稳不稳——尤其是连续拍照时,卡顿和内存暴涨会直接毁掉体验。OCR 是典型的计算密集 + 内存密集任务:一张高像素照片要解码成 RGBA 像素图、送入识别引擎,结果动辄几十毫秒到几百毫秒,解码出的 PixelMap 又是内存大户。本文结合工程 Camera.etsrecognizeImage 的真实代码,分析性能瓶颈来源,给出缩小输入、手动释放、避免重复创建、异步化四条优化主线,并梳理"拍照 → 识别"的时序,附一份可直接落地的优化清单。

正文知识点

1. 性能瓶颈在哪

一次识别的时间大致由四段组成:

  • 图片解码image.createImageSource(buffer) + createPixelMap() 把 JPEG 解成像素图,图越大越慢、内存越高;
  • 朝向检测isDirectionDetectionSupported: true 时先判断文字方向再旋转,是纯额外开销(详见本系列第 36 篇);
  • 引擎识别:NPU/CPU 推理,与图像尺寸强相关;
  • 结果序列化:生成 valueblocks 等结构。

输入图的尺寸是最大变量:官方建议 720p 以上即可保证识别质量,过高分辨率(如 4000×3000 原图)对精度提升有限,却线性放大解码、推理、内存三笔开销。所以第一条优化就是控制输入尺寸

2. 缩小图片:createPixelMap 时指定尺寸

image.ImageSource.createPixelMap 支持传入 InitializationOptions,其中 desiredSize 可以直接在解码阶段把像素图缩到目标分辨率,比"先解大图再缩放"省一次内存峰值。注意 VisionInfo.pixelMap 要求颜色格式为 RGBA_8888,缩放时不要顺手改成 RGB_565,否则会参数校验失败。

3. 手动释放:release() 是硬性要求

工程中的释放代码是教科书式示范(见下方代码示例)。imageResourceImageSource)和 pixelMapInstancePixelMap)都是必须手动释放的 Native 资源,ArkTS 侧垃圾回收不会及时回收底层 C++ 内存。不释放的后果:连续拍十几张后应用内存只涨不跌,最终被系统按内存水位回收甚至闪退。此外相机侧 releaseCamera() 里对 cameraInputpreviewOutputphotoOutputcaptureSession 的逐一 release() 同样属于这一条——凡是拿到"对象资源"的地方,都要问一句:它释放了吗?

4. 避免重复创建与并发识别

  • recognizeImage 每次调用都新建 ImageSourcePixelMap,用完即释放,这是正确的;要避免的是把"释放"漏掉,或把 ImageSource 存成成员变量长期占用。
  • 连续快速点快门时,photoAvailable 回调可能连续触发。如果上一次识别还没结束,this.result 会被后一次覆盖,属于"可接受"的最终值语义;但若要更严谨,可以在 takePicture() 里加"识别中"标志,识别期间忽略新的拍照请求。

5. 异步与 UI 卡顿

recognizeImageasync 方法,recognizeText 返回 Promise,识别本身不阻塞 UI 线程;真正需要警惕的是大图解码那一步——它跑在异步上下文里,但如果输入是超大图,解码耗时同样可观,此时 UI 虽然没有卡死,弹窗打开会明显滞后。配合第 2 条的"解码期缩小",能同时缓解耗时与内存。

6. 拍照 → 识别时序

工程时序为:takePicture()(先置空 this.result)→ photoOutput.capture() → 会话回调 photoAvailablephoto.main.getComponent(JPEG) 取字节 → recognizeImage(buffer) 识别 → 写回 this.result@Watch 触发弹窗。时序上的两个注意点:takePicture 置空 result 是为了防止旧结果重复弹出(详见第 37 篇);getComponent 的回调里 errCode 非 0 或 componentundefined 时直接 return,避免把脏数据送进识别。

代码示例

工程释放段逐行讲解(源码参考:entry/src/main/ets/common/utils/Camera.ets):

async recognizeImage(buffer: ArrayBuffer): Promise<string> {
  let imageResource = image.createImageSource(buffer);   // ① 解码源
  let pixelMapInstance = await imageResource.createPixelMap(); // ② 像素图(内存大户)
  let visionInfo: textRecognition.VisionInfo = {
    pixelMap: pixelMapInstance
  };
  let textConfiguration: textRecognition.TextRecognitionConfiguration = {
    isDirectionDetectionSupported: true
  };
  let recognitionString: string = '';
  const context: common.UIAbilityContext = AppStorage.get('context') as common.UIAbilityContext;
  try {
    if (canIUse('SystemCapability.AI.OCR.TextRecognition')) {
      await textRecognition.recognizeText(visionInfo, textConfiguration).then((TextRecognitionResult) => {
        if (TextRecognitionResult.value === '') {
          recognitionString = context.resourceManager.getStringSync($r('app.string.unrecognizable').id);
        } else {
          recognitionString = TextRecognitionResult.value;
        }
      })
      pixelMapInstance.release();   // ③ 识别完成立即释放像素图
      imageResource.release();      // ④ 释放解码源
    } else {
      recognitionString = context.resourceManager.getStringSync($r('app.string.Device_not_support').id);
      Logger.error(TAG, `device not support`);
    }
  } catch (error) {
    let err = error as BusinessError;
    hilog.error(0x0000, 'Camera', `recognizeImage failed. code=${err.code}, message=${err.message}`);
  }
  return recognitionString;
}

两点改进建议:③④ 放在 finally 里更稳妥(异常路径也能释放);② 处可用 desiredSize 提前缩小。优化版如下:

async recognizeImageOptimized(buffer: ArrayBuffer): Promise<string> {
  let imageResource = image.createImageSource(buffer);
  // 解码期直接缩到 1080 宽以内(建议 720p 以上,1080 是安全上限)
  let pixelMapInstance = await imageResource.createPixelMap({
    desiredSize: { width: 1080, height: 1080 },
    editable: false,
    pixelFormat: image.PixelMapFormat.RGBA_8888  // VisionInfo 要求 RGBA_8888
  });
  let visionInfo: textRecognition.VisionInfo = { pixelMap: pixelMapInstance };
  let textConfiguration: textRecognition.TextRecognitionConfiguration = {
    isDirectionDetectionSupported: false   // 已知正向时关闭朝向检测
  };
  let recognitionString: string = '';
  try {
    if (canIUse('SystemCapability.AI.OCR.TextRecognition')) {
      const result = await textRecognition.recognizeText(visionInfo, textConfiguration);
      recognitionString = result.value === '' ?
        AppStorage.get<common.UIAbilityContext>('context')!.resourceManager.getStringSync($r('app.string.unrecognizable').id) :
        result.value;
    } else {
      recognitionString = AppStorage.get<common.UIAbilityContext>('context')!.resourceManager.getStringSync($r('app.string.Device_not_support').id);
    }
  } catch (error) {
    let err = error as BusinessError;
    hilog.error(0x0000, 'Camera', `recognizeImage failed. code=${err.code}, message=${err.message}`);
  } finally {
    // 无论成功失败都释放,防止内存泄漏
    pixelMapInstance.release();
    imageResource.release();
  }
  return recognitionString;
}

运行效果与注意事项

  • 内存观测:用 hdc shell 查看应用内存,或在 DevEco Studio 的 Profiler 里连续拍照识别,观察 native 内存曲线是否持续走高——若只涨不跌,基本可以断定有 release() 漏网之鱼。
  • 优化前后对比:把输入从 4000×3000 缩到 1080 内 + 关闭朝向检测,单次识别耗时通常能缩短一半以上,弹窗出现明显更快。
  • finally 的重要性recognizeText 抛异常(超时 200、服务异常 1001400002 等)时,原工程③④两行不会执行;用 finally 统一释放是更稳妥的写法。
  • 相机资源:页面 onPageHide / aboutToDisappear 时调用 releaseCamera() 逐一释放相机输出与会话,否则切后台再回来可能黑屏或无法重新取景。

优化清单

  • [ ] 输入图用 desiredSize 缩到 1080 以内,保持 RGBA_8888
  • [ ] 已知正向图片关闭 isDirectionDetectionSupported
  • [ ] pixelMapInstance.release()imageResource.release() 放进 finally
  • [ ] 页面隐藏/销毁时释放相机会话与输出;
  • [ ] 连续拍照加"识别中"互斥标志,避免并发覆盖结果;
  • [ ] 识别前后打 hilog 时间戳,量化每段耗时,持续调优。

总结

OCR 优化的本质是"掐两头":输入端控制图片尺寸、关掉不必要的朝向检测,输出端保证资源必释放。release() 放在 finallycreatePixelMap 时指定 desiredSize,是成本最低、收益最大的两处改动;再配合拍照时序上的互斥与页面生命周期里的相机释放,一个"又快又稳"的拍照识别应用就成型了。


*本系列文章基于 HarmonyOS 5.0.5 SDK(API 12)与示例工程 aicharacter-recognition 编写,文中接口以对应版本 API 参考为准。*

Logo

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

更多推荐