iOS与Flutter混合开发的姿势

Posted 追风的树懒

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS与Flutter混合开发的姿势相关的知识,希望对你有一定的参考价值。

首先要解释一下题目, 本文关于混合开发细节本文会简要聊一些, 因为官方文档与网友的智慧已经相当完备, 完全可以面向google编程, 这里不必赘述。那么就回到了本文的核心中来, 主要讲述了针对 ios 与 Flutter 混合开发中为了一个小优化点而进行的一系列不 ( zi ) 懈 ( zuo ) 努 ( zi ) 力 ( shou ) 。

Flutter混合集成模式简述

说到Flutter混合工程, 原由还是因为这项技术的使用渐进式。在大家的现有业务中使用, 大多数场景是在原有业务中摸索试水, 先用在一个低频页面中, 坐观其变。当看到开发case覆盖率、crash率、性能、稳定性等一些列指标微微一笑的时候, 加之开发效率提升的加持, 一声令下要大规模使用, 集结多个团队, 更多名同学来用的时候, 混合开发模式的优化就迫在眉睫了。

如何让多名同学协同开发Flutter, 如何对原有工程开发模式的最小侵入, 以及如何快速集成开发和更好的工程化都是Flutter混合开发模式要解决的问题。

官方方案

官方Flutter工程集成至IOS工程, 在这里能看到Flutter官方是多么亲切的指导着我们来使用这个技术, 其中总共给出了 ABC 三种方案。

本着不会都选 C 的原则, 选择这个方案最靠谱。一句话形容就是将Flutter技术的编译产物 ( Flutter.framework, App.framework等 ), 通过Cocoapods集成至iOS工程中, 它省去了方案 A 的复杂Podfile的修改, 也避免了方案 B 的手动 embed 和 link Flutter产物。

注意 这个方案的Pod引入方式是本地引入, 由于是本地打包, 并不太适合多人协作

业内方案

业内方案就比较简单了, 基于官方的方案 C, 将Flutter产物发布到远端即可, 话不多说, 直接上图

嘿嘿, 图片可能大家会觉得好熟悉啊, 这个不是重点, 直观表达才是。

当然这个方案也不是那么的简单, 还是有一些细致的工作, 包括收集依赖, 处理plugin以及生成Podspec文件等操作, 需要在一个脚手架的工具中完成这些操作。

基于这个脚手架, 原有IOS工程可以快速无成本的接入flutter的业务模块, 就像引入一个三方库那么简单, 只需要在 Podfile 文件增加一行即可

pod 'FlutterXXX'

是不是很开心, 很兴奋, 完成了如此重大技术的引入尽在弹指之间。但是在开发 Flutter 的同学就一脸黑了, 开发过程中只能使用 flutter 提供的 demo 工程跑起来, 根本没有原有 iOS 工程的上下文环境, 没办法开发的, 这就是下面要聊的混合工程开发模式。

一键集成的思路

想要进行 Flutter 与 iOS 工程混合开发我们需要什么能力?

  • 能在 IOS 工程中运行 Flutter 项目
  • 运行的 Flutter 项目能够进行热更新 ( hot reload )

我理解至少有上面两项能力就能开心的进行玩耍了, 体会着拥有 Web 开发的体验, 看着拥有 Native 开发的高性能也是内心欢喜, 还可以不时的幻想着一个人完成 iOS、android、桌面应用三种产出结果, 貌似就更有成就感了。

于是我们进一步查看 Flutter 的 Module 类型工程, 会发现谷歌baba已经帮我们做好了这一切, 在工程目录有一个 .ios文件夹, 下面这一个 Ruby 脚本 podhelper.rb, 一个方法 install_all_flutter_pods 搞定一切。

马上我们就开始马不停蹄的来包装它, 将它集成到我们的脚手架工具中, 为了用户体验 ( KPI ) 也是拼了, 最终我们想要的是这样的 :

程序猿 : 脚手架, 我们想要把 IOS 工程和 Flutter 工程集成在一起脚手架 : 好的, 给我他们的目录, 我帮你搞定...$> integrate iOS_PATH FLUTTER_PATH$> done

