iOS 一步步带你实践组件二进制方案

10,579 阅读7分钟

前言

随着业务的扩展、项目体积的增大,CocoaPods组件库越来越多,每次重新编译的时候速度越来越慢,这给我们提出了需要提高编译速度的需求。

为了提高项目编译速度,对于大量使用组件化开发的项目组而言,组件二进制化是必然要走的路线,虽然中心思想就是要将各个组件打包成.a二进制库,但是各个公司可能方案都不太相同,网上的方案也有很多可供选择,这里我大体总结成以下几种:

  • 分仓库管理
  • Carthage管理
  • podspec环境变量(宏管理)
  • podspectag管理(只针对私有库)

前两个就不在这里讨论了可以看看这篇讲解。今天重点给大家分享一下第三和第四种方案的实施,但是目前只能针对私有库实施,对于一些第三方的公有库目前没有什么好的方案(😁 有好方法的同学可以在评论区推荐一下)。

实施

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、源文件目录、Podfilepodspec.gitignore文件等(真是一个贴心的小家伙),而且很规范,Demo文件在Example目录下

窥视一下podspec文件你就明白了源码需要指定在./Classes/**/*路径下

 s.source_files = 'ABC/Classes/**/*'

为了演示效果,我们创建两个源文件ABC.hABC.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,只做引用即可

之后我们需要到ABCBinaryBuild Setting中指定静态库所能运行的最低版本:

Build Setting->Deployment->iOS Deployment Target

并在Build Phases中指定头文件,将ABC.h拖入Public中,具体步骤:

TARGETS->ABCBinary->Build Phases->New Header Phase

至此我们完成了一套代码管理二进制与源码,但有个小细节需要注意:就是如果源代码有变动需要在XXXBinary文件中重新导入一遍,不然二进制的文件不会自动更新(同学们有好的建议可以评论区讨论下)

4、是否需要将二进制上传至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仓库空间压力了(运维小哥哥们表示“尼玛松了一口气”)

5、如何在源码与二进制间切换

在提升编译速度的前提下,还需要考虑到能随时进行源码调试,这就涉及到了如何在源码与二进制间切换的问题,网上的思路有很多:环境变量、白名单、tag切换等。

这几种方式在前言部分我们已经讲过了,接下来我们介绍一下“环境变量”和“tag切换”这两种方式:

5.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

若没问题,在ABCgit仓库打一个0.1.0的版本tag,并上传ABC.podspec至私有源,上传成功后修改podspec.version0.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中如果某些库没有指定版本就会更新到最新版本)

5.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目录下移除

6、对比两种方式

方式 优点 缺点
Ruby环境变量切换 1、不需要上传两份podspec
2、切换时不需要修改Podfile
1、需要清除私有库的缓存
2、需要手动删除/Pods/XXX
3、不能针对单独库进行切换,除非自定义白名单之类的规则
tag切换 1、可以针对单独某个库进行切换 1、需要执行pod update(需等待repo master源的更新)
2、私有库的tag需要打两个,podspec上传时需要传两次
3、切换时需要手动修改Podfile文件的版本信息

7、总结

好,至此iOS组件二进制方案就介绍完了,我们通过ABC项目的实践了解了整个过程:

  • 创建pod私有库
  • 在私有库Demo中创建静态库target,并配置头文件及最低iOS版本支持
  • 创建打包脚本
  • 设置.gitignore忽略输出的二进制包
  • 配置podspec根据tag版本判断或根据环境变量判断
  • 验证并上传源码及二进制的podspec
  • 在实际项目中切换时需要执行pod update或删除Podfile.lock中相关库信息

8、链接

本文demo相关链接如下,另附自动上传podspec脚本地址(相关文章),喜欢的朋友点个star