Flutter迁移原生双端架构复盘:71.6万行到112.5万行的经验教训
71.6 万行 Flutter 代码最后重写成了 112.5 万行原生双端。这个架构迁移项目加上前期评估和灰度我们整整做了一年。作为从头跟到尾的技术负责人我想把这次 AI 驱动的迁移完整复盘一遍。我知道很多人听到这件事的第一反应是代码量变多了这不是倒退吗但真实情况恰恰相反。70 万行 Flutter 阶段我们已经被性能和平台能力卡了很久。核心页面在低端 Android 机型上掉帧直播、地图、后台定位、系统级相机这些能力始终要和原生桥接来回折腾。当业务对每个模块都提出“原生级体验”要求时跨平台方案的红利期基本就结束了。这篇复盘适合三类人看正在纠结要不要从跨平台迁移到原生双端的团队想搞清楚 AI 到底能在架构迁移里做什么、不能做什么的人以及吃够了 Flutter 打包部署和平台适配苦头、想换条路继续折腾的移动端工程师。我会把决策逻辑、代码盘点方法、四阶段迁移流程、踩过的坑和最后的数据对比全部摊开讲。1. 为什么非迁不可71.6 万行背后的真实瓶颈1.1 触发迁移的三个信号这个决定不是拍脑袋做的而是三个信号叠加在一起逼着我们不得不动。第一个信号是性能衰减。App 到了 70 万行这个量级后Flutter 引擎层的一些开销开始被放大。核心信息流在 iOS 上还能勉强维持流畅到了 Android 中低端机器上滚动帧率波动明显滑动跟手度和原生应用一比就有差距。内存峰值也经常顶到 iOS 的 jetsam 阈值附近时不时被系统杀掉。启动时间更是一点点变长引擎初始化、Dart isolate 启动、首帧渲染这些环节在低端机上的耗时直接影响了用户留存。第二个信号是平台能力受阻。我们的业务涉及低功耗蓝牙、持续定位、音视频推流、系统级相机调用。每接入一个原生能力就要写一次 MethodChannel 插件维护成本翻倍不说遇到系统版本更新还要两边同时改。Flutter 层的 UI 线程和原生线程之间的协调也很费劲前后台切换、生命周期管理、系统弹窗交互都要额外处理开发效率远没有当初想象得那么高。第三个信号来自团队结构。Flutter 的高水平人才本来就难招而原生 Android、iOS 工程师的梯队相对稳定。跨平台方案在创业初期帮我们快速验证了业务但到了需要精细化打磨性能和体验的阶段原生技术栈的确定性和人才储备优势就体现出来了。这三个信号叠加在一起结论已经很清楚继续在 Flutter 上打补丁成本会越来越高。长痛不如短痛我们决定启动双端原生迁移。1.2 为什么是双端原生而不是 RN 或 Kotlin Multiplatform既然决定离开 Flutter那替换方案就需要认真比较。当时主要看了三个方向React Native、Kotlin Multiplatform、还有直接做双端原生。React Native 我们直接排除了。它的动态化能力确实强但性能天花板比 Flutter 还低一些而且桥接层的复杂度同样不低。如果只是为了解决性能问题从 Flutter 迁到 RN 属于换汤不换药那些平台能力适配的坑一个都不会少。Kotlin Multiplatform 当时也评估过。它的思路是共享业务逻辑层UI 层依然要双端各自写。这样听起来很美但对我们的存量项目来说意味着要把已有 Flutter 业务代码重新拆分成共享逻辑和 UI 两层中间需要引入新的抽象边界。等于在迁移的同时还要做一次领域建模重构风险太大了。最终选择双端原生理由是三个字确定性。Android 用 Kotlin Jetpack ComposeiOS 用 Swift SwiftUI平台能力完全放开性能没有中间层损耗团队招人也好招。代价我们也很清楚代码量会涨双端要各写一遍但这些增加的成本是可控的、可预期的不像是跨平台方案那样存在不确定的隐性成本。1.3 AI 在决策阶段做了什么很多人以为 AI 是在迁移写代码阶段才介入的其实我们在决策阶段就开始用 AI 了。第一件是用 AI 做行业案例调研。我们把公开技术大会上的跨平台迁移案例、同类 App 的选型文章喂给大模型让它总结不同方案在百万行级别项目上的表现、常见问题和团队反馈。这些信息原本要花两三个礼拜去翻资料AI 半天就能给出结构化摘要我们再人工去验证关键数据。第二件是用 AI 辅助生成 POC 原型。我们选了三个最有代表性的业务模块让 AI 分别生成 Flutter、Kotlin、Swift 的实现框架然后对比代码量、可读性和性能表现。虽然 AI 生成的代码不能直接上线但用来做技术验证和成本估算已经足够了。第三件是对存量代码做迁移风险预判。我们用脚本加 AI 的方式对 71.6 万行代码做了模块依赖分析AI 负责识别哪些模块存在较深的跨层调用、哪些模块使用了不常见的 Flutter 特性。这些分析结果直接决定了后面迁移策略的制定。2. 盘点存量代码71.6 万行到底包含什么2.1 代码构成审计动手迁移之前第一步是搞清楚这 71.6 万行代码到底由什么组成。我们做了个粗略的代码构成审计。从文件类型看Dart 业务代码占六成左右其余包括自动生成的 model、JSON 序列化代码、pubspec 里引用的第三方依赖、以及我们自己写的平台插件。从目录结构看App 壳工程、基础组件库、业务模块三层结构还算清晰但模块之间的依赖关系已经有些混乱了。有的业务模块直接引用了其他业务模块的内部组件这在 Flutter 的 package 体系里很难约束住。我们还统计了第三方包的使用情况。大概有 120 多个直接依赖的包有些是 Flutter 官方维护的有些是社区个人维护的。这些包里面一部分可以很容易在原生端找到替代比如网络库、图片加载库另一部分则比较难替代比如涉及底层渲染、自定义绘制、复杂动画的包这些在原生端往往需要重写。这个审计过程听起来枯燥但非常重要。如果不搞清楚存量代码的构成直接开写后面一定会被各种“意外惊喜”打断。2.2 用 AI 生成代码资产清单人工去读 70 万行代码做盘点不现实我们用 AI 搭了一套半自动的代码分析流水线。先写脚本扫描整个仓库获取所有 Dart 文件的 AST抽象语法树。然后把 AST 数据丢给大模型让它按模块维度做语义聚类。AI 会把代码划分成数据层、网络层、状态管理、UI 组件、工具方法等类别同时标出每个模块间的引用关系。这里要说一下AI 做语义理解比传统静态分析工具强很多。传统工具能告诉你 A 模块引用了 B 模块但 AI 能进一步分析出这种引用是业务强依赖还是可替代的弱依赖能判断出某个工具方法到底是核心链路在用还是只有两三个边缘页面在用。最后我们把所有模块按“可迁移性”和“业务重要性”两个维度打标生成了一份迁移清单。这份清单是后续所有排期和分工的基础包含 46 个业务模块、12 个基础组件库、3 个平台插件。2.3 三类模块的差异化迁移策略盘点完成后我们把所有模块分成三个等级对应三种完全不同的迁移方式。A 类模块可以直接做“代码翻译”。这类主要是数据模型、网络请求封装、纯计算逻辑和 UI 没有耦合。AI 的翻译准确率可以做到很高人工 review 的成本相对低。B 类模块需要“重构式迁移”。这类是业务逻辑和状态管理深度耦合的页面比如订单流转、IM 会话、编辑器。直接用翻译的思维做会死得很惨。我们在迁移时会对业务逻辑重新做梳理保留行为和对外接口但内部实现会按原生的架构模式重新设计。C 类模块是“保留桥接”的存量原生能力。我们之前积累了十几个自研 Flutter 插件涉及蓝牙、定位、混淆加密、设备信息采集等。这些插件本来就是原生代码打包的迁移过程中不需要重写只要调整一下 API 暴露方式让双端原生代码直接调用原来的底层实现。判断一个模块归到哪类我们有一个简单规则有 UI 状态机的一律归 B 类无状态、纯函数式的归 A 类已经是原生实现的归 C 类。这套规则后来越用越顺手新模块进来直接套分类标准不用反复讨论。3. 四阶段迁移流水线AI 在工作流里的真实落点3.1 第一阶段壳工程与双端并行架构迁移策略上我们没有选择“关门重写、写完再替换”这种高风险打法而是采用了渐进式并行架构。第一阶段目标是在 Flutter 应用还正常跑的同时先建好 Android 和 iOS 的原生壳工程。AI 在这一阶段干了两件事生成双端项目骨架代码包括 Gradle 配置、工程目录、基础工具类写打包脚本和自动化测试的初始化代码保证双端工程能持续集成。并行架构的核心是路由层。我们在两个原生壳工程里都做了一套“远程路由映射”根据用户进入的页面决定走 FlutterView 还是原生页面。一开始新功能全部走 Flutter 老路由迁移完成后逐步切换到原生路由。这样每个迁移完成的模块都能立刻上线验证不用等全部写完。这个阶段最需要注意的就是不要贪多求快。壳工程的基础设施没打牢就急着迁业务模块后面会一直被各种工程配置问题拖后腿。3.2 第二阶段数据层与网络层迁移第二阶段先把地基打好数据层和网络层。我们原来的 Flutter 网络层是用 Dio 封装的统一处理了请求头、签名、错误码映射和日志上报。迁移时没有直接用 AI 把 Dio 封装翻译成 OkHttp 或者 URLSession而是先梳理出整个网络层的行为契约请求签名怎么生成、错误码怎么映射、超时重试策略是什么、埋点上报字段有哪些。然后把这份契约同时给到 Android 和 iOS 两条线各自用原生网络栈去实现。这里 AI 的作用是批量翻译数据模型。我们有大量 JSON model 定义AI 可以直接从 Dart class 生成 Kotlin data class 和 Swift Codable struct字段一一对应JSON 解析代码也顺手生成。这一块效率提升非常明显几百个 model 文件人工写可能要几周AI 辅助下两三天就能全部生成我们再逐个 review。接口验证是这阶段最容易出问题的地方。业务同事最关心的就是接口迁移后线上请求响应和原来 Flutter 里面是不是一模一样。我们的做法是双端统一用抓包工具记录线上请求再用自动化脚本对比 Flutter 老包和原生新包的请求报文与响应报文。请求参数顺序、加密字段、默认值缺一个都会暴露出来。3.3 第三阶段业务模块平移与状态管理替换第三阶段是工作量最大的主体阶段把一个个 Flutter 业务模块平移到双端原生。原生的状态管理方案我们分别选了 Android 的 ViewModel StateFlowiOS 的 ObservableObject Combine。Flutter 里的 Provider、ChangeNotifier、Bloc 各个项目风格不一到原生端必须收敛成统一模式。不统一的话双端行为很难对齐后续维护也会很痛苦。这里我放一个我们实际在用的 AI 迁移提示词模板当时实测下来对转换效率提升非常明显你是一名有十年经验的移动端架构师。下面是一段 Flutter/Dart 代码基于 Provider 和 ChangeNotifier 实现 X 模块的业务逻辑。请帮我转换成 Android/Kotlin 实现使用 ViewModel StateFlow保持对外接口和业务行为一致保留全部业务分支和错误处理逻辑。转换后请输出1) Kotlin 代码2) 转换说明3) 无法直接映射的风险点清单。这个模板的核心是把“转换代码”和“识别风险”分开。我们要求 AI 必须输出第三部分因为有些 Flutter 写法在原生里没有直接对应物比如 BuildContext 的生命周期依赖、隐式动态类型带来的行为分叉。人工 review 时只看第三部分就够了节省大量时间。UI 层的迁移比逻辑层更繁琐。我们从 Flutter Widget 到 Compose/SwiftUI 做了一个映射表像 Container、Stack、ListView、NestedScrollView 都有对应方案。但映射表只是基础真正难的是布局细节Flutter 的约束体系和原生布局系统差异不小。我们后来规定所有涉及复杂自定义绘制的页面不做逐行翻译而是重新按原生绘图能力实现宁可多花时间也不要为了省事强行套用。3.4 第四阶段UI 还原与动效适配第四阶段是做 UI 还原和动效适配这个阶段最考验耐心。Flutter 动画方案和原生差异很大。之前在 Flutter 里用 Lottie 加载网络 zip 包做动画展示的地方到了双端都有对应的加载方案但加载逻辑、缓存策略要重新写。更麻烦的是那些用 Flutter 自带动画系统写的转场、拖拽、弹簧效果原生端没有完全等价的能力需要逐一手工调参。我们内部定了一个 UI 还原标准核心流程页面视觉还原度要求 99%用截图对比工具逐像素校对。次要页面要求 98% 以上。这里的误差主要集中在字体渲染、圆角阴影、滚动回弹阻尼这几个方面需要双端单独调整参数。动效迁移的教训是不要试图用原生实现 100% 复刻 Flutter 上所有动效。很多动效本身就是靠 Flutter 的物理引擎做出来的原生复刻成本极高但从产品角度用户根本感知不到那一点点差异。合理做法是保留核心动效的“神似”砍掉非关键路径的“形似”。4. 最容易出事的三个技术难点4.1 内存优化GC 机制差异带来的隐性泄漏Flutter 用的是 Dart VM 的分代式 GC对象回收对开发者基本透明很少需要手动管理内存。到了原生端Android 用 Java/Kotlin 的 GCiOS 用 ARC 自动引用计数两者的内存管理模型都不一样之前的很多“无害写法”在原生端变成了内存隐患。我们踩过最深的一个坑是图片缓存。Flutter 的图片缓存有一套全局的管理机制页面销毁后 GC 会自动回收但原生端如果持有图片对象没有释放就会一直占用内存。迁移后的首页轮播和图片墙在 iOS 上大量页面切换后内存持续上涨最后被系统杀掉。排查下来是单例缓存持有导致的问题后来统一封装了双端图片加载组件绑定生命周期自动清理才把内存峰值压下来。迁移过程中的一个基本原则是原生环境不要照搬 Flutter 的内存使用习惯。Flutter 里随手 new 一个对象不用担心但原生端要考虑对象生命周期、循环引用、集合持有等问题。我们在迁移规范里明确要求所有涉及资源型对象的代码必须显式处理释放逻辑。4.2 并发模型迁移Isolate 到协程与 ActorFlutter 的并发模型是事件循环加 Isolate很多耗时计算我们会用 compute 函数或者手动创建 Isolate 来处理。原生端的并发模型完全不同Android 是协程加 DispatcheriOS 是 GCD 和 Swift Concurrency。迁移时最容易出错的是线程切换语义。在 Flutter 里compute 执行完会自动回到主 isolate原生里可没这么智能。Kotlin 协程如果 Dispatcher 选择不当回调跑在后台线程直接操作 UI轻则界面错乱重则直接崩溃。Swift 那边用 Task 也有类似问题MainActor 隔离不做好数据竞争会随机出现。我们后来强制规定所有跨线程操作一律通过标准化的 Dispatcher 封装禁止在业务代码里直接 new Thread 或者手动创建 DispatchQueue。这样虽然写起来繁琐一点但至少行为可预期排查问题也简单。4.3 系统级能力迁移蓝牙、定位、后台任务这块是纯翻译模式完全覆盖不了的部分。以低功耗蓝牙为例Flutter 里用 flutter_blue_plus 一个插件就能搞定扫描、连接、读写但到了原生端Android 走 BluetoothLeScanneriOS 走 CoreBluetooth两边的 API 模型、权限模型、回调线程策略完全不同。迁移过程中我们重新写了双端的蓝牙模块在各自原生的推荐架构上实现了扫描、连接、服务发现、特征读写、断线重连这些能力。原来的 Flutter 插件层只是薄薄一层桥接现在这层业务逻辑要在双端各自落地一遍。定位、后台任务、系统通知也是一样的逻辑越是靠近系统越没有捷径可走。4.4 遗留工程维护补课式避坑迁移持续了大半年旧 Flutter 工程还在并行维护。这期间我们遇到了一些工程工具链问题虽然不是迁移主线的核心但非常影响开发效率顺手记录一下。比如有同事在 Windows 上用 VS Code 开发 Flutter 工程编译一个带本地插件的模块时报了 “unable to find suitable visual studio toolc”。这个问题的原因是本地插件涉及 CMake 编译但系统里没有安装完整的 Visual Studio C 工具链VS Code 又没法自动探测到构建工具路径。解决方法是安装 VS Build Tools 组件并确保 CMake 和 Ninja 的路径正确。这类问题在纯 Dart 工程里不会出现一旦涉及原生编译就会冒出来。另一个典型问题是 Flutter Gradle 插件升级后的报错“you are applying flutters main gradle plugin imperatively using the apply syntax”。这是 Flutter 3.x 之后对声明式插件导入的收严。解决办法是改造成在 settings.gradle 里使用 pluginManagement 导入插件然后通过 id 声明的方式 apply不要再用手动 apply 语法。这种东西报错一大段不熟悉 Gradle 新机制的同事容易卡半天。这类问题不是迁移的终点但确实消耗了不少精力。建议大家如果还在维护旧 Flutter 工程尽早把工程模板升级到新版本的标准结构省得后面反复踩。5. 代码量涨了 40 万行值不值5.1 迁移前后核心指标对比先看一组我们迁移完成后的核心指标对比指标Flutter 阶段原生双端阶段Android 冷启动耗时平均 1.8s平均 1.1siOS 冷启动耗时平均 1.6s平均 0.9sAndroid 安装包体积68MB52MBiOS 安装包体积112MB85MB核心列表掉帧率1.2%0.3%百次启动崩溃率0.8%0.3%内存峰值典型场景380MB290MB这里面的性能提升不是同一个量级的小幅优化。冷启动从 1.8 秒降到 1.1 秒用户第一视觉的差异非常直观。掉帧率降了一个量级信息流滑动终于有了“丝滑感”。包体积也瘦身了Flutter 引擎那部分体积不再占用用户下载流量。这套数据就是我们回击“代码量涨了 40 万行是不是瞎折腾”这个质疑的最好答案。行数不是关键关键是把同样一套业务做得更快、更稳、更省资源。5.2 112.5 万行代码是怎么来的很多人会觉得奇怪为什么从 71.6 万行 Flutter 变成 112.5 万行原生双端代码量反而涨了这么多。这个膨胀是有明确原因的。第一个原因是天然的双端翻倍。安卓一套、iOS 一套业务逻辑大部分要实现两遍这部分大约占新增代码量的一半。第二个原因是原生平台的样板代码更多。Kotlin 和 Swift 的类型体系比 Dart 严格大量的数据模型需要显式声明字段类型和序列化方式不像 Dart 可以随手写一个动态 Map。第三个原因是 UI 描述方式的差异。Compose 和 SwiftUI 虽然是声明式但代码密度和 Flutter 的 Widget 嵌套树相比还是有一定差距同样的布局写出来的行数会更多。但这不是无意义的膨胀。原生代码虽然行数多但每一行都更明确、更好理解、更容易调试。你在原生代码里做一个变量类型修改编译器能立刻帮你找出一堆错误这在 Dart 动态类型体系里是享受不到的福利。5.3 AI 在迁移中的真实投入产出最后说下 AI 在这场迁移里的真实投入产出给大家一个理性预期。我们统计的结果是AI 辅助生成的代码量约占最终代码的 65%但 AI 生成的代码里有大约一半需要人工调整后才能合并只有纯数据模型和基础模板类的代码能做到接近零修改。AI 真正提效的地方是重复劳动密集的部分model 转换、模板框架生成、JSON 解析代码、单元测试用例生成、代码规范检查。AI 做不到的地方也很明确涉及业务状态流转的复杂逻辑、需要理解用户场景和产品意图的交互设计、以及跨模块依赖关系的调整AI 生成的东西基本不能用。我们也尝试过让 AI 端到端做一个完整业务模块结果产出的代码表面上能跑但边界条件处理得一塌糊涂返工成本比直接人工写还高。在迁移过程中我们还用 AI 辅助了代码评审。把双端代码同时丢给大模型做交叉比对让 AI 检查两端行为是否一致、处理逻辑是否有遗漏分支。这个场景下 AI 的表现非常好它能发现很多人工 review 时容易忽略的边界问题。代码缺陷率下降了差不多一个数量级这对我们保证迁移质量帮助很大。5.4 代码行数不是最终目的如果让我用一句话总结这次的收获那就是代码行数从来不是架构迁移的最终目的用户体感和团队效率才是。112.5 万行原生代码里面有很多是双端重复实现的样板逻辑从纯粹的数字上看效率好像变低了。但如果看质量指标看线上稳定性看新功能迭代速度看团队招聘的容易程度这个迁移是值得的。原生代码比 Flutter 代码更容易让工程团队发挥战斗力这是我们在迁移过程中最深刻的体会。后期团队的新人上手速度也明显变快了。原来一个没接触过 Flutter 的开发要先学 Dart 语法、Flutter 框架、状态管理方案至少一两个月出不了活。现在直接按原生技术栈招人进来就是主力培训成本从按月计算变成了按天计算。6. 迁移避坑清单与给团队的实在建议6.1 最容易翻车的五个地方第一试图开发一个全自动转换工具把 Dart 代码一键转成 Kotlin 和 Swift。这个想法听起来诱人实际做起来成本高到离谱。编译器之间没有等价语义AI 理解再强也搞不定隐式的动态类型和跨模块耦合。我们的经验是AI 负责“理解和辅助生成”人工负责“决策和检查”分工明确才高效。第二忽略用户行为埋点的一致性。Flutter 阶段很多埋点是在 UI 组件的事件回调里顺手打的迁移后如果把埋点逻辑丢了数据分析那边直接断粮。我们的做法是迁移前先梳理全部埋点事件清单迁移过程中每条埋点都要在双端实现对账。第三试图对 UI 做逐像素翻译。Flutter 的约束布局和原生布局系统区别很大逐像素翻译会在深层次带来意义不明的代码和难以维护的组件层级。遇到复杂 UI重写比翻译靠谱。第四没有做双端平行的验证机制。迁移不是写完代码就结束了线上行为是否一致是最难验证的环节。我们的办法是做了一个自动化对比闸口让 Flutter 版本和原生版本在测试环境并行跑自动比对接口请求和状态变化。第五迁移周期拖太长。我们控制在一年的原因是团队强度已经到极限了如果拉到两年以上双版本带来的维护成本会指数级上升团队成员也会产生疲惫感。节奏宁可快一点也不要陷入无休止的平行维护泥潭。6.2 如果正在考虑迁移我的建议顺序如果你们的团队也在考虑从跨平台迁移到原生双端我强烈建议先别急着喊口号按下面这个顺序试水。第一步先做一次代码资产体检。不是说知道有多少行代码就行而是要把模块依赖关系、第三方包风险、团队技术栈现状全面摸底。这一步 AI 能帮上大忙。第二步先迁移数据层和网络层把地基换成原生的。这样双端并行后至少数据链路是统一的后续业务迁移有了可靠底座。第三步选两个中等复杂度的业务模块作为试点别一上来就啃硬骨头。跑完这两个模块把流程、工具链、成本模型都沉淀成标准化文档。第四步量化收益是否达到预期。如果试点项目的性能指标、开发效率、线上稳定性没有明显改善就要停下来重新评估没必要为了迁移而迁移。如果试点效果符合预期再按模块排期逐步平移。像我们这种 70 万行级别的项目分模块渐进式迁移比整体重写靠谱得多至少业务不会断档团队压力也可控。回到开头那个问题代码量多了是不是倒退我的回答是要看这多出来的 40 万行代码换来了什么。换来的是更快的启动速度、更稳定的线上表现、更可控的技术栈、更高效的团队协作。单看行数好像退了一步从产品价值和工程价值上看这步走得值。如果你们也在经历类似的选择题希望这篇复盘能帮你们少走一些弯路。