欢迎重视微信大众号: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 运转起来进入一些相关场景,运用到了对应的第三方功用时就会直接来个找不到符号的过错,如:

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 中,有些是静态库,有些是动态库

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

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

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

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

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

所以为了避免这种状况,咱们就有必要得选对 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.xcframeworkApp.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.xcframeworkApp.xcframework,并将它们并集到咱们的原生工程内。不过这两玩意咱们用 Shorebird Release 去生成了,所以这个办法咱们用不上。

咱们能够结合上述的 Flutter.podspec 的作用,修正它内部的依靠声明,然后完成通过 Cocoapods 的办法来集成 Flutter.xcframeworkApp.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: github.com/flutter/flu…

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

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

四、原生工程调整

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.xcframeworkApp.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 引擎!

版本号能够在如下图所示进行检查

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

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

履行完结后,在 Shorebird 操控台上能够看到相应的项

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

在指令履行前,请确保不存在 7.0.0+2Release,假设有的话,请先删去

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

六、创立 Shorebird Patch

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

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

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

履行完结后,在 Shorebird 操控台上点击对应的 Release 项,进去后能够看到相应的补丁

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

看看这个补丁巨细,咱们再来看看安卓的补丁巨细

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

一样的修正,安卓的补丁巨细不到 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 的操控台都会有相应的输出

成功

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 去推迟检查。

八、脚本

因为咱们日常研发仍是运用的是源码依靠的办法,只会在打终究测验包时才需求去做上述的调整操作,所以这里用我比较熟悉的 Python 去制作了简易的脚本,并结合 Jenkins 来辅助完结这种万年不变的无聊步骤

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

看官可自取修正~

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

需求留意的是,xcodeprojtarget 的姓名被我固定写成 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 中检查 github.com/shorebirdte…

本篇到此结束,感谢咱们的支撑,咱们下次再见!

假设文章对您有所协助, 请不惜点击重视一下我的微信大众号:FSA全栈举动, 这将是对我最大的鼓励. 大众号不只有 iOS 技能,还有 AndroidFlutterPython 等文章, 可能有你想要了解的技能知识点哦~