资讯动态

Flutter鸿蒙工程死代码清理:用dead_code_analyzer精简包体积与治理技术债

发布时间:2026/10/6 8:59:45 来源:尧图企业网站定制
1. 为什么我会盯上 dead_code_analyzer鸿蒙包体积的真实痛点做 Flutter 鸿蒙跨端项目的朋友应该都躲不开“包体积告警”这件事。我最近在给一个 Flutter 鸿蒙工程做体积治理把三方库 dead_code_analyzer 引入到鸿蒙化构建链路里做无用代码扫描和删除目标很直接精简鸿蒙产物包体积提升工程纯净度。整套动作做完之后我发现它不只是省了那几 MB更关键的是让工程重新变得“可读”了——这比省体积本身更值钱。1.1 从一次构建失败说起事情的起因是我们交付前的构建矩阵里鸿蒙侧产物包连续报了体积上限。当时团队的第一反应是压图片、压资源、关掉调试输出结果折腾了两天体积只降了几个百分点。后来我把构建产物拆开看发现里面有大量旧的 feature 代码一个做了一半的下单扩展模块、两套历史协议解析器、还有一块早就被产品下线的积分兑换入口。它们没有被任何入口引用但源码还在工程里编译进 libapp.so 之后就是实打实的死重。鸿蒙侧产物和 Android/iOS 不一样的是它除了 Flutter 引擎 so 之外还有 ArkTS 字节码和各类 har/hsp 分包。如果你只是从 Flutter 层做 tree shaking根本照顾不到 ArkTS 侧的历史包袱。而 dead_code_analyzer 这个三方库能先把 Dart 那半边扫清再把桥接文件、不可达页面、失效协议解析器一次性暴露出来。配合鸿蒙侧的方舟编译器才能做到真正“两侧同时瘦身”。这个过程适合谁参考凡是维护跨端工程超过半年、被包体积困扰、或者想给大型 Flutter 鸿蒙项目梳理技术债的工程师都应该把死代码清理纳入日常治理。不要等到交付期再临时抱佛脚。1.2 死代码是从哪混进来的静态分析工具能帮你找到死代码但你要先理解它为什么会出现否则就算清完一波下一波马上又堆上来。以我手上这个工程为例死代码的典型来源基本可以归成下面几类历史 feature 下线业务开关切掉后代码没有同步删除哪怕以后再也不用。组件通信残留以前用某套通道协议做过页面间通信后来换成了状态管理库旧协议的 handler 和 model 还躺在工程里。动态派发和反射很多框架为了支持运行时扩展会注册一些类但这些类实际上从来没有被调用。自动生成代码json 序列化、路由表生成器跑出来的中间产物更新换代后旧版本没有覆盖删除。调试面板本地调试用的工具页、性能监控浮层上线时只做了视觉隐藏没做条件编译。你会发现这类代码最恶心的地方在于它们不报错、不冲突也没有明显坏味道编译器不会优化掉它们因为 Dart 层可能是可达的。比如某个类被export了但它没有任何调用方又比如一个函数被visibleForTesting标记然后所有测试文件都被删了这个函数就成了“看起来活着实际上没人用”的代码。1.3 三方库能帮到哪一步dead_code_analyzer 做的事说穿了就是静态可达性分析。它从你指定的入口文件出发构建一整张调用图把所有能被走到、被实例化、被反射注册的代码标记为 live其余的全部列进 dead 报告。它不会自己删文件但会清清楚楚告诉你哪里该删。不过我一开始也天真了以为把工具加进 pubspec 就能像 Android 的 R8 一样自动缩减。实际做下来“鸿蒙化适配”这四个字里全是细节鸿蒙工程的目录结构和 Flutter 纯工程不一样、Dart 工具链的路径需要通过定制 SDK 去适配、ArkTS 桥接侧的联动清理需要自定义脚本。所以这篇指南不会只讲“怎么装依赖”而是用一条完整链路告诉你怎么把 dead_code_analyzer 真正嵌入到鸿蒙工程的日常开发与 CI 流程里。2. 先搞懂机制再动手可达性分析的关键概念在动手扫描之前我强烈建议先花半小时理解它的分析模型。因为只有理解了规则你才能写出靠谱的 keep 白名单也才能避免误删生产代码。任何静态死代码分析都不是“万无一失”的但我们可以通过配置和人工复核把风险压到最低。2.1 什么是“可达”达性这个概念可以拿供电线路来打比方。你从总闸入口 main 函数出发沿着每一根电线函数调用、类实例化、getter 访问、构造器执行往下走凡是能通电的灯都算 live那些没有连到总闸的支路即使线路本身完整也不会亮这就是 dead。工具的实际做法是解析整个工程的 AST结合 Dart 的类型系统从你配置的 entry_points 开始遍历调用关系。每遇到一个函数调用就把它指向的函数加入 live 集合每遇到一个构造器调用就把对应类加入 live 集合。处理完整张调用图之后剩下没有进入 live 集合的顶层函数、类、成员变量就会被列进死代码报告。这里有个前提需要讲清楚分析器处理的是“静态可达性”它无法模拟真正的运行时调度。如果你在代码里通过字符串拼了一个类名再用反射去实例化分析器是看不见这个调用的。它会认为那个类是死的。同理如果你在配置文件里注册了很多路由但路由页面是通过字符串routeName去动态查找的分析器也追踪不到。这个特性决定了我们后面必须给工具配一个白名单机制。2.2 分析器最怕误杀的三类场景第一类是用反射创建的对象。Dart 里常见的generateJson序列化代码就属于这一类fromJson经常会被jsonDecode后的动态类型调用在字符串拼接的映射里分析器无法判断这个构造器是否真的被用到。真删掉的后果就是线上接口返回后解析崩掉。第二类是方法通道注册。Flutter 和鸿蒙原生侧的通信通常会走 MethodChannel但注册 handler 的代码往往在 init 阶段就会被加载。如果这个注册动作写在一个没有被入口调用的类里分析器会标记为 dead。可实际上鸿蒙侧的 PlatformView 或原生模块是通过通道名反向找 handler 的你一旦删除运行时就会找不到处理器。第三类是路由表和插件注册表。很多团队会把页面类统一写进一个路由配置 list再通过PushRoute(name)跳转。这种情况下list 里引用了页面类分析器会认为是可达的但如果你的路由实现是“只存字符串不存类引用”那页面类又会被标记为 dead。前面提到过的热词诸如 PlatformView、MethodChannel在这种场景下都是重灾区。所以我在实际操作中定了一条铁律所有涉及反射、通道、动态注册的代码在配置里必须显式 keep宁可错留不可错删。工具给出的报告只是线索不是判决书。2.3 和鸿蒙 ArkTS 死代码清理的分工鸿蒙产物包的构成比 Flutter 单端复杂。外面是 hap/hsp 外壳里面既包括 ArkTS 编译后的字节码又包括 Flutter 引擎运行时和 Dart 编译产物。dead_code_analyzer 只能作用在 Dart 侧ArkTS 侧的冗余 import、无用页面组件、过期的接口定义它完全看不见。所以你需要一套分工逻辑Dart 侧由 dead_code_analyzer 负责扫描产出不可达代码清单。ArkTS 侧交给方舟编译器的摇树机制处理一部分明显死代码但历史包袱仍然要靠人工查看。桥接层这是最关键的一环。Flutter 与鸿蒙原生通信的 NativeChannel、IDL 生成的接口、事件通道封装往往横跨两侧。假如 Dart 侧某个协议类已经被判定为 dead 并删除那 ArkTS 侧对应的桥接实现也就失去存在意义需要手动或半自动清理否则它迟早成为下一个死代码。这也是“鸿蒙化适配”和“单纯给 Flutter 工程装个插件”之间的本质差别你必须把分析维度从单一 Dart 侧扩展到 ArkTS 侧和桥接层才能拿到真正意义上的体积收益。3. 鸿蒙化适配三步走接入、配置、清理落地这一节是全文最实操的部分。我按完整流程逐步拆解过程中会用到我实际项目里的目录结构和命令你照着改路径和工程名就能跑。再次强调不同版本的 dead_code_analyzer API 可能略有差异配置字段名也要以你装的版本为准我给的是一套在当前主流版本上验证过的方案。3.1 在鸿蒙 Flutter 工程里接入工具鸿蒙 Flutter 工程一般的组织方式是宿主工程是 DevEco 工程里面通过 Module 方式引入 Flutter 侧工程Flutter 工程依旧保留 pubspec.yaml。第一步就是在pubspec.yaml的dev_dependencies里加上 dead_code_analyzer。dev_dependencies: dead_code_analyzer: ^x.y.z然后执行flutter pub get。这里有个坑鸿蒙定制的 Flutter SDK 不一定会把dart可执行文件注册到系统 PATH 里。如果你直接用dart pub global run dead_code_analyzer报了找不到命令先不要急着怀疑配置先确认你用的是哪个 Flutter SDK。我的做法是统一用 fvm 来管理 Flutter SDK 版本并把 fvm 的缓存路径显式写进脚本。这样无论是在本地开发机还是 CI 容器里都能定位到同一个 Dart 工具链。如果你团队里没有引入 fvm可以临时用 SDK 内的绝对路径去跑但一定要写在文档里不然下个同事接手又会踩一遍。3.2 写好符合工程现状的配置文件dead_code_analyzer 的配置文件我习惯放在工程根目录命名随意只要脚本里能引用到就行。核心要义是四个字段入口、排除目录、保留规则、报告输出。# 分析配置示例字段名以实际版本为准 entry_points: - lib/main.dart exclude: - build/** - .dart_tool/** - oh_modules/** - integration_test/** keep: - **/channel/** - **/router/** - **/generated/** - **/*Handler* report: enabled: true format: json output_path: build/dead_code_report.json逐个字段解释一下entry_points告诉分析器从哪几个文件开始算可达。跨端工程一般就是lib/main.dart如果你的工程有多入口比如独立运行的小工具页就把它们都列上。exclude排除掉构建产物和依赖目录。鸿蒙工程里最需要注意的是oh_modules目录它里面是 ohpm 拉下来的原生依赖分析器如果跑到这里不仅慢还会把大量鸿蒙侧源码误判成 Dart 工程的一部分。keep白名单规则支持通配符。前面说的反射、MethodChannel、动态路由全部挂在这里。我建议项目里所有和原生通信相关的目录一开始就全白名单化后续逐步精确到文件。report输出 JSON 报告方便脚本二次处理。这个字段如果不支持可以直接让 CLI 输出 stdout 然后自己落盘逻辑是一样的。3.3 扫描、出报告、删代码有了配置文件接着就是跑扫描。直接执行命令把报告输出到指定目录dart run dead_code_analyzer analyze --config dead_code_config.yaml第一次跑完报告通常大得吓人。我那次出来 400 多个 dead 节点包括类、函数、字段和整个文件。先不要动手删把报告按文件路径聚类之后开启逐个 confirm 模式。我的执行策略是这样的先把报告中明确指向某个历史模块的节点挑出来比如modules/expired_coupon/**这种可以放心批量删。再把所有命中了 keep 规则的记录点开看一眼确认工具没有误标。最后是灰色地带看起来没人调用但代码里存在字符串匹配的类我先注释掉等一轮完整回归测试后再决定去留。删除方式上对单个文件可以直接删文件对零散函数建议在源码里手动删因为无脑删行容易破坏注释和字符串结构。如果你是工程洁癖也可以写一个辅助脚本读报告自动对标记为“整个文件 dead”的路径执行删除。但我会在脚本里加一个--dry-run参数先把准备删的文件列表打出来给团队 review。python tools/remove_dead_files.py --report build/dead_code_report.json --dry-run这一步的价值在于让机器给出建议让人做最终决定。我见过不少团队为了省事直接全自动跑删除结果把还在灰度中的代码清理掉了线上事故之后又把工具关掉不用。正确的姿势是宁可慢也不要让自动化把信任感消耗掉。3.4 一个实际清理案例我们项目里最终清掉的东西大概有这么几类一个被下线半年的积分兑换页整个目录删除一个旧的组件通信协议包删除 Dart 侧 handler再清 ArkTS 侧对应桥接类三个永远不可能被触发的调试面板。清理完成后我专门对比了构建产物清理前鸿蒙 hap 包体积98.6 MB清理后鸿蒙 hap 包体积91.2 MB缩减比例约 7.5%这个数据在包体积治理方向上已经非常可观。更惊喜的是Flutter 侧重新flutter analyze之后报的 warning 数量从 37 个降到 19 个很多“未使用的字段”和“不存在的 import”本来就是因为死代码导致的。工程纯净度直接体现在了静态检查结果上。4. 打通鸿蒙产物验证链路让清理结果可度量清理完之后千万别直接合代码。你要做的是把“包体积基线”和“清理门禁”建立起来否则下一次重构又会把死代码堆回来。这一节我不讲算法只讲怎么把清理固化到流程里。4.1 把清理结果固化成 CI gate我建议在 CI 里加一个独立的 job专门跑死代码扫描。逻辑很简单分析器输出 dead 节点数量如果超过一个配置阈值就让构建失败。阈值一开始可以设得很大比如 200目的是先让大家养成看报告的习惯稳定运行几周后再逐步调低到 50、20。核心脚本大概长这样#!/bin/bash set -e dart run dead_code_analyzer analyze \ --config dead_code_config.yaml \ --output build/dead_code_report.json DEAD_NODES$(python tools/count_dead_nodes.py build/dead_code_report.json) THRESHOLD${DEAD_CODE_THRESHOLD:-50} echo dead nodes: $DEAD_NODES, threshold: $THRESHOLD if [ $DEAD_NODES -gt $THRESHOLD ]; then echo dead code exceeds threshold, build failed. exit 1 fi这样做最大的好处是把“清理无用代码”从一次性运动变成了持续约束。哪个 MR 里又躺进来一堆历史实验代码CI 会第一时间揪出来。4.2 从 so 和 har 侧验证体积变化如果一个 Flutter 鸿蒙工程里既有 hap 又有 hsp/har清理前后的体积对比不能只看最外层包。更细的验证方式是构建一次 Flutter 产物把 libapp.so、libflutter.so 和 ArkTS 的 bytecode 包分别列大小保存基线。清理后重新构建看每个产物的变化幅度。我这里放一个简化版的对比表读者可以参照这个思路自己在工程里做记录产物清理前清理后变化hap 整体包98.6 MB91.2 MB-7.5%libapp.so45.0 MB38.5 MB-14.4%ArkTS 字节码产物12.3 MB11.8 MB-4.1%Flutter 引擎 so35.1 MB35.1 MB0.0%表里最后一行很有参考价值引擎 so 基本不变。原因是 Flutter 引擎、Impeller 渲染后端这些底层库体积占比很大它们不属于业务死代码再怎么清也不会变。很多人以为用 dead_code_analyzer 能减掉所有体积一看到引擎 so 没动就认为工具没用。实际上你的优化空间本来就在 Dart 业务代码和 ArkTS 侧不在引擎层。另外如果你接的是 flutter aar/har 这种打包集成模式需要注意打包后第三方依赖里的类也可能被不当裁掉。老项目里常见的做法是打 har 时把整个 Flutter 模块压缩成一个文件然后宿主工程再做依赖剪裁。这种情况下死代码报告必须和 har 的裁剪配置结合起来看否则你在 Dart 侧删了har 里的旧符号还在包体积并不会真正降下来。4.3 联动清理 ArkTS 桥接代码Dart 侧删完之后一定要追着桥接层继续清。Flutter 鸿蒙化项目中桥接文件通常集中在原生侧的IDL或MiniProgramBridge这类目录下。Dart 侧删除一个协议类对应 ArkTS 侧往往还留着一个Implements类里面有几十个已经不会再被调用的接口方法。我常用的方法是写一个脚本从 report.json 里提取“已删除 Dart 侧 Channel 名”再到 ArkTS 目录下全局搜索对应的 Channel 字符串。凡是搜不到任何调用的桥接方法基本可以确认是残留。逐个确认后删掉再同步删除externals目录里对应的 IDL 声明。这一步做完ArkTS 侧产物体积才会继续向下走。清理桥接层最常见的意外是Dart 侧确实不再用了但另一个 ArkTS 模块还在通过事件通道订阅它的回调。如果你删得急了运行时就会出现 Channel not found 之类的错误。所以每清一个桥接类都要全局搜一遍“这个 Channel 名字”是否还出现在别的模块里再决定动不动。5. 常见问题与排查速查表这部分我把我实际踩过的坑和网上高频讨论的问题整理成一个速查表方便你遇到对应症状时快速定位。症状 / 报错大概率原因处理办法某个 Function 从未被调用但删除后界面异常它是事件回调注册发生在静态分析不可见的异步流程里把对应文件加入 keep 白名单重新跑分析删除后e/flutter (31173): unhandled exception初始化顺序被破坏某个早期启动依赖被误判为 dead用git diff回退删除项改为只标记不删除先跑回归测试Future 的 then 回调不触发你把和事件循环调度相关的代码误删了恢复相关文件对 async/await 密集区域不要做激进清理删完代码后包体积没有明显下降清理的是 Dart 源码但打包脚本没被触发检查 CI 是否用了缓存构建删除构建缓存重跑一次分析报告里出现大量oh_modules内容配置文件没加 exclude在 exclude 里补上oh_modules/**运行时报 MethodChannel 没有实现对应的 handler 被当成死代码“优化”了检查 keep 规则是否完整覆盖所有 channel 目录5.1 热词关联问题异步与动态注册最容易误报当前端社区讨论 Flutter 的时候常提到“Future 的 then 回调是放入微任务队列吗”这类问题。它和死代码分析有什么关系我解释一下如果你清理掉一段负责启动异步调度的代码后续通过then注册的回调链会因为没有入口触发而静默失效。分析器看不到运行时微任务队列的调度关系它只会认为“那个 then 回调函数体没有直接的静态调用者”于是标成 dead。所以我在清理时专门留了一条规则凡是文件顶部出现过dart:async、Stream、Completer、Microtask的模块默认不进批量删除清单。等所有同步可达代码清理完再手动逐个去 review 这些异步模块。5.2 误删代码、构建挂了怎么办误删恢复这件事团队里一定要有预案不能等到事故发生了再去翻备份。我的建议是每次清理前先基于当前主干开一个独立分支跑完分析和删除后不急着合并先在这条分支上执行完整的构建与自动化测试。一旦发现问题直接放弃该分支重新从主干切分支再试。还有一个更细的推荐在跑删除脚本前让脚本先生成一个deleted_files.patch把删除的内容留在 Git 仓库外部。这样就算分支被合并后又发现新问题你也可以用 patch 把关键文件捞回来不用靠写回忆去恢复代码。5.3 几个长期有效的避坑心得第一清理是持续工作不是一次性运动。让 CI 每周自动跑一次分析报告并抄送整个研发群其实是很有效的“软约束”。第二keep 白名单是活的。每新增一个动态注册的模块都要同步更新配置文件否则下一轮分析就有误报。可以把白名单变更纳入 code review 的关注点。第三清理完请顺手做一次依赖梳理。很多死代码删除后pubspec.yaml里对应的第三方依赖也可以去除。少一个依赖就少一次鸿蒙侧 ohpm 转换的工作量。这个联动收益常常被忽略但效果非常好。第四千万别让工具“一键删除”。我在团队里反复强调机器给报告人做决定。把工具的定位放在“分析辅助”而不是“自动执行”上工具的价值才能被最大化同时也避免工具被误判为“危险品”而遭到弃用。我个人在实际操作中最大的体会是死代码清理带来的不只是包体积数字的变化它会让工程里那些被隐藏的架构问题暴露出来。你顺着报告往下翻常常能发现“这个协议已经没人用了”“这个模块的配置中心早就不存在了”“这里本该拆掉只是没人敢动”。一旦把这些腐烂的节点全部修掉后续再改功能、加实验手感都会完全不一样。所以如果你正被鸿蒙产物包体积和工程复杂度压得头疼不要只盯着压缩图片和混淆开关先从 dead_code_analyzer 这份报告开始它会告诉你病根到底在哪。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