体验 Flutter
从 App Store 下载或更新头条(6.9.2 或以上版本),找到 懂车帝 -> 热门车型,点击打开后即可体验 Flutter 的页面效果。
由于前期业务改造顺利,线上 Crash 少,性能良好,目前我们正在进行小视频模块的 Flutter 重构,即将上线。
本文主要介绍头条 iOS 端在接入 Flutter 的过程中,选择的技术方案,遇到的问题和未来的计划。虽然是以 iOS 为例,但很多内容都是双端通用的。
Flutter 基础
为了方便对 Flutter 零基础读者阅读本文,首先介绍一些 Flutter 的基础知识。
当我们写完 Dart 代码后,需要编译后才能给客户端使用。Flutter 的产物分为两种模式,一个是 Debug 模式,采用 JIT(Just In Time)的方式,类似于解释型语言,好处是可以支持热更新,方便调试。另一种是 AOT(Ahead Of Time)模式,类似于编译型语言,好处是性能比较好。
因此在开发中,我们一般使用 Debug 模式的产物来提升调试效率,正式上线后再换成 Release 模式获得极致的性能。不管是哪种模式,在 iOS 下的产物都是三个(安卓类似):
- App.framework:和 flutter 的业务逻辑相关,在 Debug 模式下就是一个很小的空壳,在 Release 模式下包含全部业务逻辑
- flutter_assets:也和 flutter 的业务逻辑相关,在 Debug 模式下包含全部业务逻辑,在 Release 模式下很小。
- Flutter.framework:实现 Flutter 框架自己的逻辑
iOS 工程接入 Flutter 的已有方案
目前主流的方案中主要有两种,一种在 Google 的官方文档里 ,它最大的问题在于, 需要创建一步 BUILD PHASE 调用 shell 脚本去编译 Flutter。编译 Flutter 的过程需要本机有 Flutter 环境才行。
这显然是不能接受的。其实我们先不考虑 Flutter 这件事,不管是搭建什么类型的混合工程,都一定要保证以下两点:
- 对原生工程无侵入:原生工程可以增加组件的依赖,但不能修改主工程的配置,更不能让原生工程对环境产生新的依赖。
- 方便调试:不管是原生工程开发,调试新接入的模块(比如 Flutter),还是模块开发调试原生工程,都应该支持断点调试。
很明显,官网的方案不满足第一点要求,我们不能让每个参与原生工程开发的同学,本机都安装 Flutter 环境。
从 Flutter 的官网教程 中可以看到,它支持 flutter run
命令或者从 IDE 启动 iOS 工程。其实只要观察一下 ios 目录下的 Runner.xcworkspace
工程就会发现,Flutter 项目运行的就是这个工程。当然如果直接把我们的 iOS 工程拷贝过来是无法运行的。
至少在目前最新的 dev 分支(Tag:v0.10.0)上是不行的,因为 flutter 的代码中写死了工程的名字就是 Runner,可以验证一下:
这也就催生了目前主流的,也是闲鱼最先介绍的方案,魔改 Flutter 源码,让它能运行自己的工程。然而个人觉得这种方式还是太 trick,主要有两个问题:
- 后续如果 Flutter 升级了,还得把修改的部分应用到新的分支上去,并且修复冲突。
- 如果公司或者业内有别的团队要用,接入成本比较高。
头条的混合工程方案
观察一下 Runner 的工程结构:
再看一眼 Flutter 编译脚本就会发现,这一步并不是必须放在主工程的 BUILD 阶段进行。只要拥有 Flutter 的编译产物,项目就可以接入 Flutter 的功能了。因此头条选择的方案是:
- 利用
flutter build ios
和flutter build ios --debug
命令,先后构建两种模式下的产物并上传到 CDN - 构建一个 Pod,并且让 Pod 依赖上述产物
在第一步中,运行 flutter build ios
实际上会依赖 Runner 工程,相当于是我们借助这个空壳去得到产物。不过要想编译通过,还需要做如下两个微小的改动:
- 修改项目的 Bundle ID,使用自己的证书。否则无法通过签名
- 打开 Xcode -> File -> Workspace -> Setting,把 BUILD_SYSTEM 切换为 Legacy,否则会产生编译错误
第二步中,重点在于如何让 Pod 动态的下载资源,因为需要指定使用哪个版本,哪种模式的 Flutter 产物。我们目前的策略是在打包机器上使用 Release 模式产物,在开发机器上使用 Debug 模式产物。
至于动态指定版本号,可以这样写:
s.prepare_command = "bash ./BDFlutterDownload.sh"
s.vendored_frameworks = 'Framework/*.framework'
s.resources = 'Framework/flutter_assets'
通过 prepare_command
语法,可以在 Pod Install 的过程中执行 shell 脚本,把产物下载到 Framework
文件夹中,只要把版本号写到脚本中,并且及时更新就可以了。
调试 Flutter
上述方案解决了 Flutter 工程接入的问题,因为是编译后的资源接入,我们还需要保证 Flutter 开发的同学可以正常调试。这里以最新的 dev 分支举例(Tag:v0.10.0)。
如果是 Flutter 开发的同学,不关心 iOS 开发的细节,可以找到打包后的 app 文件,然后使用 flutter run --use-application-binary
指定被调试应用的路径:
如果是 iOS 开发的同学,使用 Xcode 运行项目即可。这两种方式下,都可以看到本地调试的端口(图中的 57674)。
如果是使用编译好的 app 来运行,可以直接使用 r/R 来实现热刷新/热重启。如果使用 Xcode 运行项目,需要先使用命令: flutter attach --debug-port=57674
。
如果需要在 iOS 侧打断点,必须用 Xcode 运行项目。如果需要在 Dart 侧打断点,可以利用 IDE 的 Debug 功能。这里以 VSCode 为例,首先安装 Dart 插件。