有段时间我在做 Flutter 项目的代码质量门禁时,发现sonar_analysis这个三方库在 iOS 和 Android 上表现一直很稳定,但一放到鸿蒙(HarmonyOS/OpenHarmony)环境里,整个通道直接就哑火了。翻了一圈社区资料,相关的适配文档基本都停留在“理论可行”的阶段,真正能把代码审计、质量指标采集和门禁闭环跑起来的实战案例非常少。这篇文章就是一次完整的鸿蒙适配过程记录,我会把sonar_analysis从插件工程改造、ArkTS 通道实现到对接 sonar-scanner 的完整链路讲清楚,适合正在做 Flutter 鸿蒙化、或者准备把现有 Flutter 三方库移植到鸿蒙系统的读者参考。
我这里说的“全栈质量守卫”,不是简单地把代码扫描结果展示出来,而是让sonar_analysis在鸿蒙端也能完成三个层面的任务:第一,采集 Flutter 侧的 Dart/TS 静态扫描结果;第二,通过原生通道读取鸿蒙工程产物和 HAP 包内的可分析文件;第三,把分析数据汇聚到 SonarQube 服务端,形成统一的代码质量门禁。这套东西跑通之后,鸿蒙应用和 Flutter 应用在同一个质量体系下被审查,才是真正的“全栈”。
1. 为什么要给 sonar_analysis 做鸿蒙适配,而不是另起炉灶
1.1 sonar_analysis 到底解决了什么问题
sonar_analysis不是一个简单的“报错插件”,它的定位是 Flutter 侧的代码审计客户端。在 Android 和 iOS 上,它通常负责三件事:收集 Dart 代码的静态分析指标(复杂度、重复率、代码行数)、触发本地声纳扫描器(sonar-scanner)进行分析、然后通过通道把分析结果推给上层应用或后端服务。
听起来功能不复杂,但它所在的生态位置很特殊。很多中大型 Flutter 项目并不是只有 Flutter 一门语言,工程里混杂着原生代码、ArkTS(鸿蒙侧)、Kotlin/Swift(双端原生)。如果只用原生 SonarQube 插件分析,Flutter 层的 Dart 代码基本处于“裸奔”状态。而sonar_analysis这种插件能充当桥梁,让 Flutter 层的质量数据也进入同一个 SonarQube 项目。
鸿蒙适配的难点不在于插件复杂度高,而在于它的通信链路完全依赖 Flutter 与原生平台之间的通道机制。通道一端是 Dart,另一端是平台实现,鸿蒙没有 Android 的PluginRegistry,也不能直接用 iOS 的FlutterPlugin协议,需要重新在 ArkTS 侧实现一套等价的通道逻辑。
1.2 所谓“全栈质量守卫”具体指什么
“全栈”这个词在质量领域经常被滥用,但放在这个场景里是有明确含义的。一个鸿蒙应用从源码到可交付的 HAP 包,中间要经历 Dart 层编写、ArkTS 层编写、资源编译、依赖打包等多个阶段。质量守卫需要覆盖到每个阶段产生的可分析产物:
- Dart 源码:使用
flutter analyze和自定义 linter 规则扫描。 - ArkTS 源码:使用鸿蒙侧的代码检查工具和 SonarQube 的 ArkTS 规则插件。
- 构建产物 HAP:解包后检查 so 库、资源文件、配置文件是否存在安全隐患。
- 运行时行为:通过日志、性能指标回传,发现内存泄漏、异常调用等问题。
sonar_analysis在 Android/iOS 上已经跑通了这套流程,鸿蒙适配要做的不是重新发明轮子,而是把其中依赖平台能力的部分(文件访问、进程调用、数据通道)迁移到鸿蒙的 ArkTS 环境中。
1.3 鸿蒙适配和普通 Android 移植的差异
很多人第一个念头是“鸿蒙不是兼容 APK 吗?直接把 Android 插件拿过来跑不就行了?”这个想法在早期兼容 Android 的版本里有一定道理,但在面向鸿蒙原生应用开发时,插件必须走 ArkTS 的通道实现,和 Android 插件完全是两套东西。
具体差异体现在三个地方。第一,插件发现机制不同,Flutter 引擎在鸿蒙上会寻找ohos目录下的原生插件入口,而不是 Android 的android目录;第二,通道的值编码和平台侧 API 名称有差异,MethodChannel在 ArkTS 侧要使用兼容层的MethodChannel实现,不能直接照搬 Android 代码;第三,权限模型不同,鸿蒙的沙箱、权限声明方式和 Android 截然不同,sonar_analysis要读取 HAP 文件、扫描目录,就必须重新申请权限。
2. 适配前必须吃透的通道模型与工程骨架
2.1 sonar_analysis 依赖的三类通道
sonar_analysis不是只用一种通道,而是根据业务场景混合使用。理解这一点,是适配成功的前提。它通常依赖三类 Flutter 通道:
一是MethodChannel,用于方法调用,比如“启动一次扫描”、“获取当前工程路径”、“取消任务”。这类通道是双向的,Dart 侧发起调用,原生侧返回结果,适合一次性的请求-响应模式。
二是EventChannel,用于持续事件流,比如扫描进度、实时日志、文件遍历状态等。这类通道是单向的,原生侧主动向 Dart 侧推送事件,Dart 侧只负责监听。sonar_analysis中告警信息的分批到达就是通过它实现的。
三是BasicMessageChannel,用于双向的低层消息通信,通常在传递大量自定义数据结构时使用,比如扫描结果的 JSON 序列化大对象。这类通道不会在本次适配中重点展开,但工程里也会用到。
2.2 鸿蒙 Flutter 引擎下的通道兼容层
鸿蒙的 Flutter SDK(俗称 flax 或 flutter-flutter 的鸿蒙版)在设计时保留了和官方 Flutter 一致的通道 API。也就是说,Dart 侧代码完全不用为鸿蒙单独写一套,真正要适配的是原生侧。
ArkTS 侧实现通道的姿势大概是下面这样:在鸿蒙工程中创建一个继承自Plugin的类,实现对应的方法,然后用引擎内部的PluginRegistry注册。和 Android 的MethodCallHandler相比,ArkTS 侧多了Result的异步回调方式,稍不留神就会写出无法返回结果的空通道代码。
我在实际适配中经常遇到一个现象:Dart 侧invokeMethod一直等不到回调,但插件在应用启动时明明已经被加载了。原因多半是 ArkTS 侧的消息编解码器和 Dart 侧不一致,鸿蒙版本的引擎虽然兼容通道,但值类型的转换和标准 Flutter 引擎仍有细微差别。
2.3 工程骨架:在 pubspec.yaml 里为鸿蒙开一扇门
sonar_analysis的鸿蒙适配,第一步不是写 ArkTS 代码,而是先在插件工程里把鸿蒙平台声明出来。Flutter 插件在发布时,需要通过pubspec.yaml声明各平台的原生入口。
对鸿蒙来说,核心就是在插件声明里增加ohos的pluginClass。示例配置大致长这样:
flutter: plugin: platforms: android: package: com.example.sonar_analysis pluginClass: SonarAnalysisPlugin ios: pluginClass: SonarAnalysisPlugin ohos: pluginClass: SonarAnalysisPlugin dartPluginClass: SonarAnalysisFlutterPlugin这里有两个细节要注意。第一,dartPluginClass是可选字段,但如果你希望在 Dart 侧统一处理原生通道注册顺序,建议配置上,这样可以让插件在 Dart 层初始化时自动注册通道,避免偶发的时序问题;第二,ohos平台声明后,插件工程根目录下必须存在ohos目录,内部要符合鸿蒙模块的标准结构,否则 Flutter 工具链在编译时会直接跳过这个平台,而且不会报明显的错误,表现得像“适配没做一样”。
3. ArkTS 侧通道落地:从空 channel 到真实审计数据
3.1 先搭 MethodChannel:把“扫描一次”变成一次方法调用
sonar_analysis最核心的方法调用是startAnalysis。在 ArkTS 侧实现时,我会在插件类里定义一个方法处理器,用MethodChannel接收来自 Flutter 侧的调用。下面是一段简化后的 ArkTS 实现示意:
import { MethodChannel, MethodCall, Plugin } from '@ohos/flutter_ohos'; export default class SonarAnalysisPlugin implements Plugin { private channel: MethodChannel = new MethodChannel('sonar_analysis/methods'); onAttachedToEngine(binding: any): void { this.channel.setMethodCallHandler(this.handleMethodCall.bind(this)); } private async handleMethodCall(call: MethodCall, result: any): Promise<void> { switch (call.method) { case 'startAnalysis': const path: string = call.arguments['path'] as string; try { const summary = await this.runAnalysis(path); result.success(summary); } catch (e) { result.error('ANALYSIS_FAILED', JSON.stringify(e), null); } break; default: result.notImplemented(); } } private async runAnalysis(projectPath: string): Promise<object> { // 调用鸿蒙侧代码检查服务,这里根据实际能力扩展 return { files: 1024, warnings: 13, defects: 2 }; } }这段代码虽然简单,但能带你跑通第一个链路。关键的几个点:MethodChannel的名字必须和 Dart 侧完全一致,否则 Dart 会报MissingPluginException;call.arguments在鸿蒙侧可能被解码成Map<string, Object>,所以在取值前最好做一个类型守卫;result是异步回调,如果你在runAnalysis里调用耗时任务,千万不要把result.success放到同步代码后面,否则一旦任务长时间未返回,Dart 侧会超时。
3.2 用 EventChannel 把扫描进度和告警推成流
代码审计是一个耗时操作,用户肯定不希望等所有分析完成才看到结果。sonar_analysis在设计上会把扫描进度、命中的规则、实时日志通过EventChannel分批推送。鸿蒙侧的实现要比 MethodChannel 稍微复杂一点,因为你需要实现一个事件流对象。
import { EventChannel, EventSink } from '@ohos/flutter_ohos'; export default class AnalysisEventPlugin { private eventChannel: EventChannel = new EventChannel('sonar_analysis/events'); private sink: EventSink | null = null; constructor() { this.eventChannel.setStreamHandler({ onListen: (args: any, events: EventSink) => { this.sink = events; }, onCancel: (args: any) => { this.sink = null; } }); } pushProgress(percent: number, message: string): void { if (this.sink) { this.sink.success({ percent: percent, message: message }); } } pushWarning(defect: object): void { if (this.sink) { this.sink.success(defect); } } pushError(error: Error): void { if (this.sink) { this.sink.error('SCAN_ERROR', error.message); } } }这里最容易翻车的点在于EventSink的生命周期。如果 Dart 侧的EventChannel.receiveBroadcastStream没有被激活,onListen就不会触发,你在 ArkTS 侧拼命推数据是没用的,数据全部被引擎丢弃。所以我在适配时会在 Dart 侧做一次“冷启动探测”,先让事件流处于监听状态,再触发扫描任务,保证进度推送不会丢失。
3.3 沙箱与权限:能让分析器读到 HAP 才算第一步
sonar_analysis到鸿蒙之后要做的第一件实际工作,是找到 HAP 包的位置并读取出可分析内容。鸿蒙应用的沙箱机制比 Android 更严格,普通应用能读到的路径只有自己的沙箱目录和公共媒体目录。
我一开始最天真的做法是直接读取应用安装路径下的 HAP,结果发现代码审计进程根本没有权限访问安装目录。正确的做法是:通过应用上下文获取到 bundle 信息,把需要分析的产物复制到沙箱的 cache 目录,然后让分析逻辑在沙箱内完成读取。
权限部分的 ArkTS 声明也只能落在两处:一是module.json5中声明相关权限,比如读取存储的权限;二是运行时判断权限是否已授权。有一点需要特别提醒:如果你只是分析 HAP 内的静态资源,有时候并不需要存储权限,因为你可以直接通过BundleManager拿到资源的绝对路径,但不一定能直接打开文件流,还是要靠沙箱中转。
3.4 线程模型:别让扫描任务堵住 UI
ArkTS 侧实现代码审计时,最危险的做法是直接在方法调用线程里执行文件遍历、zip 解包、规则匹配。这些操作动辄几百毫秒到几秒,一旦跑在 UI 线程,用户就会明显感到卡顿,严重时会被系统判定为应用无响应。
我建议的做法是:把分析任务放到 WorkScheduler 或者自建的 worker 线程中去执行。下面这个思路在鸿蒙上是可行的:先构建一个任务队列,把startAnalysis请求封装成任务,然后利用异步回调把结果传递回通道。ArkTS 的async/await并不自动切换线程,所以如果你在插件方法里写了await this.unzip(),它依然可能阻塞调用线程。
实测下来,最稳的方案是在Ability侧启动一个独立线程来处理扫描,或者用鸿蒙提供的任务分发机制。这里不能为了图省事而省略线程设计,否则线上环境一旦在高频上报日志时触发通道拥堵,整个质量守卫链路都会反噬业务。
4. 打通代码审计链路:把 sonar_analysis 与 sonar-scanner 接起来
4.1 拿到可分析的构建产物
sonar_analysis在 Flutter 侧的工作本质上只是“数据采集和质量指标托管”,真正完成代码审计的是 sonar-scanner。鸿蒙适配的第二步,是把 Flutter 构建产物和鸿蒙构建产物都整理成 sonar-scanner 能识别的目录结构。
Flutter 产物通常位于build/app/outputs/flutter-apk或鸿蒙引擎对应的输出目录中,鸿蒙的 HAP 产物则位于entry/build/default/outputs/。要让 sonar-scanner 同时分析 Dart 和 ArkTS,我需要把两种产物映射到一个临时目录,或者直接用多个sonar.sources指定。
4.2 sonar-project.properties 关键配置
SonarQube 的项目配置是整个质量守卫的“指挥中心”。结合鸿蒙项目的特殊性,我整理了一份通常可以跑通的配置模板:
sonar.projectKey=com.example.harmony_quality sonar.projectName=Harmony Flutter Quality Guard sonar.projectVersion=1.0.0 sonar.sources=lib,entry/src/main/ets sonar.exclusions=**/*.g.dart,**/generated/** sonar.sourceEncoding=UTF-8 sonar.language=multi sonar.dart.analysisTimeout=120 sonar.analysis.mode=publish sonar.host.url=https://sonar.internal.example.com sonar.login=${SONAR_TOKEN}这里几个字段值得展开说一下。sonar.sources必须同时包含 Flutter 的lib目录和鸿蒙的ets目录,否则审计范围就会偏科,达不到“全栈”的目的;sonar.language=multi是为了让 SonarQube 的多个语言插件同时生效;sonar.exclusions一定要排除生成代码,Flutter 里最常见的噪音来源是*.g.dart和*.freezed.dart,不排除的话重复率指标会被严重稀释。
4.3 规则集覆盖:Dart、ArkTS 双线审计
大多数团队为了快点上线,会让 SonarQube 只跑默认规则集,这样在鸿蒙项目上的结果其实很有限。因为默认规则集不会覆盖 ArkTS 的专属问题类型,比如状态管理滥用、taskpool 误用、分布式对象生命周期问题等。
在我的适配方案里,sonar_analysis会作为 Dart 侧的扫描触发器,执行完成后生成sonar-report.json;ArkTS 侧则通过自定义分析器导出问题列表。最终这些结果都会被sonar-scanner收集起来,汇总成可审计的质量数据。两条线的规则集建议分开管理:
- Dart 层规则:聚焦可维护性、复杂度、空安全、反模式。
- ArkTS 层规则:聚焦生命周期、并发模型、资源释放、API 合规。
4.4 CI/CD 门禁集成
质量守卫不能只在本地跑,要真正“守卫”工程,就得接入 CI 流水线。我这里的落地方式是在 GitLab 流水线里加一个 job,先执行 Flutter 侧分析和 ArkTS 侧分析,再由 sonar-scanner 把结果上报给 SonarQube,最后用 Quality Gate 的返回状态来决定构建是否继续。
#!/usr/bin/env bash set -euo pipefail flutter pub get flutter analyze --no-fatal-infos --no-fatal-warnings || true dart run sonar_analysis \ --project-path . \ --output build/sonar/dart-report.json ohpm install --all hvigorw assembleHap --mode module -p product=default sonar-scanner \ -Dsonar.host.url=$SONAR_HOST_URL \ -Dsonar.login=$SONAR_TOKEN \ -Dsonar.projectKey=$SONAR_PROJECT_KEY \ -Dsonar.sources=lib,entry/src/main/ets \ -Dsonar.dart.reportPath=build/sonar/dart-report.json需要注意一个细节:flutter analyze不能因为发现 warning 就直接终止,否则会被 CI 的set -e卡死。做法是先允许分析完成,把结果转成报告,再交给 sonar-scanner 统一判定。这套流水线跑起来之后,每次提交的质量反馈不再是零散的,而是一个带告警趋势、文件维度和规则分布的中枢报告。
5. 适配过程中最容易翻车的五个场景(完整排查链路)
5.1 插件声明不生效:ohos 目录存在但 Flutter 引擎不加载
我遇到的第一个坑是:pubspec.yaml里明明写了ohos的pluginClass,Flutter 项目在鸿蒙引擎上运行也没有报错,但 Dart 侧一调用就提示MissingPluginException。这种问题的排查链路是:
- 先检查
ohos目录是否存在于插件根目录,并且是否被正确发布到 pub 缓存中。 - 再检查
Index.ets中是否导出了插件入口类。鸿蒙插件的注册,依赖Index.ets里把插件实例抛给 Flutter 引擎,漏掉这一步引擎根本感知不到。 - 最后检查插件类的
onAttachedToEngine是否被调用,如果这个生命周期方法没执行,多半是注册方式写错了,而不是通道实现的问题。
5.2 通道收到的数据从 Map 变成 List 乱码
sonar_analysis传参和返回值经常使用嵌套的 Map。在 Android 上这套做法很稳,但鸿蒙引擎的编解码器对Map的键顺序和嵌套类型处理会有差异。表现为:Android 侧好好的数据,鸿蒙侧解析出来键名全没了,或者Map变成了Array。
排查时我先在 Dart 侧打了日志,确认传入的Map是正常的,然后回归 ArkTS 侧打印call.arguments的类型,发现直接被解析成了Array。根源是标准通道编码对Map的 Key 类型要求是字符串,而我在 Dart 侧传的时候用了非字符串键,Android 的编解码器自动容错了,鸿蒙侧没有。
解决办法不复杂:统一参数结构,所有 map 的 key 强制字符串,避免使用数字或枚举做键。
5.3 权限与沙箱:读取不到缓存目录
sonar_analysis在鸿蒙上读取cacheDir的时候,一度返回空目录,但应用明明可以正常写入缓存。最后排查发现:Flutter 引擎的 Dart 侧拿到的path_provider缓存路径和鸿蒙原生沙箱路径并不一致,直接拼接出的路径根本不存在。
破解方法是在数据返回前,先通过鸿蒙 API 归一化路径,确保 Dart 拿到的路径是沙箱内真实存在且可读的。有时还需要主动在 native 侧创建目录,因为某些系统版本下缓存子目录不会自动生成。处理完后,最好再回头在 Dart 侧做一个existsSync校验,避免后续扫描流程静默失败。
5.4 HAP 内的 so 没有被 sonar-scanner 识别
代码审计不能只看源码,HAP 包里集成的 so 库、二进制资源也是检查对象。我在对接 sonar-scanner 时发现,它默认只按源码扩展名识别文件,so、hap都没被纳入分析范围。
解决方法是在配置中增加二进制分析插件,或者先把 HAP 解包到临时目录、把里面的*.so导出成 ELF 分析产物,再让 sonar-scanner 扫描这个临时目录。说白了就是让扫描器不要直接面对 HAP 这个压缩包,而是面对解包后的真实文件树。这个环节最容易出现“结果假绿”的假象:看着质量门禁通过了,实际上核心资产根本没有被审计。
5.5 回归验证:Charles 抓包与日志对照
适配完成后,必须做的事是回归验证。我的习惯是同时打开 Charles 抓包和鸿蒙侧的 hilog 日志,检查sonar_analysis是否真的把分析请求发到了 SonarQube 服务端,而不是在本地模拟结果糊弄过去。
具体验证步骤是:执行一次全量分析,然后在 Charles 里过滤 SonarQube 域名,应该能看到 scanner 上报的POST请求;再在 hilog 里观察 ArkTS 侧的事件推送日志,确认进度流和告警流都有数据。两端数据对照完整,才算真正跑通。这一步不能省,很多团队就是栽在“本地截图看起来成功,服务端却没有记录”的假适配里。
6. 实测收益和后续可以继续做的事
这套方案在我的工程里跑通之后,最直观的变化是:Dart 层和 ArkTS 层的质量数据终于合到同一个 SonarQube 仪表盘里了。团队做代码审查时,不再是“看谁的报错少”,而是能直接按模块、按规则、按提交记录去追溯质量波动。鸿蒙侧新引入的规则问题,也会在 MR 阶段被门禁拦住,而不是等到发版之后才被用户反馈暴露。
后续我计划做两件事。第一,把sonar_analysis的分析能力从“静态扫描”扩展到“运行时采集”,定时上报鸿蒙应用的内存占用、崩溃堆栈和分布式调用链数据,进一步补全运行时质量维度。第二,把当前依赖本地构建产物路径的逻辑改成服务端下发配置,这样 CI 和本地开发环境就不需要手工维护目录映射,每位新同事接入时也不会因为路径问题卡壳。
如果你也在做 Flutter 插件的鸿蒙适配,我给你一个建议:先别急着改业务逻辑,把一个最小的通道跑通,从 MethodChannel 到 EventChannel 一点点递进。通道通了,后续的权限、线程、产物收集这些问题都会有明确的方向。鸿蒙的适配表象上看是 API 迁移,实际上是对插件架构理解的重新梳理,至少我现在看任何 Flutter 三方库,第一反应都是先拆解它的通道依赖,鸿蒙侧能不能接得住,心里马上有数。