Flutter - 混编项目集成Shorebird热更新🐦(iOS篇)

欢迎关注微信公众号:FSA全栈行动 👋

一、概述

关于 Shorebird 的初始化内容可以在上一篇《Flutter - 混编项目集成Shorebird热更新🐦(安卓篇)》中查看,这里就不再赘述了。

Shorebird 官方文档上对于 iOS 混编方案集成热更新的介绍不算详细,只能说点明了要点,指明了方向。

本文将根据实际的项目应用情况做出集成调整,并补充说明正确的补丁验证方案。

二、踩坑

Shorebird 文档里指出需要我们使用类似 Flutter 官方文档里 Option B - Embed frameworks in Xcode 的方式去集成 Flutter 模块。

关于 Flutter 官方文档指出的各种集成方式可以查看: docs.flutter.dev/add-to-app/...

具体的步骤就是:

  1. 先注释掉原来的 Option A 集成方式的相关配置代码
  2. 执行 Shorebird Release 去构建对应的所有 xcframework 文件。(xcframework 文件包括了 Flutter.xcframeworkApp.xcframework,以及插件依赖的原生第三方库对应的 xcframework
  3. 将所有构建完成的 xcframework 拖到 Build Phases 中的 Embed Frameworks 内。
  4. xcframework 的所在目录路径配置到 Framework Search Paths
  5. 配置 xcframeworkEmbed 模式,静态库必须选 Do Not Embed,动态库必须选 Embed & Sign

因为 Option A - Embed with CocoaPods and the Flutter SDK 的方式只需要简单的配置 Podfile 就可以集成 Flutter 模块,所以相信大家在一般情况下都是会选择 Option A 的方式。很明显,要改成 Option B 需要我们大改特改。

改成 Option B 这种方式有以下几点问题:

1、vendored_frameworks 缺失

如果你依赖的 Flutter 插件依赖了原生第三方的二进制包,如 realm,在它的 podspec 文件是这样声明的 s.vendored_frameworks = 'realm_dart.xcframework',那你会发现在最终构建完成的 xcframework 的目录里会缺少这些 vendored_frameworks

相关的 issue: github.com/flutter/flu...

因为 Option B 是二进制依赖,所以在编译的时候并不会报任何错误,等你 App 运行起来进入一些相关场景,使用到了对应的第三方功能时就会直接来个找不到符号的错误,如:

shell 复制代码
Failed to lookup symbol 'native_method_signature': dlsym(0xa47e7c10, native_method_signature): symbol not found

接着就是闪退,可想而知这得多吓人!

2、重复编译

vendored_frameworks 缺失的问题我通过脚本解决了,但是还有另一个问题,这些 xcframework 中也有可能出现涵盖你原来的原生工程里依赖的第三方包,比如,Flutter 的插件用到了 FMDB,生成的 xcframework 中就会包含 FMDB.xcframework,而你的原生工程本来就有依赖 FMDB,这个时候编译,Xcode 就会告诉你重复了,编译不通过,报错内容如下:

shell 复制代码
Showing Recent Messages
Multiple commands produce '/Users/lxf/Library/Developer/Xcode/DerivedData/xxx.app/Frameworks/FMDB.framework'

如果是你,你选择留下哪个呢?

  • 如果你选择了 Flutter 帮你生成的 FMDB.xcframework,你就得去处理其它原生第三方依赖的 pod 'FMDB',假如此时原生工程里的一些第三方库或私有库也依赖 FMDB,那你要处理这些库可就太麻烦了。
  • 如果你选择使用 pod 'FMDB' 的方式,那你只需要去判断原生工程里是否有对应的依赖,有的话就不再声明依赖,这种还好。

3、静态库与动态库

生成的 xcframework 中,有些是静态库,有些是动态库

如图所示,静态库必须选 Do Not Embed,动态库必须选 Embed & Sign

如果你全选了 Embed & Sign,那么你就无法启动 App 了,如下图所示

该问题的相关 issue: github.com/flutter/flu...

所以为了避免这种情况,我们就必须得选对 Embed 选项,可以使用 file 命令去判断 xcframework 是静态库还是动态库

shell 复制代码
file FlutterPluginRegistrant.xcframework/ios-arm64/FlutterPluginRegistrant.framework/FlutterPluginRegistrant
FlutterPluginRegistrant.xcframework/ios-arm64/FlutterPluginRegistrant.framework/FlutterPluginRegistrant: 
current ar archive random library // 静态库

file url_launcher_ios.xcframework/ios-arm64/url_launcher_ios.framework/url_launcher_ios
url_launcher_ios.xcframework/ios-arm64/url_launcher_ios.framework/url_launcher_ios: 
Mach-O 64-bit dynamically linked shared library arm64 // 动态库

这部分判断逻辑只能交给脚本处理了,因为当数量起来后你就会体验到什么叫崩溃,别问我是怎么知道的 😭

4、直接崩溃

后面我直接用脚本判断 Flutter 插件依赖了哪些原生第三方,将它们统一在原生工程内声明依赖,在一些情况下这也是很危险的,如 connectivity_plus 这个 Flutter 插件依赖了 ReachabilitySwift,你必须得使用 Reachability.xcframework 二进制嵌入的方式,否则运行就崩~

shell 复制代码
dyld[31764]: Symbol not found: _$s12ReachabilityAAC10ConnectionO4wifiyA2DmFWC
  Referenced from: <8142F86E-4C9C-3513-AD29-D3522FC6677F> /Users/lxf/Library/Developer/Xcode/DerivedData/xxx/connectivity_plus.framework/connectivity_plus
  Expected in:     <DA318000-9A97-35AD-87EA-7C5B635DE010> /Users/lxf/Library/Developer/xxx.app/Frameworks/Reachability.framework/Reachability

三、分析

后来仔细想想,Shorebird 的热更新是针对 Dart 代码,跟原生无关,能不能按原来的 Cocoapods 方式去集成 Flutter.xcframeworkApp.xcframework 以及插件依赖的原生第三方库呢?

答案是可以的,来看看 install_all_flutter_pods 方法

ruby 复制代码
def install_all_flutter_pods(flutter_application_path = nil)
  ...
  flutter_application_path ||= File.join('..', '..')
  # 生成 .ios/Flutter/Flutter.podspec
  install_flutter_engine_pod(flutter_application_path)
  # 集成 插件依赖的原生库 Pods
  install_flutter_plugin_pods(flutter_application_path)
  # 编译并集成 Flutter.xcframework 和 App.xcframework
  install_flutter_application_pod(flutter_application_path)
end

1、install_flutter_engine_pod

install_flutter_engine_pod 生成的 Flutter.podspec 是假的podspec,里面没啥实质内容,仅代表 Flutter.xcframework,为什么要这么做呢?因为一些 Flutter 插件声明需要依赖 Flutter,如:

ruby 复制代码
Pod::Spec.new do |s|
  s.name             = 'sqflite'
  ...
  s.dependency 'Flutter'
  s.dependency 'FMDB', '>= 2.7.5'
  ...
end

如果没有这个 Flutter.podspec,那么执行 pod install 就会从 CocoaPods trunk 下载 Flutter 了。

2、install_flutter_application_pod

install_flutter_application_pod 会去编译 Flutter.xcframeworkApp.xcframework,并将它们并集到我们的原生工程内。不过这两玩意我们用 Shorebird Release 去生成了,所以这个方法我们用不上。

我们可以结合上述的 Flutter.podspec 的作用,修改它内部的依赖声明,从而实现通过 Cocoapods 的方式来集成 Flutter.xcframeworkApp.xcframework

diff 复制代码
Pod::Spec.new do |s|
s.name             = 'Flutter'
s.version          = '1.0.0'
s.summary          = 'A UI toolkit for beautiful and fast apps.'
s.homepage         = 'https://flutter.dev'
s.license          = { :type => 'BSD' }
s.author           = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' }
s.source           = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s }
s.ios.deployment_target = '11.0'
# Framework linking is handled by Flutter tooling, not CocoaPods.
# Add a placeholder to satisfy `s.dependency 'Flutter'` plugin podspecs.
#
# 以上到这句都是原来的,将这句注释掉
+ # s.vendored_frameworks = 'path/to/nothing'
# 新增下面这句,声明依赖当前目录下的 Flutter.xcframework 和 App.xcframework
+ s.vendored_frameworks = 'Flutter.xcframework', 'App.xcframework'
end

