在iOS项目中依赖Flutter Module-③本地podspec中转依赖远程Flutter编译产物

1,585 阅读8分钟

在上一篇# 在iOS项目中依赖Flutter Module-②远程依赖Git资源实现了将Flutter编译产物发布到远程Git仓库以及iOS端远程依赖Flutter编译产物的方案。但是这个方案有一个很大的问题,就是Flutter.xcframework文件太大,新版Flutter编译出来接近480M,如果随其它产物一起打上版本tag,每次发布到Git、从Git上拉取新版本都将耗费不少时间。由于我们并不需要改动Flutter Engine,所以每次编译导出的Flutter.xcframework都不会变,除非Flutter版本更新了。而App.xcframework和其它第三方插件库跟我们编写的Dart代码是关联的,每次编译都可能会变更。所以正常情况下,每次发布新版本产物,只需要发布除Flutter.xcframework以外的产物即可。

回顾# 在iOS项目中依赖Flutter Module-①本地依赖 - 章节3,可以选择编译导出Flutter.podspecFlutter.podspec依赖了远程服务器上的Flutter.xcframework压缩文件,并且支持CDN加速,压缩后不到原文件的一半大小,首次下载下来也比拉取Git快。Flutter.podspec的版本号对应本机上的Flutter版本号,基本不会变化,CocoaPods根据版本号缓存资源,就不会反复下载更新。相比将Flutter.xcframework放到Git上,通过本地的Flutter.podspec中转依赖Flutter.xcframework.zip,不仅减少Git服务器的存储压力,还能减少不必要的时间损耗,能明显缩短整个流程的时间。

围绕本地podspec中转依赖远程Flutter,编译产物,我尝试了几种方案,最终我选择了本地podspec中转依赖文件服务器上的Flutter.xcframework.zip + 本地podspec中转依赖Git上的其它.xcframework,充分发挥远程zip文件的下载优势和Git版本管理的优势。

也可以自行搭建文件服务器,将其它的xcframework压缩成zip文件上传到文件服务器,像Flutter.podspec一样通过本地podspec中转依赖zip文件。但是不好做版本控制和版本回滚,文件服务器上面也会不断累计旧版本的编译产物,对于APP端开发者而言搭文件服务器也有点学习成本,整体而言我觉得没`远程zip & git混合依赖`的方案好用,后面就不做介绍了,如果感兴趣,就进传送门,可以找我要Node.js搭文件服务器的代码,网上找也可以。

如果想看更多的试验方案,请进传送门🚪

准备阶段

为了方便测试代码,我把ios_module/flutter_module/andriod_module放在了一个工作区目录下。ios_module就是iOS项目所在目录,整体目录结构如下:

some/path/ 
├── andriod_module
│   ├── ...
├── flutter_module
│   ├── ...
├──ios_module
│   ├── ...

先创建一个Flutter Module,随便加点flutter代码和第三方组件(比如flutter_boost);

cd some/path/ 
flutter create --template module flutter_module

创建一个临时目录flutter_build/Frameworks/,用于存放编译导出的产物,用完即删,至此形成的目录结构如下;

some/path/ 
├── andriod_module
│   ├── ...
├── flutter_build
│   ├── Frameworks
├── flutter_module
│   ├── README.md
│   ├── build
│   ├── flutter_module.iml
│   ├── flutter_module_android.iml
│   ├── lib
│   ├── pubspec.lock
│   ├── pubspec.yaml
│   └── test
├──ios_module
    ├── FlutterBoostPro
    ├── FlutterBoostPro.xcodeproj
    ├── FlutterBoostPro.xcworkspace
    ├── Podfile
    ├── Podfile.lock
    └── Pods

创建Git仓库FlutterModuleSDK.git,用来存放除Flutter.xcframework以外的编译产物,建好git之后克隆到本机(可以是工作区some/path/下)。

然后创建Git仓库FlutterModuleSDKPodspec.git,建好git之后克隆到本机(可以是工作区some/path/下),用来存放Flutter.podspecFlutterModuleSDK.podspecFlutter.podspec是编译导出的文件,FlutterModuleSDK.podspec需要我们自行创建,并指定依赖前面创建的FlutterModuleSDK.git,文件内容如下:

Pod::Spec.new do |s|
  s.name                  = 'FlutterModuleSDK'
  s.version               = '1.0.1'
  s.summary               = 'Flutter Module SDK'
  s.source                = { :git => 'https://a.gitlab.cn/flutter/FlutterModuleSDK.git', :tag => "#{s.version}" }
  s.platform              = :ios, '8.0'
  s.requires_arc          = true
  s.vendored_frameworks   = '*.xcframework'
