做 Flutter 跨端开发的人,这两年基本都会被同一个问题问住:你们的 App 什么时候能上鸿蒙?说起来跨端框架跨的是 Android 和 iOS,到了鸿蒙这个新平台,很多好用的三方库根本不认识它。尤其是摄像头扫码这种场景,Android 有 ML Kit,iOS 有原生 Vision,Flutter 生态里 qr_code_vision 算是封装得比较完整的方案,但它底层还是依赖平台通道,HarmonyOS 上直接编译就会卡在原生实现缺失这一步。
这篇文章分享的是我在鸿蒙应用上适配 qr_code_vision 的完整过程,包括二维码生成、精密扫码、视觉管理、相机帧处理和 Flutter 侧的状态联动。不是官方文档式陈列,而是把踩过的坑和验证过的路径讲明白。适合正在做 Flutter 鸿蒙化迁移、或者想评估扫码方案的人参考,哪怕你对鸿蒙开发不熟,只要写过 Flutter,跟着步骤走也能跑通最小闭环。
1. 为什么要在鸿蒙上重新审视 Flutter 二维码库
1.1 先从一次失败的集成说起
当时我接到一个项目,要在原来的 Flutter 应用里加入设备绑定功能,产品经理的需求很简单:打开扫码页,对准设备上的二维码,识别成功后在页面弹出设备信息。原项目在 Android 和 iOS 上用的是 qr_code_vision,代码都写好了,拍照识别、相册识别、生成码都在跑。等到鸿蒙版本立项,我理所当然认为这个库也能直接编译过去——毕竟 Flutter 的平台抽象层在那里摆着。
结果编译直接报错,原生模块找不到对应的 HarmonyOS 实现。原因是 qr_code_vision 的 Android 端依赖 CameraX,iOS 端依赖 AVFoundation,这两套东西在鸿蒙上都不存在。这个现象其实不是孤立问题,Flutter 生态里大量三方库都有类似困境:Dart 层写得再优雅,只要原生层用了 Android/iOS 专属 API,到了鸿蒙就得重新适配。
当时鸿蒙新平台还处在快速迭代期,三方库适配大多是社区驱动的,Flutter 官方的三端支持还没有完全铺开。与其等库作者更新,不如自己上手改造成果更直接。后面我花了差不多两周时间把这个库在鸿蒙上跑通了,核心思路就是保留 Dart 层 API 不变,把原生实现彻底替换成鸿蒙自己的能力。
1.2 qr_code_vision 到底解决什么问题
先把这个库本身讲清楚。qr_code_vision 在 Flutter 生态里属于少见的“一条龙”扫码方案,它不仅做识别,还把二维码生成、相册图片识别、实时视频流扫码、多码识别这些都打包进去了。相比直接用 zxing 或者单独接相机插件,它的优势是 API 统一,开发者不需要自己去拼 Camera 插件和解码库。
我从实际使用角度列一下它最核心的几个能力:
- 实时相机扫码:拿摄像头预览流,每一帧送到解码器,返回二维码内容。
- 多码识别:一帧画面里可以同时识别多个二维码,返回结果列表。
- ROI 区域识别:可以指定画面中的一块矩形区域,只扫描这个区域里的码。
- 相册图片识别:从图库里拿图片,解析里面包含的二维码。
- 二维码生成:传入字符串内容,生成不同尺寸、不同容错率的二维码图片。
- 视觉管理:对生成出来的码提供样式参数控制,比如颜色、边距、嵌入 Logo 等。
标题里说的“生成式二维码视觉管理”,我的理解不是 AI 生成二维码,而是指这套“生成 + 管理 + 识别”一体化的视觉处理能力。二维码本身承载的内容是数据,但到了业务层,你需要控制它的容错率、尺寸、跳转逻辑、扫码后的行为,这套东西统称视觉管理。
在实际业务里,我见过有人把二维码生成和识别分开做:生成用 qr_flutter,识别用 mobile_scanner。这么拆分本身没问题,但会带来两个痛点,一个是两张二维码的样式规范难以对齐,另一个是两个插件各自维护相机权限和生命周期,代码会显得很碎。qr_code_vision 一个库打通生成和识别链路,在鸿蒙化改造时反而省事,因为只需要适配一个原生接口面。
1.3 鸿蒙化适配的整体思路:抽掉平台层,留下 API
做鸿蒙化的第一步不是写代码,而是先认清这个库的架构。qr_code_vision 走的是 Flutter 标准的 plugin 模式:Dart 层定义统一的接口,MethodChannel 把调用转发到原生侧,原生侧通过 CameraX 或 AVFoundation 去操作相机,解码成功后把结果回传给 Dart。
所以鸿蒙化适配的本质是一个替换问题。Dart 层完全不用动,Flutter 侧的业务代码也不用动,需要改的是原生实现。具体做法就是给这个库增加一个 harmony 平台包,让 Flutter 在鸿蒙设备上自动加载鸿蒙实现。这种改造方式在 Flutter 生态里叫 federated plugin,也是官方推荐的插件组织方式。
整体改造可以分成三层来看。第一层是通道层,保留原有的 MethodChannel 名称和参数格式,确保 Dart 侧调用方无感知。第二层是能力层,把鸿蒙系统的 CameraKit、ScannerKit、ImageKit 接进来,分别对应原库的相机预览、解码、图片处理功能。第三层是数据层,需要把鸿蒙的图像帧格式转换成 Dart 侧能理解的格式,这个往往是整个适配里最容易被忽略也最容易出问题的地方。
这个思路听起来不复杂,但实际操作里有不少细节,尤其是帧数据的生命周期和线程切换。我把整个适配过程走下来之后,最大的感受是:鸿蒙不是 Android 的替代品,而是一个新的、有自己的 API 设计和数据流转逻辑的平台。你不能把 Android 代码直接搬过来,而是要用鸿蒙的方式重新表达一遍。
2. qr_code_vision 核心原理与关键参数拆解
2.1 一帧图像变成一个二维码结果,中间经历了什么
要适配好扫码功能,先得理解二维码识别的完整链路。摄像头采集到的原始数据是一帧 YUV 格式的图像,但大多数解码器输入需要的是灰度图或 RGBA 图,所以第一步是把 YUV 转成解码器需要的格式。
接下来是定位阶段。解码器会在图像里寻找二维码的三个角点定位图案,通过黑白模块的比例关系判断候选区域。这一步对图像质量要求很高,如果画面模糊、光线不均匀或者二维码被部分遮挡,定位就会失败。定位成功之后,解码器读取模块序列,按照二维码的编码规则还原出原始字节流,最后再做一次纠错和字符编码转换,输出字符串结果。
整条链路里最容易被忽视的是帧格式。Android 的 CameraX 给出来的是 ImageProxy,iOS 给出来的是 CMSampleBuffer,两者的像素格式和内存布局都不一样。鸿蒙给出来的是 Frame 对象,你需要自己处理 buffer 的生命周期。如果漏掉了格式转换,直接拿 YUV 数据去解码,结果大概率是识别失败或者识别率急剧下降。
我在适配过程中画了一张很简单的流转图,后面代码实现也是按这个逻辑走的:相机帧采集 → 格式转换 → 发送给解码器 → 返回解码结果 → 回调到 Flutter 侧。每一步之间都用线程隔离,避免把相机回调塞到 UI 线程。
2.2 识别精度与性能的三个关键参数
扫码功能跑起来之后,紧接着就是调优。qr_code_vision 的 API 里有三个参数对识别体验影响最大,这里单独拆解一下。
第一个是帧间隔。相机每秒可能产生 30 帧甚至 60 帧数据,如果把每一帧都送去解码,CPU 和内存都会被拖垮。常规做法是设置帧间隔,比如每 200 毫秒处理一帧。这个值直接影响识别速度:间隔太长,用户会感觉扫码迟钝;间隔太短,性能压力大还会出现发热。我的经验值是 100 到 150 毫秒,在绝大多数设备上都能兼顾流畅度和功耗。
第二个是 ROI 区域,也就是识别区域。很多扫码页会在画面中间画一个框,提示用户把码放在框内。如果不设置 ROI,解码器会对整帧图像做扫描,不仅浪费时间,还容易误识别背景里的其他二维码。把 ROI 设置成和扫描框重合的矩形区域,识别速度和准确率都会明显提升。
第三个是容错率级别。二维码容错率分为 L、M、Q、H 四个等级,选择多少级取决于二维码的展示方式。如果是印在设备上的贴纸,我用 M 级;如果是屏幕显示,我会用 Q 级,因为屏幕有反光和刷新,更高容错率能提升扫码成功率。但容错率也不是越高越好,等级越高码上的数据密度就越大,同样内容生成的二维码图案会更密集,反而增加了识别难度。
下面这张表是我在鸿蒙测试机上整理出来的参数对比,可以直观看到差异:
| 参数 | 低配 | 推荐值 | 高配 | 说明 |
|---|---|---|---|---|
| 帧间隔 | 500ms | 100-150ms | 50ms | 越小响应越快,CPU 开销越大 |
| ROI | 全画面 | 画面中央 60% 区域 | 自定义精确区域 | 越小越精准,但容易漏码 |
| 容错率 | L | M/Q | H | 越高越耐遮挡,但码更密集 |
这三个参数不是孤立存在的。帧间隔缩短之后,如果 ROI 还是全画面,解码耗时会成倍上升;容错率提高之后,如果帧间隔还是 500 毫秒,用户稍微手抖一下可能就扫不出来了。调优需要一起动。
2.3 生成式二维码的视觉管理不只有“画个码”
很多人对二维码生成的理解就是“把一个字符串变成一张图”,其实落到业务里,这里牵扯到不少视觉层面的管理逻辑。
二维码的容错率直接决定扫码距离。容错率越高,码可以印得越小、离摄像头越远,因为纠错机制能补回一部分缺失或污损的区域。反过来,如果二维码要印在产品包装盒上,距离远、光线差,容错率就得上调。我在生成设备标签二维码时,固定用 Q 级容错率,实测下来比 M 级的扫码距离提高了差不多 40 厘米。
样式定制是另一块。qr_code_vision 的生成接口支持调颜色、背景色、边距和尺寸,但这里有个很容易踩的坑:不要把前景色调成浅色,也不要把背景色调成深色。扫码设备对颜色对比度极其敏感,低对比度的二维码看起来“很好看”,实际扫描时定位图案可能根本无法识别。在我经手的项目里,凡是扫码率低的二维码,九成都是对比度不够。
“视觉管理”这个词容易让人以为只跟视觉有关,我理解它其实包含两层意思:一层是把二维码本身做成视觉上可控、可定制、可预测的东西;另一层是在业务上管理这些码,比如给每个二维码绑定业务 ID、设计跳转链接、统计扫码来源。适配的时候,第一层靠生成 API 的样式参数,第二层靠 Flutter 侧的业务逻辑。
我遇到过一种情况:同一个二维码内容,在 Android 上生成的码能被鸿蒙设备扫出来,在鸿蒙上生成的码却被 Android 设备拒之门外。排查到最后发现,问题出在字符编码上。Dart 侧的字符串默认是 UTF-8,但生成二维码时如果不显式指定编码,有的库会退回到默认的 ISO-8859-1,中文内容直接就乱码了。鸿蒙化适配中,字符编码一定要显式指定,尤其是内容里带中文或特殊符号时。
2.4 扫码结果要进 Flutter 的 Provider
扫码功能从来不是独立的一环。二维码识别出来之后,结果要传给业务页面做后续处理。热搜词里有个高频问题“flutter provider 怎么用”,正好对应这个场景:扫码结果从原生通道回到 Flutter 侧之后,最好用状态管理把数据分发到多个页面。
我常做的做法是定义一个 ScanResultProvider,继承 ChangeNotifier,扫码结果解析好之后写入这个 Provider,页面通过 context.watch 或者 Consumer 去监听变化。
import 'package:flutter/foundation.dart'; class ScanResult extends ChangeNotifier { String _rawValue = ''; String? _businessCode; bool _isProcessing = false; String get rawValue => _rawValue; String? get businessCode => _businessCode; bool get isProcessing => _isProcessing; void setResult(String raw) { _rawValue = raw; _businessCode = _extractBusinessCode(raw); _isProcessing = false; notifyListeners(); } void startProcessing() { _isProcessing = true; notifyListeners(); } String? _extractBusinessCode(String raw) { final uri = Uri.tryParse(raw); if (uri != null && uri.host.isNotEmpty) { return uri.queryParameters['code']; } return null; } }这里有一个特别重要的意识:MethodChannel 的返回结果必须回到主线程再更新状态。Flutter 的 Provider 只能在 UI 线程里改状态,如果扫码回调在后台线程执行,直接调用 notifyListeners 会触发 “setState called after dispose” 或者 “setState called on non-UI thread” 一类的报错。解决方案是在 Dart 侧拿到原生返回值后,用 WidgetsBinding.instance.addPostFrameCallback 或者直接 await 通道结果,让回调自然回到主线程。
3. 鸿蒙化适配实操:从环境配置到扫码跑通
3.1 准备环境与改 pubspec
开始动手之前,先把开发环境准备好。鸿蒙应用开发需要 DevEco Studio,Flutter 这边要使用支持 HarmonyOS 平台的 Flutter SDK 分支。我这里用的是社区维护的版本,它让 Flutter 工具链认识鸿蒙设备,能够正常构建和安装应用。
环境就绪后,需要对项目做工程改造。qr_code_vision 原本是普通 plugin 结构,我们要把它改成 federated plugin:主包里只保留 Dart 层接口和 Android/iOS 原有实现,然后新增一个单独的 harmony 实现包,专门处理鸿蒙原生代码。
pubspec 里依赖声明的常见写法是这样的:
dependencies: flutter: sdk: flutter qr_code_vision: # 指向你维护的鸿蒙化分支 git: url: https://github.com/yourname/qr_code_vision.git path: packages/qr_code_vision鸿蒙实现包和主包通过 pluginClass 关联。具体来说,鸿蒙原生侧需要提供一个类实现 FlutterPlugin,并在 register 方法里把 MethodChannel 的 handler 注册进去。这个步骤和 Android 的插件注册逻辑是类似的,但语法是 ArkTS,页面要用 ArkTS 文件。
有朋友问过我,能不能不 fork 原始库,直接在应用里通过 MethodChannel 自己调用鸿蒙扫码能力。这当然可以,但带来的问题是应用层代码会夹杂大量原生逻辑和线程管理,业务复用性很差。我更推荐花半天时间把插件工程建起来,后面只要调 API 就行。
3.2 权限与相机配置
相机扫码绕不开权限。鸿蒙的权限体系和 Android 类似,也需要在 module.json5 里声明,然后运行时动态申请。
权限声明部分要在 module.json5 的 requestPermissions 数组里加这两项:
{ "name": "ohos.permission.CAMERA", "reason": "需要使用相机扫描二维码", "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } }注意,只声明权限还不够。鸿蒙系统和 Android 一样强调运行时权限,在 Flutter 侧进入扫码页时,需要通过 permission_handler 或者鸿蒙原生代码动态发起授权请求。我遇到过一次很诡异的黑屏,排查到最后发现就是权限没授予,相机已经打开了但 Preview 流拿不到数据。
相机配置这一步,鸿蒙提供的是 CameraKit。初始化流程大致是:获取相机管理器 → 获取相机列表 → 创建输入流和预览流 → 把预览流绑定到 Surface 上。核心示意代码如下:
import { camera } from '@kit.CameraKit'; import { common } from '@kit.AbilityKit'; import { BusinessError } from '@kit.BasicServicesKit'; async function setupCamera(context: common.UIAbilityContext, surfaceId: string) { const cameraManager = camera.getCameraManager(context); const cameras = cameraManager.getSupportedCameras(); if (cameras.length === 0) { throw new Error('未找到摄像头'); } const cameraDevice = cameras[0]; const capability = cameraManager.getSupportedOutputCapability(cameraDevice); // 创建预览输出并关联 SurfaceId,SurfaceId 来自 XComponent 组件 const previewOutput = cameraManager.createPreviewOutput( capability.previewProfiles[0], surfaceId ); // 创建帧输出,用于扫码识别 const frameOutput = cameraManager.createFrameOutput( capability.frameProfiles[0] ); const session = cameraManager.createSession(camera.SceneType.NORMAL_PHOTO); session.beginConfig(); session.addInput(cameraDevice); session.addOutput(previewOutput); session.addOutput(frameOutput); await session.commitConfig(); await session.start(); return frameOutput; }这段代码把预览和帧输出绑在同一个会话里,确保用户看到的预览画面和拿去识别的帧数据来自同一路相机流。后面识别回调就在 frameOutput 的输出回调里拿到。
3.3 相机帧到解码结果的全链路
帧输出的回调里拿到的是 YUV 格式的数据。qr_code_vision 原来在 Android 侧直接拿 ImageProxy 转灰度图,鸿蒙侧没有这个类,需要自己写转换逻辑。这里我提供一个简化版的思路:
// Dart 侧伪代码,示意 YUV420 转 RGBA Uint8List yuvToRgba(Uint8List yuvData, int width, int height) { final rgba = Uint8List(width * height * 4); final ySize = width * height; final uvSize = ySize ~/ 4; for (var y = 0; y < height; y++) { for (var x = 0; x < width; x++) { final yIndex = y * width + x; final uvIndex = ySize + (y ~/ 2) * (width ~/ 2) + (x ~/ 2); final yValue = yuvData[yIndex]; final uValue = yuvData[uvSize + uvIndex]; final vValue = yuvData[uvSize + uvIndex + 1]; final r = (yValue + 1.402 * (vValue - 128)).clamp(0, 255); final g = (yValue - 0.344136 * (uValue - 128) - 0.714136 * (vValue - 128)).clamp(0, 255); final b = (yValue + 1.772 * (uValue - 128)).clamp(0, 255); final index = yIndex * 4; rgba[index] = r.toInt(); rgba[index + 1] = g.toInt(); rgba[index + 2] = b.toInt(); rgba[index + 3] = 255; } } return rgba; }转换之后的数据要尽快交给解码模块,同时要及时释放鸿蒙侧 Frame 对象,避免 buffer 堆积。这里我交过学费:最初版本没有主动 release 帧,连续扫码 10 分钟后内存直接翻了三倍,界面肉眼可见卡顿。正确做法是在帧回调里处理完数据立刻调用 Frame 的 release 方法。
解码这一步,我用的是 ScannerKit 的扫码能力。ScannerKit 能识别二维码、条形码等多种码制,也有实时帧识别接口。传入转换后的灰度图像,回调里会返回识别结果,包括码内容、码类型和坐标信息。代码示意:
import { scanBarcode, ScanOptions } from '@kit.ScannerKit'; const options: ScanOptions = { scanTypes: ['QR_CODE'], enableMultiMode: true, }; const scanResult = await scanBarcode(frame, options); if (scanResult.length > 0) { // 取第一个结果,通过 EventChannel 或回调返回给 Flutter const content = scanResult[0].value; }把解码放到独立线程是底线。ScannerKit 的解码虽然做了性能优化,但帧数据量大,如果放在相机回调的调用线程里做,会阻塞后面的帧采集。我实践下来的方案是在帧回调里只做格式转换和投递,由一个单独的 DispatchQueue 去跑解码任务,完成后再通过 EventChannel 把结果送出去。
3.4 二维码生成的鸿蒙实现
生成端相对识别要简单一些,因为不涉及实时视频流,不需要处理线程切换和内存释放。qr_code_vision 在生成时的核心参数有内容、尺寸、容错率、前景色和背景色。
在鸿蒙侧实现生成,我用了 Canvas 绘制方案:创建一个和指定尺寸一致的 PixelMap,在上面绘制定位图案和数据模块。这样做的优点是样式可控性强,能直接支持 Logo 嵌入和圆角调整。
要注意的是图片数据回传 Flutter 侧的格式。如果直接回传 PixelMap,Dart 侧拿到的是一堆像素字节,还需要再转换成 Flutter 的 Image widget 才能展示。我建议在原生侧直接把 PixelMap 编码成 PNG 字节流,通过 MethodChannel 以 Uint8List 形式返回。这样 Dart 侧直接用 Image.memory 就能渲染。
这里有一个细节:生成二维码时内容里的中文字符串必须用 UTF-8 编码,并保证编码器按照 UTF-8 进行数据编码。我在调试阶段遇到过中文内容生成的码扫出来是乱码,后来定位到是编码器默认用了 GBK 或者 ISO 编码。鸿蒙侧显式声明 UTF-8 之后就解决了。
3.5 把扫码能力封装成可复用的 Flutter 组件
原生侧改完之后,Dart 侧要封装一个让业务方能直接用的页面。我用的是典型的 PageView + 相机预览组件结构:进入扫码页启动相机,识别成功后通过 Provider 提交结果,然后自动跳转。
import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:qr_code_vision/qr_code_vision.dart'; class ScanPage extends StatefulWidget { const ScanPage({super.key}); @override State<ScanPage> createState() => _ScanPageState(); } class _ScanPageState extends State<ScanPage> { final _controller = QrCodeVisionController(); @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, body: Stack( children: [ QrCodePreview( controller: _controller, onDetect: (results) { if (results.isNotEmpty) { context.read<ScanResult>().setResult(results.first.value); Navigator.of(context).pop(); } }, ), const ScanOverlay(), ], ), ); } }组件化封装的好处是扫码页可以作为一个独立功能块,放在应用任意入口。绑定设备时用扫码页,登录时用扫码页,后续增加新的扫码业务也不用重复写相机逻辑。
4. 常见问题与排查技巧实录
4.1 一张排查速查表
整个适配过程里我整理了一张速查表,每次遇到问题都会先对照一遍,能省不少排查时间。
| 现象 | 可能原因 | 排查方向 | 解决方案 |
|---|---|---|---|
| 相机预览黑屏 | 权限未授予 / Surface 绑定失败 | 检查权限状态和 XComponent 的 surfaceId | 动态申请权限,确保 Surface 初始化完成后再启动相机 |
| 扫码识别率低 | 帧格式转换错误 / ROI 设置过大 | 打印帧分辨率,确认 YUV 转灰度无误 | 缩小 ROI,检查转码算法 |
| 解码卡顿 | 在相机回调线程解码 | 查看 CPU 占用和线程堆栈 | 解码放到独立子线程 |
| 中文扫码结果乱码 | 编码方式不一致 | 检查生成端是否显式指定 UTF-8 | 生成和识别都统一用 UTF-8 |
| 画面方向不对 | 设备旋转角度未适配 | 检查传感器角度 | 根据设备方向计算图像旋转角度再解码 |
| 结果回调丢失 | 生命周期管理问题 | 检查页面销毁时是否 deregister 通道 | 在 dispose 中注销监听,防止回调到已销毁页面 |
| 生成码扫不出来 | 对比度过低 / 边距不足 | 用放大镜观察码的安静区 | 设置至少 4 个模块宽的边距 |
4.2 六个踩坑细节
第一个坑是 XComponent 的初始化时机。Flutter 侧的预览组件在首次 build 时会请求鸿蒙侧创建一个 Surface,但 Surface 的创建是异步的。如果相机启动调用在 Surface 创建之前发起,预览就会黑屏。解决方法是把 SurfaceId 通过回调提供给原生侧,等 Surface 就绪再 start 相机会话。
第二个坑是帧回调频繁导致 ANR。鸿蒙的帧输出可以在高频场景下每秒回调几十次,如果不能及时处理就会堆积。我的做法是做一个简单的生产者消费者模式:帧回调只入队,解码线程从队列里取帧。同时控制入队数量,比如队列超过一定长度就丢帧,保证解码线程永远处理最新数据。
第三个坑是“挑角度”。项目测试时发现同样一个二维码,正对着扫能识别,倾斜 30 度就识别不出来。排查下来是解码器对畸形二维码的容错不足,尤其是二维码在画面上带透视畸变时,定位图案的比例会发生变化。这里不能只靠调容错率,更合理的方案是让用户移动手机重新对焦,同时适当调大 ROI 区域,给解码器更多上下文信息。
第四个坑是结果回调时机和 Flutter 页面生命周期的竞争。扫码成功后原生通道把结果发回 Dart,如果这时候用户已经退出扫码页,通道回调依然会执行,导致空指针或者页面状态被修改。我的处理方式是在 Flutter 组件销毁时显式调用原生侧的 stopSession,同时给回调加一个 mounted 判断。
第五个坑是 release 版本下生成二维码接口偶发崩溃。后来定位到是原生侧把 PixelMap 释放得太早,PNG 编码还没完成内存就被回收了。这个问题的教训是:异步操作的回调里,资源释放时机一定要放在所有数据处理完成之后,绝不能贪图省内存提前释放。
第六个坑是 Flutter 侧拿到 Uint8List 图片后直接用 Image.memory,导致大图加载卡顿。生成出来的二维码图片,如果尺寸是 1024x1024,内存占用会非常可观。更合理的做法是生成时控制尺寸,界面展示时用 256 或 512 就足够,毕竟屏幕扫码根本不需要 4K 级别的二维码。
结尾
整个适配做下来,我最深的一个体会是:鸿蒙不是换个壳的 Android,它的相机框架、权限模型、生命周期管理和图片流转都有自己的一套逻辑。做 Flutter 鸿蒙化适配,不能指望一个包解决所有问题,更重要的是理解原有库的架构,然后把它拆开、替换、重新组装。
最后再分享一个小技巧:调试扫码帧数据时,把每一帧的亮度直方图打印出来,能很直观地判断是环境光线不足还是帧数据本身有问题。这个习惯我保留到现在,遇到扫码率骤降的情况,先看数据,再动代码,比盲目调参高效得多。如果你正在做类似的适配,希望这篇文章能帮你少走几段弯路。