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

前言

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

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

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

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

实施

一、建立pod私有库

😝 若是您对这一块很了解请跳过这一步直接看第二步github

对于私有库的建立,通常咱们会采用pod lib create XXX模板来进行构建(若是还不知道这条命令是干吗的同窗能够先移步了解一下理解CocoaPods的Pod Lib Createxcode

这里咱们拿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 ]
 > 

复制代码

通常若是咱们构建好了的话工程目录会相似这样一个结构:ruby

.
├── 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目录下bash

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

s.source_files = 'ABC/Classes/**/*'
复制代码

为了演示效果,咱们建立两个源文件ABC.hABC.m并放入Classes路径下,同时将默认的ReplaceMe.m删除 ssh

接着在Example下执行pod install,能够发现ABC.h/m已经导入成功

至此,咱们就明白了私有库的建立过程,须要编写源代码须要放入指定目录下并在执行pod install进行同步

二、建立静态库

组件二进制其实指的就是打包成动态库/静态库,因为过多的动态库会致使启动速度减慢得不偿失,此外iOS对于动态库的表现形式只有framework,若想作源码与二进制切换时,引入头文件的地方也不得不进行更改,例如:

import <ABC.h> // 源码引用
import <ABCBinary/ABC.h> // 动态库引用
复制代码

而打包成静态库.a文件(注意不要打包成framework形式)则不须要更改引用代码,因此综上所述,咱们选择打包成静态库的方式不需修改引用代码、缩小体积提高编译速度。

肯定目标以后,就是实施了,通常而言咱们私有库都会在远程托管地址有git仓库,而后再上传到指定的私有源(specs)上,那么就会引伸出几个问题:

  • 要不要将静态库上传到git(若是包体积很大会很占用git空间)
  • 怎么作到一套代码同时管理源码和二进制
  • 为了可以调试源码,如何在源码及二进制间切换(下一步骤会讲到)

针对这几个问题,一一回答:

三、静态库与源码如何用同一套代码管理?

其实这个很简单,咱们接着拿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文件中从新导入一遍,否则二进制的文件不会自动更新(同窗们有好的建议能够评论区讨论下)

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

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

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

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

5.一、 如何利用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.二、如何利用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目录下移除
复制代码

六、对比两种方式

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

七、总结

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

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

八、连接

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

相关文章
相关标签/搜索