end

最后形成的目录结构如下

井号 # 代表目前不存在,编译后才会有,先占个位而已

some/path/ 
├── andriod_module
│   ├── ...
├── flutter_build
│   ├── Frameworks
│   │   ├── # Debug
│   │   ├── # Profile
│   │   ├── # Release
├── flutter_module
│   ├── README.md
│   ├── build
│   ├── flutter_module.iml
│   ├── flutter_module_android.iml
│   ├── lib
│   ├── pubspec.lock
│   ├── pubspec.yaml
│   └── test
├──ios_module
│   ├── FlutterBoostPro
│   ├── FlutterBoostPro.xcodeproj
│   ├── FlutterBoostPro.xcworkspace
│   ├── Podfile
│   ├── Podfile.lock
│   └── Pods
├──FlutterModuleSDK
│   ├── # App.xcframework、FlutterPluginRegistrant.xcframework和其它.xcframework
├──FlutterModuleSDKPodspec
│   ├── FlutterModuleSDK.podspec
│   ├── # Flutter.podspec   

编译

先编译flutter_module,指定导出目录是flutter_build/Frameworks/output的参数是相对路径。编译失败的话要么是Dart代码问题,要么是.ios/Runner.xcworkspace的开发证书没配置好。

cd flutter_module/
flutter build ios-framework --cocoapods --xcframework --no-universal --output=../flutter_build/Frameworks/

编译成功后,flutter_build/Frameworks/目录下面会新增以下文件夹。

├── flutter_build
│   ├── Frameworks
│   │   ├── Debug
│   │   ├── Profile
│   │   ├── Release

20210706145922.jpg

处理产物

App.xcframework、FlutterPluginRegistrant.xcframework和其它.xcframework移到FlutterModuleSDK.git本地目录下;

├──FlutterModuleSDK
│   ├── App.xcframework
│   ├── FlutterPluginRegistrant.xcframework
│   ├── 其它.xcframework,比如flutter_boost.xcframework

Flutter.podspec移到FlutterModuleSDKPodspec.git本地目录下(并不需要修改这个文件,直接用);

├──FlutterModuleSDKPodspec
│   ├── FlutterModuleSDK.podspec
│   ├── Flutter.podspec

修改FlutterModuleSDK.podspec中的版本号;

发布产物

提交FlutterModuleSDK.git中的更新,打上版本tag,push到远程仓库;

提交FlutterModuleSDKPodspec.git中的更新,push到远程仓库;

清空删除flutter_build/Frameworks/

发布版本更新通知(钉钉、微信、邮件);

iOS端拉取更新

同步刚刚发布的*.podspec到iOS开发机上,第一次需要克隆FlutterModuleSDKPodspec.git到本地,后面只需要拉取更新FlutterModuleSDKPodspec.git即可;

在Podfile中添加本地依赖,:podspec指定FlutterModuleSDKPodspec.git中对应的.podspec文件,并使用相对路径;

  pod 'Flutter', :podspec => './../FlutterModuleSDKPodspec/Flutter.podspec'
  pod 'FlutterModuleSDK', :podspec => './../FlutterModuleSDKPodspec/FlutterModuleSDK.podspec'

pod update or install,运行代码测试;

尽量在.gitignore文件中忽略掉编译期间导入的.xcframework,不然一并提交到Git会很慢,还明显增加Git仓库的文件大小。

由于Flutter.podspec的版本号基本不会变,只有第一次下载zip文件会耗费点时间,CocoaPods缓存以后执行更新都会非常快。而FlutterModuleSDK.git里面的文件并不大,在内网Git的话每次更新都会比较快。所以相比将Flutter.xcframework放到Git上,通过本地的Flutter.podspec中转依赖Flutter.xcframework.zip,不仅减少Git服务器的存储压力,还能减少不必要的时间损耗,能明显缩短整个流程的时间。 另外这种方案也不用iOS同事安装Flutter开发环境,依赖Flutter侧代码就跟依赖第三方库一样,由于*.xcframework已经编译过了,编译iOS项目时不需要再编译Flutter代码,相比本地依赖就要快得多。

