iOS 一步步带你实践组件二进制方案
前言
随着业务的扩展、项目体积的增大,CocoaPods
组件库越来越多,每次重新编译的时候速度越来越慢,这给我们提出了需要提高编译速度的需求。
为了提高项目编译速度,对于大量使用组件化开发的项目组而言,组件二进制化是必然要走的路线,虽然中心思想就是要将各个组件打包成.a
二进制库,但是各个公司可能方案都不太相同,网上的方案也有很多可供选择,这里我大体总结成以下几种:
- 分仓库管理
-
Carthage
管理 -
podspec
环境变量(宏管理) -
podspec
分tag
管理(只针对私有库)
前两个就不在这里讨论了可以看看这篇讲解。今天重点给大家分享一下第三和第四种方案的实施,但是目前只能针对私有库实施,对于一些第三方的公有库目前没有什么好的方案(😁 有好方法的同学可以在评论区推荐一下)。
实施
1、创建pod私有库
😝 如果您对这一块很了解请跳过这一步直接看第二步
对于私有库的创建,一般我们会采用pod lib create XXX
模板来进行构建(如果还不知道这条命令是干嘛的同学可以先移步了解一下理解CocoaPods的Pod Lib Create)
这里我们拿ABC
这个项目进行举例,首先我们执行pod lib create ABC
创建ABC
的私有库
CocoaPods
会从https://github.com/CocoaPods/pod-template.git
下载模板文件,并询问你一些构建信息,正常填就好了。
[MichaeldeMacBook-Pro:~ michaelwu$ pod lib create ABC
Cloning `https://github.com/CocoaPods/pod-template.git` into `ABC`.
Configuring ABC template.
------------------------------
To get you started we need to ask a few questions, this should only take a minute.
If this is your first time we recommend running through with the guide:
- https://guides.cocoapods.org/making/using-pod-lib-create.html
( hold cmd and double click links to open in a browser. )
What platform do you want to use?? [ iOS / macOS ]
>
一般如果我们构建好了的话工程目录会类似这样一个结构:
.
├── ABC
│ ├── Assets
│ └── Classes
├── ABC.podspec
├── Example
│ ├── ABC
│ ├── ABC.xcodeproj
│ ├── ABC.xcworkspace
│ ├── Podfile
│ ├── Podfile.lock
│ ├── Pods
│ └── Tests
├── LICENSE
├── README.md
└── _Pods.xcodeproj -> Example/Pods/Pods.xcodeproj
这里你会发现,CocoaPods
已经帮我们创建好了Demo
、源文件目录、Podfile
、podspec
、.gitignore
文件等(真是一个贴心的小家伙),而且很规范,Demo
文件在Example
目录下
窥视一下podspec
文件你就明白了源码需要指定在./Classes/**/*
路径下
s.source_files = 'ABC/Classes/**/*'
为了演示效果,我们创建两个源文件ABC.h
与ABC.m
并放入Classes
路径下,同时将默认的ReplaceMe.m
删除
接着在Example
下执行pod install
,可以发现ABC.h/m
已经导入成功
至此,我们就明白了私有库的创建过程,需要编写源代码需要放入指定目录下并在执行pod install
进行同步
2、创建静态库
组件二进制其实指的就是打包成动态库/静态库,由于过多的动态库会导致启动速度减慢得不偿失,此外iOS
对于动态库的表现形式只有framework
,若想做源码与二进制切换时,引入头文件的地方也不得不进行更改,例如:
import <ABC.h> // 源码引用
import <ABCBinary/ABC.h> // 动态库引用
而打包成静态库.a
文件(注意不要打包成framework
形式)则不需要更改引用代码,所以综上所述,我们选择打包成静态库的方式不需修改引用代码、缩小体积提升编译速度。
确定目标之后,就是实施了,一般而言我们私有库都会在远程托管地址有git
仓库,然后再上传到指定的私有源(specs)上,那么就会引申出几个问题:
- 要不要将静态库上传到
git
(如果包体积很大会很占用git
空间) - 怎么做到一套代码同时管理源码和二进制
- 为了能够调试源码,如何在源码及二进制间切换(下一步骤会讲到)
针对这几个问题,一一回答:
3、静态库与源码如何用同一套代码管理?
其实这个很简单,我们接着拿ABC
这个项目举例子,进入Example
打开我们的ABC.xcworkspace
工程,然后创建新的Target
为静态库,并取名为ABCBinary
(一定要取这个名字,后面我会解释)
File->New->Target->Static Library
此时在Example
目录下会增加刚刚创建的Target
文件夹,结构如下:
├── ABCBinary
│ ├── ABCBinary.h
│ └── ABCBinary.m
Xcode默认会帮我们生成两个文件,我们将.h
改名为placeholder.h
,.m
删除,这里为什么要将.h
换成placeholder.h
呢?先卖个关子,待会我们再作解释。
我们把刚才写的ABC.h/m
的源码拖到ABCBinary
中,注意不要勾选Copy items if needed
,只做引用即可
之后我们需要到ABCBinary
的Build Setting
中指定静态库所能运行的最低版本:
Build Setting->Deployment->iOS Deployment Target
并在Build Phases
中指定头文件,将ABC.h
拖入Public中,具体步骤:
TARGETS->ABCBinary->Build Phases->New Header Phase
至此我们完成了一套代码管理二进制与源码,但有个小细节需要注意:就是如果源代码有变动需要在XXXBinary
文件中重新导入一遍,不然二进制的文件不会自动更新(同学们有好的建议可以评论区讨论下)
3、是否需要将二进制上传至git?
其实git
对代码管理时会将不同的diff
做备份(在.git
这个文件夹下),但是对于二进制文件来说git
就没用那么友好了,会将二进制的每一次提交都做磁盘备份,以便于随时版本回滚,倘若我们每次都对私有库进行更新时都将二进制包传至git
,那么时间久了无疑是对git
仓库空间的一个挑战(如果你们公司空间足够大不需要考虑,那么请忽略这一步)
网上有很多针对这个问题给出的解决方案,但都不是很完美,大体上都是说将二进制包
单独传到另一份静态资源地址,以此解决git
过大问题,不过我觉得没有解决痛点,能不能不上传二进制包呢?
结论当然是可以,CocoaPods
本地的缓存目录在
~/Library/Caches/Cocoapods
其实每次我们更新pod
库时,CocoaPods
都会先从指定源去拉源代码再根据该库的podspec
文件指定输出目标文件,那么我们如果能把静态库打包推迟到pod install
阶段就不需要上传二进制包到git
了,但是如何做到延迟打包呢?
很幸运,CocoaPods
提供了针对podspec
的预执行脚本,prepare_command(戳我进官网)命令,该命令可以指定相应的脚本在pod install
时去执行,那么我们就可以将编译打包的脚本放入其中,从而完成延迟打包
好了,理论上貌似可行了,实践出真知啊(😄 绝对不能做一个理论性选手啊),具体怎么做?
首先我们需要一个能一键打静态库包的脚本(一刀99级那种),帅气的我这边已经为大家准备好了,只修改一下PROJECT_NAME
即可,拷贝脚本至根目录并赋予执行权限:
# 当前项目名字,需要修改!
PROJECT_NAME='ABC'
# 编译工程
BINARY_NAME="${PROJECT_NAME}Binary"
cd Example
INSTALL_DIR=$PWD/../Pod/Products
rm -fr "${INSTALL_DIR}"
mkdir $INSTALL_DIR
WRK_DIR=build
BUILD_PATH=${WRK_DIR}
DEVICE_INCLUDE_DIR=${BUILD_PATH}/Release-iphoneos/usr/local/include
DEVICE_DIR=${BUILD_PATH}/Release-iphoneos/lib${BINARY_NAME}.a
SIMULATOR_DIR=${BUILD_PATH}/Release-iphonesimulator/lib${BINARY_NAME}.a
RE_OS="Release-iphoneos"
RE_SIMULATOR="Release-iphonesimulator"
xcodebuild -configuration "Release" -workspace "${PROJECT_NAME}.xcworkspace" -scheme "${BINARY_NAME}" -sdk iphoneos clean build CONFIGURATION_BUILD_DIR="${WRK_DIR}/${RE_OS}" LIBRARY_SEARCH_PATHS="./Pods/build/${RE_OS}"
xcodebuild ARCHS=x86_64 ONLY_ACTIVE_ARCH=NO -configuration "Release" -workspace "${PROJECT_NAME}.xcworkspace" -scheme "${BINARY_NAME}" -sdk iphonesimulator clean build CONFIGURATION_BUILD_DIR="${WRK_DIR}/${RE_SIMULATOR}" LIBRARY_SEARCH_PATHS="./Pods/build/${RE_SIMULATOR}"
if [ -d "${INSTALL_DIR}" ]
then
rm -rf "${INSTALL_DIR}"
fi
mkdir -p "${INSTALL_DIR}"
cp -rp "${DEVICE_INCLUDE_DIR}" "${INSTALL_DIR}/"
INSTALL_LIB_DIR=${INSTALL_DIR}/lib
mkdir -p "${INSTALL_LIB_DIR}"
lipo -create "${DEVICE_DIR}" "${SIMULATOR_DIR}" -output "${INSTALL_LIB_DIR}/lib${PROJECT_NAME}.a"
rm -r "${WRK_DIR}"
我们还是拿ABC
的项目来接着实践,拷贝脚本后,先来看一下我们ABC
目前的结构:
.
├── ABC
│ ├── Assets
│ └── Classes
├── ABC.podspec
├── Example
│ ├── ABC
│ ├── ABC.xcodeproj
│ ├── ABC.xcworkspace
│ ├── ABCBinary
│ │ └── placeholder.h
│ ├── Podfile
│ ├── Podfile.lock
│ ├── Pods
│ └── Tests
├── LICENSE
├── README.md
├── _Pods.xcodeproj -> Example/Pods/Pods.xcodeproj
└── build_lib.sh
可以看到最下面多了一个build_lib.sh
脚本(就是刚刚拷贝的那个脚本),另外ABCBinary
里面有一个placeholder.h
,这里解释一下之前埋下的悬念:因为ABCBinary
文件夹里对于源码的引用没有copy
,所以在提交到git
时会自动将文件夹清空(也就是说在git目录里找不到),因此需要加一个占位防止文件夹不上传到git
,但是切记不要编译到静态库里!
好的,至此一键打包脚本也准备好了,通过查看脚本我们发现这个二进制包最终会输出到根目录下的./Pod/Products/
目录中,那不还是得传到git
吗?别急,你忘了gitignore
了吗?
配置.gitignore
忽略Pod/
文件不就行了嘛,在.gitignore
最下面增加忽略
Pod/
好了至此,我们完成了自动打包脚本及git
忽略二进制包,再也不用担心我们的git
仓库空间压力了(运维小哥哥们表示“尼玛松了一口气”)
4、如何在源码与二进制间切换
在提升编译速度的前提下,还需要考虑到能随时进行源码调试,这就涉及到了如何在源码与二进制间切换的问题,网上的思路有很多:环境变量、白名单、tag切换等。
这几种方式在前言部分我们已经讲过了,接下来我们介绍一下“环境变量”和“tag切换”这两种方式:
4.1、 如何利用tag进行切换:
首先我们需要约定好规则:当version
中包含.Binary
关键字时执行prepare_command
命令并输出source
为静态库,具体操作如下(podspec
是用ruby
写的,支持条件判断):
if s.version.to_s.include?'Binary'
puts '-------------------------------------------------------------------'
puts 'Notice:ABC is binary now'
puts '-------------------------------------------------------------------'
s.prepare_command = '/bin/bash build_lib.sh'
s.source_files = 'Pod/Products/include/**'
s.ios.vendored_libraries = 'Pod/Products/lib/*.a'
s.public_header_files = 'Pod/Products/include/*.h'
else
s.source_files = 'ABC/Classes/**/*'
end
由于tag
是根据version
走的(tag => s.version.to_s
),因此只需要我们修改s.version = '0.1.0.Binary'
即可实现二进制打包
好,我们贴一段此时ABC.podspec
完整的代码:
Pod::Spec.new do |s|
s.name = 'ABC'
s.version = '0.1.0.Binary'
s.summary = 'A short description of ABC.'
s.description = <<-DESC
TODO: Add long description of the pod here.
DESC
s.homepage = 'https://github.com/609223770@qq.com/ABC'
# s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { '609223770@qq.com' => '609223770@qq.com' }
s.source = { :git => 'https://github.com/609223770@qq.com/ABC.git', :tag => s.version.to_s }
# s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'
s.ios.deployment_target = '8.0'
if s.version.to_s.include?'Binary'
puts '-------------------------------------------------------------------'
puts 'Notice:ABC is binary now'
puts '-------------------------------------------------------------------'
s.prepare_command = '/bin/bash build_lib.sh'
s.source_files = 'Pod/Products/include/**'
s.ios.vendored_libraries = 'Pod/Products/lib/*.a'
s.public_header_files = 'Pod/Products/include/*.h'
else
puts '-------------------------------------------------------------------'
puts 'Notice:ABC is source code now'
puts '-------------------------------------------------------------------'
s.source_files = 'ABC/Classes/**/*'
end
end
让我们来看看效果,在Example
下执行pod install
,发现切换过来了,Nice 😝~
接下来验证本地podspec
(若有问题按照提示更改,ssh://xxx.git
是你私有源的地址):
pod lib lint --sources=ssh://xxx.git --allow-warnings --verbose --use-libraries
若没问题,在ABC
的git
仓库打一个0.1.0
的版本tag
,并上传ABC.podspec
至私有源,上传成功后修改podspec.version
为0.1.0.Binary
再次执行上传:
pod repo push XXXSpecs ABC.podspec --allow-warnings --verbose --use-libraries
✅ 如果一切顺利,我们已经将Binary和源码的ABC
上传到了私有源。
接下来我们在实际项目实验一下,Podfile
中指定,并执行安装
pod 'ABC', '~> 0.1.0' # source code
pod install
不出意外源码ABC
安装成功,这时我们修改tag
版本后面加.Binary
,再次执行pod install
,如下所示:
pod 'ABC', '~> 0.1.0.Binary' # source code
pod install
很遗憾,你可能会发现源码并没有切换成功,为什么呢?
原来Pod
的版本管理是放在Podfile.lock
中,每次执行pod install
时若Podfile.lock
中已经存在此库,则只下载Podfile.lock
文件中指定的版本进行安装,否则去搜索这个pod
库在Podfile
文件中指定的版本来安装。
因此,解决办法有两种,一种是从Podfile.lock
中将包含ABC
的地方全部删除或是干脆直接删除Podfile.lock
,再次执行pod install
会发现切换变过来了。
还有一种方法是执行pod update
,这也是 update 和 install 的区别,update会读取Podfile
中的版本去更新Podfile.lock
文件。(戳我查看pod install和pod update区别)
pod update ABC
执行后,先是会更新一下master和其他私有源,再去更新ABC
,发现此时切换成功。(缺点就是如果Podfile
中如果某些库没有指定版本就会更新到最新版本)
4.2、如何利用Ruby环境变量进行切换:
Ruby语法支持一些环境变量的读取,因此可以在pod install
时增加参数以此判断是否要切换源码:
IS_BINARY=1 pod install # 1 代表二进制
IS_BINARY=0 pod install # 0 代表源码
pod install # 默认也是0 源码
在podspec
中做修改:
Pod::Spec.new do |s|
s.name = 'ABC'
s.version = '0.1.0.Binary'
s.summary = 'A short description of ABC.'
s.description = <<-DESC
TODO: Add long description of the pod here.
DESC
s.homepage = 'https://github.com/609223770@qq.com/ABC'
# s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2'
s.license = { :type => 'MIT', :file => 'LICENSE' }
s.author = { '609223770@qq.com' => '609223770@qq.com' }
s.source = { :git => 'https://github.com/609223770@qq.com/ABC.git', :tag => s.version.to_s }
# s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'
s.ios.deployment_target = '8.0'
if s.version.to_s.include?'Binary' or ENV['IS_BINARY']
puts '-------------------------------------------------------------------'
puts 'Notice:ABC is binary now'
puts '-------------------------------------------------------------------'
s.prepare_command = '/bin/bash build_lib.sh'
s.source_files = 'Pod/Products/include/**'
s.ios.vendored_libraries = 'Pod/Products/lib/*.a'
s.public_header_files = 'Pod/Products/include/*.h'
else
puts '-------------------------------------------------------------------'
puts 'Notice:ABC is source code now'
puts '-------------------------------------------------------------------'
s.source_files = 'ABC/Classes/**/*'
end
end
同tag切换一样,这种方式在实际项目中切换也存在问题,需要两个必要步骤:
pod cache clean ABC # 先清理ABC的pod缓存
rm Pods/ABC # 再把ABC从实际项目中的Pods目录下移除
5、对比两种方式
方式 | 优点 | 缺点 |
---|---|---|
Ruby环境变量切换 | 1、不需要上传两份podspec 2、切换时不需要修改Podfile |
1、需要清除私有库的缓存 2、需要手动删除/Pods/XXX 3、不能针对单独库进行切换,除非自定义白名单之类的规则 |
tag切换 | 1、可以针对单独某个库进行切换 | 1、需要执行pod update(需等待repo master源的更新) 2、私有库的tag需要打两个,podspec上传时需要传两次 3、切换时需要手动修改Podfile文件的版本信息 |
6、总结
好,至此切换tag
方式的组件二进制方案就介绍完了,我们通过ABC
项目的实践了解了整个过程:
- 创建pod私有库
- 在私有库Demo中创建静态库target,并配置头文件及最低iOS版本支持
- 创建打包脚本
- 设置
.gitignore
忽略输出的二进制包 - 配置podspec根据tag版本判断或根据环境变量判断
- 验证并上传源码及二进制的podspec
- 在实际项目中切换时需要执行
pod update
或删除Podfile.lock
中相关库信息
7、链接
本文demo相关链接如下,另附自动上传podspec脚本地址(相关文章),喜欢的朋友点个star
- 组件化方案demo地址:CocoaPodsBinary
- 自动上传podspec脚本:upload_podspec