经过程序猿的一顿操作猛如虎, 将 Flutter 的混合开发模式能力也集成到了脚手架工具中。这个能力在官方的文档中也有体现, 是需要在 iOS 工程编译阶段插入可执行脚本, 使用 flutter_tools 中的脚本生成 Flutter 相关的 framework, 进而可以进行混合开发, 实现 hot reload 功能。

这里暂时不针对为什么使用 flutter_tools 脚本会实现 hot reload 能力的原因展开, 我是不会告诉你我还不知道呢

下面就是如何将 Flutter 集成至 iOS 工程中进行开发的核心逻辑图 :

iOS与Flutter混合开发的姿势

有了 Flutter 脚本在编译过程中的集成, 原有 iOS 工程就被赋予了针对 Flutter 代码 hot reload 的能力, 不得不兴奋一小下。

发现的小问题

非 Flutter 开发的同学的混合集成方式以及 Flutter 开发同学的混合开发方式都已经在脚手架中具备了能力, 现在是时候由程序猿大展身手的时候了, 他们快速下意识的打开了 Xcode 和 VS Code ( Android Studio 也可 )。 正所谓倚天屠龙在手, 谁与争锋, 在 Xcode 下熟练的按下了快捷键 Command + R , 开始编译启动 App, 待 App 顺利唤起的时候, 在 Flutter 工程的根目录下一键 flutter attach 命令, 搞定。

当完成了今天的开发任务的时候, 由于开发效率的提升, 程序猿还有一丢丢时间来回顾整个开发过程:

  1. 打开 Xcode 进行 App 启动
  2. 打开 Flutter 工程, 进行工程链接 , 使用 flutter attach 命令
  3. 在 Flutter 工程中进行开发

对于 Flutter 开发者, 在多数场景下, 只需要在 Flutter 工程下进行开发即可, IOS工程只有在提供桥接能力的时候, 才会通过 IOS 端上的原生能力进行支持。

那么, 为什么不能通过在 Flutter 工程下的 flutter run 命令来启动开发呢?  这样的话日常开发步骤就变成了 :

  1. 打开 Flutter 工程, 使用 flutter run 命令启动 App
  2. 在 Flutter 工程中进行开发

省去了个打开 iOS 工程的过程, 有没有更简单些, 也对非 iOS 开发者比较友好呢 ?

好的, 程序猿收到需求, 准备开工~

分析过程 ( zi zuo )

既然需求已定, 那么程序猿必使命必达, 完成需求。

首先来看下 Flutter 工程中 module 模式工程的工程目录 :

├── .android├── .ios├── lib├── pubspec.yaml

通过目录结构可以清晰的看到, lib 是 Flutter 相关代码, .ios.android 是 native 工程, 那么 flutter run 命令一定是启动了这两个 native 工程, 进一步看下 .ios 目录结构 :

├── .ios│   ├── Config│   ├── Flutter│   ├── Runner│   ├── Runner.xcodeproj│   └── Runner.xcworkspace

没错, 就是它, 熟悉又陌生的 Runner 工程, 下面也会多次提到它。这样可以推断出当执行 flutter run 命令的时候, 会使用 xcodebuild 命令找到这个 Runner工程进行编译启动, 所以如果能把 Runner 工程替换成我们的项目工程, 理论上就可以了。

经过查看 Flutter 源码可以发现, flutter run 命令只会查找根目录下 .iosios 两个目录作为启动工程目录, 并且优先查找 ios 目录, 因为 .ios 目录会被 flutter clean 命令清除掉, 所以认为有 ios 目录是开发者的工程目录, 源码片段如下 :

// 源码目录: packages/flutter_tools/lib/src/project.dart 315:324 Directory get ephemeralDirectory => parent.directory.childDirectory('.ios'); Directory get _editableDirectory => parent.directory.childDirectory('ios');
/// This parent folder of `Runner.xcodeproj`. Directory get hostAppRoot { if (!isModule || _editableDirectory.existsSync()) { return _editableDirectory; } return ephemeralDirectory; }

