
我一直觉得“数代码行数”这件事在工程效能里被严重低估了。当团队开始做鸿蒙版本、又把 Flutter 工具链牵进来之后你才会发现一个朴素的问题——这个仓库到底有多少行代码其实没有标准答案。于是我们写了 loc_checker一个用 Flutter 实现的本地代码规模度量工具目标是精准统计代码行数、支持鸿蒙项目的自动化度量并且作为 CI 里的质量门禁基石。这篇文章就是它的适配实战记录内容包括统计口径设计、扫描器实现、鸿蒙化适配链路、门禁集成方式和踩坑实录。如果你也在做 Flutter 工具类应用的鸿蒙化或者团队正准备把代码规模指标接入发布流程这系列经验可以直接参考。1. 为什么用 Flutter 造一个 loc_checker需求拆解与选型逻辑1.1 团队真正需要的不只是“行数总和”最初有人提议“直接用 cloc 不就行了”但我们很快发现团队要的东西远远超出了 cloc 能给的范畴。首先是统计口径问题鸿蒙工程里往往混合了 ArkTS 的 .ets 文件、C 源码、资源 JSON 和自动生成的构建产物不同语言的注释规则差异很大如果只是简单按行数 split空行、块注释、长字符串模板都会污染结果。其次是过程度量问题。研发管理者想知道的不只是“现在有多少行”还包括“这个迭代新增了多少行”“哪些模块在失控膨胀”“一次代码评审通常要面对多少新增量”。最终我们需要的其实是一套可以被数据化、可被脚本消费、能在流水线里自动判断“能不能合入”的规则引擎。光靠命令行工具人工跑一次根本无法形成质量门禁。所以 loc_checker 从一开始就不是“行数展示器”而是一个本地可执行、输出结构化数据、支持阈值判定的度量服务。界面只是辅助真正的产品是那台能够稳定复现统计口径的“计算引擎”。1.2 已经有 cloc 和 tokei 了为什么还要自研我承认 cloc 是经典tokei 的速度也很出色但它们对“团队自定义规则”的扩展性都一般。比如我们要排除oh_modules、.hvigor、build这类鸿蒙工程里的特殊目录希望把“自动生成文件”和“手写代码”分开统计还希望在 JSON 报告里明确区分 ArkTS、Dart、C 的各自行数。这些需求用 cloc 的--exclude-dir和--json也能凑合可是遇到更复杂的多行注释规则、模板字符串边界别人家的工具就不好控制了。另一个更现实的理由是分发场景。我们希望同一个工具能跑在 Windows 开发机、Linux CI 容器、macOS 笔记本上用得爽的话甚至能装到鸿蒙平板上让不熟悉命令行的同事点点界面就能看报告。Flutter 在这类跨平台工具分发上有天然优势一套 Dart 逻辑、一套 UI桌面端和鸿蒙端都能出产物。相比之下cloc 是 Perl 脚本tokei 是 Rust 静态二进制它们的 UI 都等于零。自研的风险也很明显统计口径必须自己维护规则 bug 只能自己扛。但好处是我们的阈值策略、忽略规则、报告格式完全贴合团队流程能真正嵌进 Code Review 和发布流水线里。对于一个想要长期运转的工程效能工具这种掌控感比“用现成工具快速完事”重要得多。1.3 鸿蒙化适配的真实含义不是重写而是少改很多人一听“鸿蒙化适配”下意识以为是按照鸿蒙原生语言重新写一套应用。我们走的是另一条路线loc_checker 本身是 Flutter 项目核心统计逻辑和模型层全部用 Dart 编写鸿蒙侧通过开源的 Flutter 引擎分支运行 Dart 代码UI 层继续复用 Flutter 组件。所谓“适配”是让现有代码在鸿蒙运行时里保持行为一致在文件访问、路径处理、原生能力拉取这些边界点上做插件层桥接。这套思路对于工具型应用特别合适因为工具类应用对系统 API 的依赖通常集中在“读写文件、遍历目录、读取剪切板”之类少数能力上。对比一个重交互的 IM 应用loc_checker 的鸿蒙适配复杂度其实小不少但仍然能让你完整经历一次“Flutter 工程跑成 HAP 包”的全过程。提前说明鸿蒙侧的 Flutter 支持是由社区和操作系统生态共同维护的版本跟随速度未必比得上官方渠道所以工程配置里要留出升级缓冲。2. 统计口径与扫描器设计一行代码如何被正确“数”出来2.1 统计规则的边界空行、注释、字符串、生成物行数统计的难点不在遍历文件而在“怎么定义一行有效代码”。我们最终把规则收敛为四类边界问题。第一是空行与纯空白行。策略比较简单trim().isEmpty就跳过不用管行尾是否有空格或者 CRLF。但要注意 BOM 头UTF-8 BOM 是三个不可见字符会把第一行变成“非空”所以读文件时必须显式跳过 BOM。第二是注释识别。单行注释按语言区分ArkTS 和 Java 用//C/C 也支持//和/* */Dart 额外有///文档注释。我们只做“可解释的近似识别”遇到//且不在字符串里就忽略遇到/*进入块注释状态直到*/结束期间所有行都算注释不计入有效行。字符串内容里的//和/*不能误判这是个容易踩的细节。第三是多行字符串和模板字符串。ArkTS 支持反引号模板字符串Dart 支持三引号字符串C 还有 raw string。对于模板字符串我们采用“进入引号状态后持续累计直到匹配到闭合引号为止”的方式处理。这里不追求 100% 精确但要求在门禁场景下不能有系统性偏差。第四是生成物排除。鸿蒙工程里build、.hvigor、oh_modules、.ohpm这些目录会生成大量文件如果不排除统计数字会被自动生成的代码淹没。我们内置了默认排除列表同时允许通过参数追加自定义忽略目录。团队规范里还会额外提醒凡是生成的.d.ts、或格式化产物目录原则上都不进统计口径。2.2 扫描器结构同步遍历加异步按行处理扫描器的实现分两层。第一层是目录遍历负责找出所有符合后缀白名单的文件第二层是文件解析负责逐行喂给规则引擎。目录遍历我们用递归实现但对符号链接单独做了防环处理用一个visited集合记录已经访问过的真实路径防止link循环导致死循环。对普通项目和鸿蒙大型工程来说这个递归深度通常不会成为问题但保险起见还是设置了最大深度限制。文件解析这里有一个性能取舍用File.readAsLines()一次把整个文件读进内存最省事但遇到几 MB 级别的自动生成文件会瞬间拉高内存占用。后来改成按行 Stream 处理逐行进入统计状态机内存占用非常平稳。反正我们最终只需要聚合计数不需要保留整文件内容Stream 读取是性价比最高的选择。对于超大仓库我们引入了固定数量线程的并发扫描池。注意Dart 的Isolate适合 CPU 密集型解析但文件 IO 其实用异步 Future 并发就够了。我们按文件数分片每片最多并发 8 个文件解析任务实测在 5000 文件规模下耗时表现稳定。千万不要创建无上限的并发任务否则文件句柄会先耗尽。2.3 “可解释”的报告格式为什么选 JSON 而非纯文本第一版 loc_checker 输出的是表格文本后来很快后悔了。因为纯文本只能给人看CI 脚本想判断“ArkTS 文件行数是否超标”还得再写字符串解析非常脆弱。我们最终定了三层报告结构summary记录总行数、有效行数、注释行数、空行数和文件数byType按扩展名聚合每种语言的行数files列出行数异常的 Top 文件清单比如单文件有效行数超过 2000 的文件。三层数据全部输出为 JSON同时通过命令行打印一个缩小版摘要给人眼看。格式调整的收益在接质量门禁时直接体现出来了。CI 拿到 JSON 后可以用jq直接取值比如判断summary.effectiveLines是否超过阈值比文本正则可靠太多。从这个教训出发我更推荐所有工具类项目先把“机器可读输出契约”定好再考虑人看的展示格式。3. 鸿蒙化适配实操从 Flutter 工程到 HAP 的运行链路3.1 搭建鸿蒙侧的 Flutter 工程基础鸿蒙化适配的第一步不是写代码而是把环境跑通。我们在本机安装 DevEco Studio配上 HarmonyOS SDK同时把鸿蒙侧维护的 Flutter 引擎源码拉到本地。这个引擎分支本质上是把 Flutter 的 Dart 运行时嵌入鸿蒙运行环境工程量很大但对我们这种应用开发者来说更像是“换个宿主环境跑同一套 Flutter 代码”。工程侧的具体做法是在现有 Flutter 项目旁边增加鸿蒙工程壳把 Flutter 模块作为依赖打进去然后构建出 HAP 包。这里的关键是版本对齐鸿蒙侧 Flutter 引擎、Dart SDK、Flutter SDK 必须匹配混版本大概率会遇到莫名其妙的白屏或插件注册失败。我的建议是先跑通官方示例再动业务代码别一上来就把 loc_checker 整个塞进去。从验证路径上看HAP 包既可以在模拟器上跑也可以在真机上跑。工具类应用对权限要求少基本不会遇到签名和权限申请的大坑。但文件访问策略特别值得注意鸿蒙对应用沙箱的管理比桌面系统严格得多后面单独说。3.2 Dart 代码在哪里“踩了鸿蒙的坑”这套适配过程里最典型的坑来自dart:io的行为差异。Dart 的核心库在鸿蒙侧引擎上是支持的但“文件路径”的语义不完全一致。我们在桌面 Linux 上使用的绝对路径/home/user/project在鸿蒙 App 沙箱里不存在必须先通过平台通道拿到鸿蒙侧的应用沙箱路径再让 Dart 层基于这个基址拼接目录。另一个坑是目录遍历时遇到 inaccessible 目录会抛出异常。在桌面端权限不足通常直接报错但在鸿蒙沙箱里有些系统目录是你根本无权访问的连读取目录列表都会返回空或抛PathAccessException。我们统一用try/catch包裹读取操作并记录skippedDirectories列表避免一个目录坏了整次扫描。还有一个细节是换行符。鸿蒙侧的文本文件可能来自 Windows 协作环境带着 CRLF我们的规则里trim()已经抹平了影响但如果你直接在统计逻辑里用split(\n)CR 会残留到字段尾部可能污染后续的拓展统计。所以统一先做\r剥离是最省心的。3.3 平台通道补齐原生能力虽然大部分能力在 Dart 层就能完成但“让用户选择一个本地目录”这类原生能力必须走通道。我们用了两种通道MethodChannel负责一次性调用比如弹出目录选择器并返回路径EventChannel负责流式回调比如扫描过程中把当前进度推给 UI。在鸿蒙侧需要用 ArkTS 实现对应的 ChannelHandler并注册到 Flutter 引擎的插件注册表里。这里最值得分享的经验是通道协议一定要先定好而且错误处理要完整。比如用户取消目录选择原生侧应该返回一个特定的取消码Dart 侧不能把它当成路径字符串解析。我们早期版本就吃了这个亏原生侧返回空字符串时 Dart 侧直接抛异常后来约定了{code: -1, path: }统一错误结构问题才消停。如果后续要支持鸿蒙平板的桌面模式还得考虑多窗口场景下的路径记忆。这类需求在普通手机上不存在但平板上用户可能同时开着文件管理器拿路径这时候 App 侧缓存上次路径就非常重要了。我们加了本地配置持久化不算复杂但体验提升明显。3.4 性能实测1000 文件目录的数据表现鸿蒙化适配完成后我们用团队的真实 ArkTS 工程做了压测结果如下。场景文件数有效代码量首次扫描耗时内存占用峰值小型模块432.1 万行0.8 秒约 180 MB中型应用61218.6 万行3.4 秒约 420 MB大型工程含生成物526091.2 万行11.7 秒约 900 MB首测时扫描 5260 文件的耗时接近 30 秒排查后发现瓶颈是逐文件读取时没有复用File句柄导致打开关闭开销放大。优化方向是保持合理的并发度并把重复的目录状态缓存起来。最终 11 秒的成绩虽然比 tokei 慢但考虑到数据还包含 String 规则判断和文件路径归类这个水平已经可接受。从内存角度也验证了 Stream 按行处理的必要性。如果换成全量读入再解析5000 文件场景内存大概率超过 1.5 GB在鸿蒙设备上很容易被杀后台。现在峰值控制在 900 MB 以内普通测试机跑得动。4. 质量门禁集成把统计结果变成一条不可妥协的规则4.1 输出契约先行report.json 与 exit code质量门禁最重要的是“确定性”。我们在 loc_checker 里强制规定扫描完成后必须生成report.json同时进程的 exit code 必须能反映门禁结果。具体约定如下。正常扫描完成且所有指标通过阈值时exit code 为 0扫描完成但阈值触发时exit code 为 1扫描本身失败、目录不存在、读取异常时exit code 为 2。CI 侧可以只关心这三位 exit code完全不用去解析 key 判断业务语义。为了让人工也能快速定位命令行摘要里会打印触发优先级的第一个告警项例如“ArkTS 单文件行数 2150 超过 2000 上限”。如果想让门禁更细report.json里还可以增加thresholdChecked字段把此次检查启用的阈值和实际值全部列出来。这样即使后续调整规则历史报告也保留可追溯性不会出现“上周同一份代码今天结果不同”的困惑。4.2 结合 git diff 做“增量行数”门禁总量统计只能治标真正波动大的是增量。我们在 CI 流水线里配合git diff做增量行数判断思路也很简单先定位 target branch 和当前分支的 merge base再用git diff --numstat拿到变更文件列表交给 loc_checker 的过滤模式处理。git diff --numstat的输出格式是“新增行数、删除行数、文件名”对这些行数求和就行。但要注意一个细节二进制文件的行数是-需要明确跳过。第二个细节是 git 的 diff 默认按解析后的文本行统计对于自动格式化生成的一长串 JSON某些文件会变成“一行改动几万字符”的极端数据。我们后来增加了“单文件增量行数上限”超过 2000 行的文件单独标记为需要人工复核避免这类极端文件绕过评审。结合增量和存量之后质量门禁可以同时看两个方向总量防止库存膨胀失控增量防止单次提交引入过量债务。比较有效的做法是当 PR 的新增有效行数超过 800 行时系统自动要求上传覆盖率报告或补充分层评审说明。这种规则给了开发者明确的预期不搞突然袭击。4.3 阈值怎么定才能避免被开发同学吐槽初版门禁我们拍脑袋定了“单文件不超过 1000 行”结果上线第一天就误伤了几个历史遗留的核心模块开发同学直接炸锅。后面改用“基于历史分位数的动态阈值”而不是静态定死。具体做法是统计最近 30 天各文件有效行数分布取 95 分位作为建议阈值再给团队留 20% 的冗余空间。动态阈值不是每次跑都变那样没有一致性。我们允许在配置文件里固化某一周的统计基线然后按版本迭代逐周微调。这样既避免拍脑袋也保留稳定可预期的门禁规则。门禁规则还需要考虑豁免通道。比如“重构类代码一次性迁移 3000 行”、低风险文档样例的搬入搬出这些应该走人工审批而不是直接卡死流水线。我们最终的策略是默认全开规则文件里提供allowlist注释注明豁免人和豁免期限。这类机制虽然增加了一点管理成本但换来了开发团队的配合意愿。5. 常见问题与排查技巧实录5.1 统计结果和 cloc 对不上口径差异我几乎确定每个接入工具的人都会遇到一次“为什么 loc_checker 和 cloc 数字不一样”的质疑。原因通常集中在三处默认忽略目录不同、多行字符串的识别深度不同、以及注释规则的边界不同。cloc 对语言定义更全但我们更关注 ArkTS 专属场景和企业自定义规则所以数字一定有差异。解决方式不是追求“一模一样”而是把口径文档化并在报告里明确写出“本报告采用 loc_checker 团队定义的统计规范 2.0”。只要数字稳定复现用什么口径是团队自己的选择。反过来如果两个工具数字若即若离反而说明口径有 bug。5.2 鸿蒙模拟器与真机上的文件访问不一致我们在模拟器上调试时选目录功能一切正常换到真机后发现目录选择器弹不出任何内容。排查半天发现是模拟器拥有较宽松的媒体文件访问权限而真机要求先声明文件管理权限并且用户要在系统设置里手动开启“所有文件访问”能力。这个问题在适配阶段非常典型因为 Flutter 层代码不报错只是原生侧获取到的 ContentProvider 结果为空。遇到这类情况第一件事就是检查鸿蒙工程的module.json5权限声明确认权限配置和用户授权状态。切记不要把调试环境的权限经验直接搬到线上真机的权限控制会严格得多。我们最终的处理是扫描本地目录用沙箱路径扫描外部目录时明确引导用户授权并用清晰的错误弹窗代替静默失败。5.3 超大目录扫描卡死的瓶颈定位当统计时间从 3 秒长到 30 秒大部分人第一反应是“解析逻辑太慢”其实多数情况是 IO 并发模型出了问题。我们用--debug模式打开栅栏日志后发现阻塞点在遍历阶段每发现一个文件就await File.length()一次产生了上万次串行异步往返。优化方式是合并批量请求或者在遍历时直接读取文件大小减少系统调用次数。另一个隐藏瓶颈是完整的path.realpath调用。每次遇到符号链接都解析真实路径会对性能造成明显拖累。改成只在进入新目录时判断一次符号链接属性命中后再做防环记录速度立刻提升。大目录优化这种事一定要先 profile 再动手可千万别靠猜。5.4 门禁误杀风险如何保住团队信任质量门禁最大的敌人不是绕过程序而是误杀率过高导致开发同学产生对抗心理。我们有一次在阈值配置里误把“总行数上限”写成了“单文件行数上限”导致一个 3000 行历史文件每次 PR 必挂两天后几乎所有相关团队都在吐槽工具。复盘后的改进有两点第一上线前必须用最近一批真实历史 PR 做回放确认旧提交的通过率不低于 95%第二凡是被门禁拦截的失败结果报告里必须给出可读的原因和修改建议而不是冷冰冰的 exit code。工具越透明团队越愿意配合。到现在loc_checker 已经是发布流程里大家默认会主动跑一遍的必选项不是因为强制而是因为它的结论一直靠谱。最后再说一个偏实践的小技巧如果你计划在团队里落地类似的效能工具不要先急着做大而全的界面而是先把输入输出契约、统计口径、阈值策略这三件事钉死在文档里。哪怕第一版只有一个干巴巴的命令行它的数字和规则只要可信后面慢慢加 UI、加鸿蒙端、加趋势报表都会非常顺。毕竟工具的本质不是炫技而是让工程判断有据可依。