FSA全栈行动 FSA全栈行动
首页
  • 移动端文章

    • Android
    • iOS
    • Flutter
  • 学习笔记

    • 《Kotlin快速入门进阶》笔记
    • 《Flutter从入门到实战》笔记
    • 《Flutter复习》笔记
前端
后端
  • 学习笔记

    • 《深入浅出设计模式Java版》笔记
  • 逆向
  • 分类
  • 标签
  • 归档
  • LinXunFeng
  • GitLqr

公众号:FSA全栈行动

记录学习过程中的知识
首页
  • 移动端文章

    • Android
    • iOS
    • Flutter
  • 学习笔记

    • 《Kotlin快速入门进阶》笔记
    • 《Flutter从入门到实战》笔记
    • 《Flutter复习》笔记
前端
后端
  • 学习笔记

    • 《深入浅出设计模式Java版》笔记
  • 逆向
  • 分类
  • 标签
  • 归档
  • LinXunFeng
  • GitLqr
  • AndroidUI

  • Android第三方SDK

  • Android混淆

  • Android仓库

  • Android新闻

  • Android系统开发

  • Android源码

  • Android注解AOP

  • Android脚本

  • AndroidTv开发

  • AndroidNDK

  • Android音视频

  • Android热修复

  • Android性能优化

  • Android云游戏

  • Android插件化

  • iOSUI

  • iOS工具

  • iOS底层原理与应用

  • iOS组件化

  • iOS音视频

  • iOS疑难杂症

  • iOS之Swift

  • iOS之RxSwift

  • iOS开源项目

  • iOS逆向

  • Flutter开发

    • Dart - 抽象类的实例化
    • Flutter - 打印好用的Debug日志
    • Flutter - 混合开发
    • Flutter - 解决混合开发iOS脚本打包遇到的问题
    • Flutter - 低版本在iOS14上遇到的问题与解决方案
    • Flutter - 解决原生弹窗的触摸事件被Flutter响应的问题
    • Flutter - 实现列表上下拉切换header
    • Flutter - 获取ListView当前正在显示的Widget信息
    • Flutter - 列表滚动定位超强辅助库,墙裂推荐!🔥
    • Flutter - 快速实现聊天会话列表的效果,完美💯
    • Flutter - 聊天输入框更新文本时的必备优化点🔖
    • Flutter - 我给官方提PR,解决run命令卡住问题 😃
    • Flutter - 探索run命令到底做了什么 🤔
    • Flutter - 引擎调试(iOS篇)🛠
    • Flutter - 引擎调试bug到提交PR实战 🐞
    • Flutter - 船新升级😱支持观察第三方构建的滚动视图💪
    • Flutter - 瀑布流交替播放视频 🎞
    • Flutter - IM保持消息位置大升级(支持ChatGPT生成式消息) 🤖
    • Flutter - 滚动视图中的表单防遮挡 🗒
    • Flutter - 秒杀1/2曝光统计 📊
    • 一天内加入 Flutter 和 FlutterCandies 两大组织是什么体验 🧐
    • Flutter - 如何快速搓一个微信通讯录列表(azlist) 📓
    • Flutter - 混编项目集成Shorebird热更新🐦(安卓篇)
    • Flutter - 混编项目集成Shorebird热更新🐦(iOS篇)
      • 一、概述
      • 二、踩坑
        • 1、vendored_frameworks 缺失
        • 2、重复编译
        • 3、静态库与动态库
        • 4、直接崩溃
      • 三、分析
        • 1、installflutterengine_pod
        • 2、installflutterapplication_pod
        • 3、installflutterplugin_pods
      • 四、原生工程调整
      • 五、创建 Shorebird Release
      • 六、创建 Shorebird Patch
      • 七、热更新验证
      • 八、脚本
        • switchflutterintegrate.py
        • shorebird.py
      • 九、最后
    • Flutter - 解决返回原生页面时dispose方法未被触发的问题 🐞
    • Flutter - 升级3.19之后页面多次rebuild?🤨
    • Flutter - 热更新 Shorebird 1.0 正式版来了 🐦
    • Flutter - 使用Pigeon实现视频缓存插件 🐌
    • Flutter - 轻松搞定屏幕旋转功能 😎
    • Flutter - 解决Connection closed before full header was received
    • Flutter - 实现聊天键盘与功能面板的丝滑切换 🍻
    • Flutter - 支持观察NestedScrollView,兼容性更强 😈
    • Flutter - 聊天键盘与面板丝滑切换的强势升级 🍻
    • Flutter - 升级到3.24后页面还会多次rebuild吗?🧐
    • Flutter - 轻松实现PageView卡片偏移效果
    • Flutter - 轻松搞定炫酷视差(Parallax)效果
    • Flutter - 危!3.24版本苹果审核被拒!
    • Flutter - 子部件任意位置观察滚动数据
    • Flutter - iOS编译加速
    • Flutter - Xcode16 还原编译速度
  • 移动端
  • Flutter开发