进一步查看源码也不难发现, flutter_tools 工具中也对 iOS 工程名称做了限制 ( 就是hard code), 必须是 Runner 的工程名称, 以及在后面的 xcodebuild 阶段指定的 target 也是 Runner

$> xcodebuild --target Runner

分析到这里, 只能说程序猿太难了, 要做这么多适配工作, 但是, 但是不得不说只是个开始而已...

实现过程 ( zi shou )

经过了大量的分析及实验之后, 终于有了一份可行性很高的适配指南产出了, 棒棒哒

1. 修改 Podfile 文件, 注入脚本2. 在 Flutter工程根目录创建 ios 目录 3. ios 目录中有项目工程, 工程名必须是 Runner.xcodeproj4. 工程中必须有名称为 Runner 的 target5. 修改xcodeproj文件, 适配环境变量及 target 配置

可以看出来 Flutter 官方的 flutter run 命令是基于将 iOS工程属于 Flutter工程的一部分来设计的, 并且需要遵循许多 Flutter工程的标准才能不出错的运行起来。

但是往往事与愿违, 我们想要的是项目工程与 Flutter 工程解耦, 貌似只能搬出软连接的方式了, 例如 :

$> ln -s source target

这样既能保证原有的项目工程和 Flutter 工程物理分离开来, 又可以满足工程改名称等操作, 于是程序猿将 iOS 工程链接到了 Flutter 工程下的 ios目录下, 然后进行了一顿操作, 大致的过程如下图所示 :

从图中可以看出来, 第二、三步骤花费的精力比较多, 也是整个方案的核心工作。其中修改 Podfile 的工作主要有两点 :

  1. 复制原有工程 target, 命名为 Runner, 主要是为了能让 flutter_tools 脚本找得到我们的工程
  2. 由于是使用软链接来与 Flutter 工程的关联, 那么 flutter_tools 里面定义的一些路径会有偏差, 我们需要矫正这些使用到的系统环境目录

最终修改过的 Podfile 大致是这样的 :

load "Dflu.rb" # 加载脚本target xxx do... dflu_install_flutter_pods do # 调用脚本安装依赖 end...endtarget Runner do # 从 xxx 复制而来, 用于 flutter run 命令调用... dflu_install_flutter_pods do # 调用脚本安装依赖 end...en

Dflu.rb这个Ruby脚本做了两件事情 : 第一是通过解析Flutter工程, 调用 CocoaPods 库的接口来插入 Flutter engine库, 以及相关plugin等依赖, 第二是适配所有 Flutter 使用的系统环境变量中和工程路径有关的变量。

修改 xcodeproj 文件的操作也是比较大的一个工作量, 这一步骤主要是为了创建一个 Runner的 target 来适配 flutter_tools 脚本的调用, 这里的开发主要还是依赖 Cocoapods 的xcodeproj 源码, 这个库已经帮我们做了许多针对xcodeproj的操作的封装, 踩在巨人的肩膀上那叫一个爽, 很快就能实现复杂功能。

为了实现程序猿当初许下的诺言 -- 一键集成开发的能力, 在他们在脚手架工具中就开发了这样一条指令 :

$> dflu integrate ios NATIVE_PATH FLUTTER_PATH

程序猿默默的祭出了这样一条命令, 心里是莫名的自豪, 顿时觉得我的代码是最好的, 觉得此处应该有掌声,  然而这只是他的幻想罢了, 还是简单看下这条命令的大致入口代码吧

执行完集成命令, 看到命令行输出 All Done 的那一刻, 程序猿狠狠的敲下了 flutter run 这条他梦寐以求的命令, 默默点了支烟, 看着 flutter pub get , pod install, xcodebuild, run 等命令的执行, 看似表面很平静的表情, 其实内心早已焦急不已, 除非没有任何错误报出。