但这个方案也有个短板,如果iOS团队人多的话,要保证所有人在Podfile中填的:podspec相对路径都是一样的,也就是要求在每台开发机上,本地FlutterModuleSDKPodspec.git相对本地ios_module.git的路径是一致的。如果跟其他人填的相对路径不一致,提交到Git就会发生冲突。当然也可以禁止大家在克隆git时重命名本地仓库名,并且把FlutterModuleSDKPodspec.gitios_module.git在同一个目录下,只是时间一久,难免会遗忘这个规则,另外还得跟新人强调这个规则。

改良

前面说了要保持相对路径一致,而FlutterModuleSDKPodspec.gitios_module.git是纯人为建立并维护的关联关系,时间久了容易遗忘这个共识,所以需要更强烈的绑定关系来解决这个隐患。我找到的方案是借助git submoduleFlutterModuleSDKPodspec.git"内嵌"到ios_module.git中,成为子模块,还能让FlutterModuleSDKPodspec.git保持独立的Git仓库。

关于git submodule的介绍可以看某大佬整理的《Git中submodule的使用》

所以只需要修改iOS端拉取更新流程,在ios_module.git中增加git submodule,解决相对路径难以保持一致的问题,前面的编译、发布流程都不用改,FlutterModuleSDKPodspec.git还是独立的仓库。

iOS端拉取更新-改良版

首先在ios_module.git中添加子模块FlutterModuleSDKPodspec.git

  cd some/path/ios_module
  git submodule add 'https://a.gitlab.cn/flutter/FlutterModuleSDKPodspec.git'

添加子模块后,主要会遇到两种情况,首先是新同事首次克隆ios_module.git,然后是后续的子模块更新。

  • 如果是新同事首次克隆ios_module.git,可能会遇到2种情况。
    • 1.常规的克隆指令,可能指定分支,但没有拉取submodule子模块,就需要额外执行指令去递归拉取更新子模块。
      git clone -b a_branch https://a.gitlab.cn/ios/ios_module.git 
      git submodule update --init --recursive
    
    • 2.克隆的同时指定递归拉取更新子模块,则需要在指令中加上--recurse-submodules
      git clone -b a_branch https://a.gitlab.cn/ios/ios_module.git --recurse-submodules
    
  • FlutterModuleSDKPodspec.git发布更新后,需要在ios_module.git中更新本地子模块,加上foreach可以帮助我们遍历所有的子模块。
  git submodule foreach 'git pull origin master'

如果更新了子模块(包括添加子模块),都会在ios_module.git产生modify,所以还需要额外提交更新到ios_module.git

  git status
  # 下面2步可以在SourceTree之类的客户端操作,是可以看到submodule的
  git add -A && git commit -m "添加或更新子模块 FlutterModuleSDKPodspec.git"
  git push origin --tags && git push origin master 

然后在Podfile中添加本地依赖,:podspec指定FlutterModuleSDKPodspec.git中对应的.podspec文件,并使用子模块的相对路径

  pod 'Flutter', :podspec => './FlutterModuleSDKPodspec/Flutter.podspec'
  pod 'FlutterModuleSDK', :podspec => './FlutterModuleSDKPodspec/FlutterModuleSDK.podspec'

pod update or install,运行代码测试;

尽量在.gitignore文件中忽略掉编译期间导入的.xcframework,不然一并提交到Git会很慢,还明显增加Git仓库的文件大小。

综上所述,改良版就是把更新依赖的FlutterModuleSDKPodspec.git改成了更新子模块,而FlutterModuleSDKPodspec.git以子模块嵌入ios_module.git后路径是固定的,这样就能保住每台电脑上这2个git的相对路径是始终一致的,不用担心人为因素导致路径不一致的问题。

至此就实现了在iOS项目中远程依赖Flutter Module编译产物,特别感谢闲鱼团队分享的《Flutter in action》,远程依赖的思路也是从中获取的,只是不知道闲鱼团队具体是怎么实现的,所以花了不少时间在找合适的方案,试了几种方案,有可行的也有行不通的方案,最后选择了Git子模块 + 本地podspec中转远程zip & git的方案,具体的特点就是下面这些。总体而言,这个方案充分发挥了远程zip文件的下载优势和Git版本管理的优势,适合大小团队使用,技术难度低,关键能为iOS端节省很多编译时间,这一点特别实用。

  • 将*.xcframework发布到FlutterModuleSDK.git中
  • 将*.podspec发布FlutterModuleSDKPodspec.git中
  • 将FlutterModuleSDKPodspec.git 以子模块的形式内嵌到 ios_module.git中
  • 通过子模块的Flutter.podspec中转依赖远程服务器上的zip文件
  • 通过子模块的FlutterModuleSDK.podspec中转依赖远程Git上的*.xcframework