LinXunFeng
2024-01-21
目录

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

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

# 一、概述

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

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

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

# 二、踩坑

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

关于 Flutter 官方文档指出的各种集成方式可以查看: https://docs.flutter.dev/add-to-app/ios/project-setup (opens new window)

具体的步骤就是:

  1. 先注释掉原来的 Option A 集成方式的相关配置代码
  2. 执行 Shorebird Release 去构建对应的所有 xcframework 文件。(xcframework 文件包括了 Flutter.xcframework 和 App.xcframework,以及插件依赖的原生第三方库对应的 xcframework)
  3. 将所有构建完成的 xcframework 拖到 Build Phases 中的 Embed Frameworks 内。
  4. 将 xcframework 的所在目录路径配置到 Framework Search Paths。
  5. 配置 xcframework 的 Embed 模式,静态库必须选 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: https://github.com/flutter/flutter/issues/125530 (opens new window)。

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

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 就会告诉你重复了,编译不通过,报错内容如下:

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: https://github.com/flutter/flutter/issues/122183

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

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 二进制嵌入的方式,否则运行就崩~

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.xcframework,App.xcframework 以及插件依赖的原生第三方库呢?

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

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,如:

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.xcframework 和 App.xcframework,并将它们并集到我们的原生工程内。不过这两玩意我们用 Shorebird Release 去生成了,所以这个方法我们用不上。

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

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 时是会报如下错误的:

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。

关键代码如下:

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 这一行代码,以解决上述错误。

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: https://github.com/flutter/flutter/pull/141521

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

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

# 四、原生工程调整

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

- 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.xcframework 和 App.xcframework。
  2. Option A 方式所需要的代码统统保留,只需要将 install_all_flutter_pods 替换为 install_flutter_plugin_pods,用于集成 Flutter 插件所依赖的原生第三方库

# 五、创建 Shorebird Release

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

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+2 的 Release,如果有的话,请先删除

# 六、创建 Shorebird Patch

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

shorebird patch ios-framework-alpha --release-version 7.0.0+2

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

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

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

一样的修改,安卓的补丁大小不到 2 MB,iOS 的补丁大小高达 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 改回 Release 用 Xcode 重新运行 App,一切正常的话即可看到变化。

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

成功

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 定位

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 (opens new window) 去延迟检查。

# 八、脚本

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

脚本已上传至 Github: https://github.com/LinXunFeng/script_box/tree/main/flutter (opens new window)

看官可自取修改~

# switch_flutter_integrate.py

切换 Flutter 项目的集成方式

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

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

# shorebird.py

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

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

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

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

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 中查看 https://github.com/shorebirdtech/shorebird/issues/871 (opens new window)

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

#Dart#Flutter#Shorebird
Flutter - 混编项目集成Shorebird热更新🐦(安卓篇)
Flutter - 解决返回原生页面时dispose方法未被触发的问题 🐞

← Flutter - 混编项目集成Shorebird热更新🐦(安卓篇) Flutter - 解决返回原生页面时dispose方法未被触发的问题 🐞→

最近更新
01
Flutter - Xcode16 还原编译速度
04-05
02
AI - 免费的 Cursor 平替方案
03-30
03
Android - 2025年安卓真的闭源了吗
03-28
更多文章>
Theme by Vdoing | Copyright © 2020-2025 FSA全栈行动
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×