然而, 不是一切尽在程序猿掌握之中, 在项目工程中, 还是有一些特殊情况需要处理, 例如项目工程使用了自定义的 xcconfig 配置, Podfile 文件中通过 path 方式引入了三方库等, 都需要一一适配, 只能说程序猿太难了...

实现难点

为了实现这个小优化, 没想到要做这么多工作, 整体实现下来不能说有多大的难点, 只是需要反复不停的实验以及在 Flutter, Cocospods 源码中穿梭。

为了达成当初可以通过 flutter run 进行开发的目标, 也需要考虑原有的直接在 iOS 工程中开发的诉求, 为了适配两种开发方式, 在能力适配上, 比较多的工作是适配两种启动方式的工程路径问题, 保证两种启动方式都能找到正确的工程目录以及系统环境路径。

Podfile 脚本注入

为了能方便的调用 Cocoapods 的 API, 脚手架工具也使用了 Ruby 作为开发语言, 在 Podfile 文件的修改过程中, 主要调用了 CocoaPodsCocoaPods Core 的 API, 大致过程包括 :

  • 修改原有工程 pod 引入方式为 path 的,  将 path 路径修改为绝对路径
  • 针对 target 添加 script_phases 的自定义脚本
  • 通过 pod 方式引入 Flutter engine 及相关的库
  • 引入 ( load ) dflu 脚本, 并复制原有工程的 target, 添加至 Podfile 尾部并改名为 Runner

这个过程中会不断的对 CocoaPods 源码有了解, 尤其是在对 pod 的 path 修改的时候, 怎么样修改已经存储过的数据, CocoaPods 对 Podfile 的数据是如何存储的, 都需要一一搞清楚, 然后去修改它, 正如下面的源码, 整个Podfile会被存储到一个 hash 数据结构中 :

# 源码目录 : Core/lib/cocoapods-core/podfile.rb 365:371 private
# @!group Private helpers
# @return [Hash] The hash which store the attributes of the Podfile. # attr_accessor :internal_has

至于 Podfile中的 pod, source, target 等方法都是定义在 Core/lib/cocoapods-core/podfile/dsl.rb 这里的, 它会将信息分发存储到不同的实例中。

xcodeproj 文件修改

针对xcodeproj文件进行的修改就只有一个操作, 就是复制一份原有工程的target, 然后命名为 Runner。

这里要依靠 CocoaPods Xcodeproj 的源代码, 由于 CocoaPods 的功能也是会对 xcodeproj 文件进行多维度修改, 所以这个工具库比较独立, 可以直接使用。

在复制的过程中. 比较难确定的一点就是具体需要复制哪些信息, 修改哪些信息,  这些都需要从 Flutter 工程中自带的 Runner工程中获取, 进行不断的文件对比, xcodeproj 文件本质上是一个由 JSON 形式表达的数据结构, 它可以被 Xcode 解析成操作界面, 也可以被解析成内存对象, 从下面源码可以看出, Xcodeproj 库就将该文件解析成内存对象 :

# 源码目录 : Xcodeproj/lib/xcodeproj/project.rb 106:114def self.open(path) path = Pathname.pwd + path unless Pathname.new(path).exist? raise "[Xcodeproj] Unable to open `#{path}` because it doesn't exist." end project = new(path, true) project.send(:initialize_from_file) projectend

当然, 这个解析过程还是比较复杂的, 因为这个 JSON 文件中的对象对应的 Key 都是类似 25E80FB17FE2B7862DABB507 这样的, 由 xcode 生成的, 而且嵌套层级比较深。

经过不断的对比和试错, 程序猿也是最终找到了需要的信息, 修改步骤如下 :

  1. 新建一个 Runner target, 并添加至工程文件中
  2. 从原有 target 中复制 Build phases 相关信息至 Runner, 这里还要区分 source , resource , framework, shell 等类型, 进行不同的复制操作
  3. 复制 product 信息
  4. 复制 build configuration list 信息
  5. 保存工程文件

程序猿终于可以长出一口气了, 自作之路马上就要结束了, 就剩下一些收尾工作就可以了。