生成的所有 xcframework 所在路径为: xxx/flutter_module/build/ios/framework/Release, 我们自己创建的 Flutter.podspec 中的依赖是相对路径,所以该 podspec 也是跟 xcframework 放到一起,当然也可以根据你自己的习惯进行调整。

3、install_flutter_plugin_pods

install_flutter_plugin_pods 会将 Flutter 插件依赖的原生库集成到我们的原生工程,这正是我们需要的。

不过如果你直接将 Podfile 中的 install_flutter_application_pod 给替换成 install_flutter_plugin_pods ,执行 pod install 时是会报如下错误的:

shell 复制代码
pod install

[!] Invalid `Podfile` file: undefined method `flutter_relative_path_from_podfile' for #<Pod::Podfile:0x000000010e74c520 @defined_in_file=#<Pathname:/Users/lxf/xxx/Podfile>, @internal_hash={}, @root_target_definitions=[#<Pod::Podfile::TargetDefinition label=Pods>], @current_target_definition=#<Pod::Podfile::TargetDefinition label=Pods>>

  relative = flutter_relative_path_from_podfile(export_script_directory)

也就是找不到 flutter_relative_path_from_podfile 方法,因为该方法在并不在你的 Flutter 模块的 podhelper.rb 中,而是在 packages/flutter_tools/bin/podhelper.rb

至于为什么原来的 install_all_flutter_pods 方法不会报错,是因为在该方法内先引用了 flutter_tools/bin/podhelper.rb

关键代码如下:

ruby 复制代码
def install_all_flutter_pods(flutter_application_path = nil)
  ...
  # 就是这句
  require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)

  flutter_application_path ||= File.join('..', '..')
  install_flutter_engine_pod(flutter_application_path)
  install_flutter_plugin_pods(flutter_application_path)
  install_flutter_application_pod(flutter_application_path)
end

所以我们可以如法炮制,在 install_flutter_plugin_pods 方法中加入 require 这一行代码,以解决上述错误。

diff 复制代码
def install_flutter_plugin_pods(flutter_application_path)
+  require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
  flutter_application_path ||= File.join('..', '..')
  ...
end

老是这么改也不是个办法,所以我提了个 PR: github.com/flutter/flu...

PR 现已合并,应该会在 3.16.9 及之后的版本中生效。

经过验证,该方案是可行的,下面我们来看看如何调整原生工程和 ShorebirdiOS 混编下如何使用吧。

四、原生工程调整

Podfile 文件中,将 Flutter 壳工程的源码依赖方式调整为二进制依赖

diff 复制代码
- install_all_flutter_pods(flutter_application_path)
+ # 源码集成
+ # install_all_flutter_pods(flutter_application_path)

+ # 二进制集成
+ pod 'Flutter', path: 'xxx/flutter_modules/build/ios/framework/Release'
+ install_flutter_plugin_pods(flutter_application_path)
  1. 声明 Flutter 依赖,用于集成 Flutter.xcframeworkApp.xcframework
  2. Option A 方式所需要的代码统统保留,只需要将 install_all_flutter_pods 替换为 install_flutter_plugin_pods,用于集成 Flutter 插件所依赖的原生第三方库

五、创建 Shorebird Release

打发布包的时候操作,在 Flutter 工程目录下执行

shell 复制代码
cd xx/xx/flutter_modules

# 7.0.0+2: 版本号+build版本号
shorebird release ios-framework-alpha --release-version 7.0.0+2

该命令内部会去执行 flutter build ios-framework --no-debug --no-profile ...,并且使用的是 Shorebird 魔改的 Flutter 引擎!

版本号可以在如下图所示进行查看

ShoreBird 的内部逻辑会去以这个版本号组合,向服务器请求判断是否存在相应版本的相关补丁!

执行完成后,在 Shorebird 控制台上可以看到相应的项

在命令执行前,请确保不存在 7.0.0+2Release,如果有的话,请先删除

六、创建 Shorebird Patch

紧急修复线上包的bug时操作,在 Flutter 工程目录下执行

shell 复制代码
shorebird patch ios-framework-alpha --release-version 7.0.0+2

注:版本号与上述的 release 命令中使用的要保持一致!

执行完成后,在 Shorebird 控制台上点击对应的 Release 项,进去后可以看到相应的补丁

看看这个补丁大小,我们再来看看安卓的补丁大小

一样的修改,安卓的补丁大小不到 2 MBiOS 的补丁大小高达 54.83 MB 😂

七、热更新验证

官方文档上就只是说重启 App 查看补丁是否生效,并没有说明失败了该如果排查问题~

1、在执行完 shorebird release 命令并完成上述原生工程的调整后,将原生工程的编译模式调整为 Release 进行编译。

此时会依赖的 flutter_modules/build/ios/framework/Release 下的 xcframework,备份为 Release_release

2、关闭 App,打 patch,注意,此时 flutter_modules/build/ios/framework/Release 下的内容会被清空并重新创建。

3、打 patch 后,将 Release_release 改回 ReleaseXcode 重新运行 App,一切正常的话即可看到变化。

无论成功还是失败,Xcode 的控制台都会有相应的输出

成功

shell 复制代码
2024-01-03 18:37:55.838328+0800 xxx[623:70498] [VERBOSE0:shorebird.cc(151)] Shorebird updater: no active patch.
2024-01-03 18:37:55.838424+0800 xxx[623:70498] [VERBOSE0:shorebird.cc(155)] Starting Shorebird update
[00:00:00.002] (1701cb000) INFO   Sending patch check request: PatchCheckRequest { app_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", channel: "stable", release_version: "7.0.0+2", patch_number: None, platform: "ios", arch: "aarch64" }

[00:00:30.871] (1701cb000) INFO  Patch 1 successfully installed.
[00:00:30.871] (1701cb000) INFO   Update result: Update installed

失败

可以搜索关键字 PatchCheckRequest 定位

shell 复制代码
2024-01-03 18:37:55.838328+0800 xxx[623:70498] [VERBOSE0:shorebird.cc(151)] Shorebird updater: no active patch.
2024-01-03 18:37:55.838424+0800 xxx[623:70498] [VERBOSE0:shorebird.cc(155)] Starting Shorebird update
[00:00:00.002] (1701cb000) INFO   Sending patch check request: PatchCheckRequest { app_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", channel: "stable", release_version: "7.0.0+2", patch_number: None, platform: "ios", arch: "aarch64" }

[00:00:30.871] (1701cb000) ERROR  Update failed: error decoding response body: operation timed out
Caused by:
    operation timed out

[00:00:30.871] (1701cb000) INFO   Update thread finished with status: Update had error

该失败是因为国行机特有的网络权限导致的,开启 Shorebird 的自动检查更新的话,会在网络权限被赋予前去请求,结果就是失败,所以需要关闭自动检查更新,使用 shorebird_code_push 去延迟检查。

八、脚本

由于我们日常研发还是使用的是源码依赖的方式,只会在打最终测试包时才需要去做上述的调整操作,所以这里用我比较熟悉的 Python 去制作了简易的脚本,并结合 Jenkins 来辅助完成这种万年不变的无聊步骤

脚本已上传至 Github: github.com/LinXunFeng/...

看官可自取修改~

switch_flutter_integrate.py

切换 Flutter 项目的集成方式

shell 复制代码
# 二进制依赖
python switch_flutter_integrate.py -p '原生工程路径' -m 'binary' -f 'ios'

# 源码依赖
python switch_flutter_integrate.py -p '原生工程路径' -m 'source' -f 'ios' 

shorebird.py

自动获取版本号,并执行 Shorebird 相关命令

shell 复制代码
# release
python shorebird.py -p '原生工程路径' -s 'Flutter工程路径' -m release -f ios

# patch
python shorebird.py -p '原生工程路径' -s 'Flutter工程路径' -m patch -f ios

需要注意的是,xcodeprojtarget 的名字被我固定写成 OCProject,如下代码中高亮的那两行,大家请先将其修改为自己的工程名再使用 shorebird.py

diff 复制代码
def handle_ios():
    """
    处理iOS项目
    """
    # 1. 读取主版本号
    # 请将 OCProject 修改为你们自己的工程名
+    xcodeproj_path = os.path.join(project_path, 'OCProject.xcodeproj')
    version = ReleaseVersionTool.fetch_project_version(
        xcodeproj_path=xcodeproj_path,
+        target_name='OCProject',
    )

由于我比较懒,就不改成通用的了 😏

九、最后

虽然 iOS 的热更新能用,但也仅仅只是能用,应用于很简单的应用程序,运行起来没有太明显的卡顿感知,但是稍微大点就可以感知到了,卡到怀疑人生那种,相比安卓端的没有任何性能损耗,iOS端的还需要再等等,毕竟现在 iOS 还是 Alpha 版本,相信不久将来 Shorebird 团队会解决该问题。

具体关于安卓和 iOS 两端之间的实现区别可以在这个 issue 中查看 github.com/shorebirdte...

本篇到此结束,感谢大家的支持,我们下次再见! 👋

如果文章对您有所帮助, 请不吝点击关注一下我的微信公众号:FSA全栈行动, 这将是对我最大的激励. 公众号不仅有 iOS 技术,还有 AndroidFlutterPython 等文章, 可能有你想要了解的技能知识点哦~

相关推荐
CocoaKier18 分钟前
苹果商店下载链接如何获取
ios·apple
喵叔哟24 分钟前
重构代码之取消临时字段
java·前端·重构
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解1 小时前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶21361 小时前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js