其他适配

在真实的项目中, 可能还会出现很多的项目配置项, 至少目前碰到了一些 :

  • 项目工程使用了自定义的 xcconfig 文件来配置系统环境变量, 这个时候需要在自定义的 xcconfig 中配置 PODS_ROOT变量的路径
  • 项目中其他引入了其他的 Flutter 项目工程, 并且也依赖了 Flutter engine 等核心库, 这样会造成冲突, 需要协调解决

源码调试

在整体适配过程中, 会查看几个库的源码, 也会进行调试, 在这里程序猿使用的 VS Code 开关工具, 可谓一个套路走天下, 完全可以覆盖 Flutter , Shell , Ruby 的开发调试, 真香~

调试是使用的 code 的 debugger, 配置的 launch.json 是这样的 :

 "configurations": [ { "name": "Debug Local File", "type": "Ruby", "request": "launch", "cwd": "${workspaceRoot}", "program": "${workspaceRoot}/bin/dflu", "args": ["mode", "ios"] }, ]

调试 flutter_tools 源代码的配置如下 :

 "configurations": [ { "name": "Dart", "program": "这里是flutter工程的入口文件 xxx.dart", "request": "launch", "type": "dart" } ]

有了源码调试能力, 程序猿是有如神助, 不用再猜测, 幻想, 扣代码, 为了那个 hash 里面到底存的是啥而发愁?

问题

当一切都风平浪静的时候, 程序猿缓缓坐下来, 反思这样做可能带来的问题, 不可避免的要说下, 毕竟自己挖的坑自己还是要填的。

  • 在 iOS 中添加或修改 Podfile 的依赖的时候, 由于 Runner target 的存在, 需要同步修改两部分
  • 脚手架集成是在项目的分支基础上集成的, 如果项目要切换分支, 需要做现场的复原操作

这样的利弊分析下来, 貌似利小于弊, 程序猿有些恍惚了, 我都做了什么。没事, 我们还可以继续优化它, 这时内心的另一个声音出现了。

小结

整篇下来出现频次最好的当属 Runner 了, 这个工程启动的引路人, 为了适配它, 程序猿已经好几个风黑月高的夜晚不能寐, 抓耳挠腮也经常伴随, 偶尔还会自问, 这样做是不是太麻烦了, 原本的操作,  打开 xcode -> 启动 App -> flutter attach , 它不香么?

正如老罗的那本书名一样  <<生命不息,折腾不止>>, 程序猿也是拼了。

虽然通过这次小优化的折腾, 程序猿确实丢了半条命, 但是也收获了不少, 比如我们常用的 Podfile 文件到底是怎么样工作的, 在 CocoaPods 里面我们引入的一些依赖, 包括不同的引入方式是怎么通过格式化的数据存储的, 甚至对 CocoaPods 的源码架构都有了一些的了解, 再比如针对 flutter_tools 这个库的工作原理, 是如何高效的支持 Flutter 应用的开发协作的, 甚至对 flutter_tools生成的整个 fluttert 命令行工具的原理都有了一些了解, 也是小有收获的。

目前这个脚手架还是属于 Flutter 混合工程集成与开发的一个工具, 小展望一下在 Flutter 开发生态上的建设道路还很长, 至少有一套 flutter 开发套件来支撑混合集成、开发、engine管理、打包和发布等一些列工程化工具集, 才能更好的聚焦技术和业务成长。

参考


  1. Flutter 源码

  2. CocoaPods 源码

  3. CocoaPods Core 源码

  4. CocoaPods Xcodeproj 源码


以上是关于iOS与Flutter混合开发的姿势的主要内容,如果未能解决你的问题,请参考以下文章

flutter 腾讯云 上传腾讯云cos 使用flutter1.12 1.17及以上版本 ios与flutter混合开发

iOS与Flutter混合开发

原生(iOS)与Flutter混合开发步骤

原生(iOS)与Flutter混合开发步骤

原生(iOS)与Flutter混合开发步骤

原生(iOS)与Flutter混合开发步骤