加快编译速度-iOS组件二进制库/源码切换方案

移动端项目复杂到一定程度都会走上组件化的道路,组件一多就会出现联编缓慢的问题。Android项目可以通过gradle,依赖源码生成jar包,提高编译速度。对于Objective-C语言的项目,想要加速编译打包的速度,就需要将大量依赖的组件在打包的时候都使用静态库或者动态库依赖,加快编译链接速度,以满足持续集成或者是快速部署的要求。


iOS的项目进行组件化,往往会使用cocoapods包管理工具,该方案以此为基础。二进制库在iOS项目中,指的是静态库与动态库,当组件提供静态库或动态库的时候,可以加速项目编译与构建,因为静态库与动态库本身就是已经编译好的库文件,从而能达到加速的目的。

目标

  1. pod组件同时提供源码与二进制库
  2. 项目进行调试的时候,pod组件库切换为源码模式,方便开发进行源码断点调试
  3. 项目进行集成与构建的时候,pod组件库切换为二进制模式,加快编译构建速度
  4. 开发组件的时候不因为源码与二进制的切换方案而丧失依赖其他组件的能力

方案说明与步骤

对于某一个组件来说,组件的pod库应该包含源码和静态库或动态库两种文件,这样才能够在开发的过程中使用源码进行编译调试,在编译构建的时候使用静态库或动态库。关键问题在于如何切换,cocoapods的pod库是通过podfile文件指明依赖的库与对应版本,当使用pod install的时候,cocoapods会通过podfile文件,到cocoapods的中央仓库中找到该库对应的podspec文件,再通过podspec文件中的信息来构建pod库。
一个pod库的podspec文件如下:

Pod::Spec.new do |s|
  s.name             = 'HBAuthentication'
  s.version          = '0.1.6-beta5'
  s.summary          = '基础认证组件'
  s.description      = <<-DESC
iOS认证组件,相关文档请访问内部wiki:
http://***.***.com/member/Auth
                       DESC

  s.homepage         = 'http://***.***.com/Hbec_IOS_common/HBAuthentication'
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  s.author           = { 'Neo' => '***@***.com' }
  s.source           = { :git => 'git@***.com:Hbec_IOS_common/HBAuthentication.git', :tag => s.version.to_s }
  # s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'

  s.ios.deployment_target = '7.0'

  s.resource_bundles = {
    'HBAuthentication' => ['HBAuthentication/Assets/*.{png,cer,json,der,p12}']
  }
  s.source_files = 'HBAuthentication/Classes/**'
end

cocoapods从中央仓库拉取到对应版本的podsepc文件以后,通过s.source获得对应版本tag的代码git版本库,而后通过s.source_files、s.resource_bundles指明该库的源码文件与资源文件对应的路径,从而最终进行源码依赖构建编译。如果该库的为二进制库,则需要通过 s.public_header_files、s.ios.vendored_libraries来指明该库的二进制库的头文件、library文件的路径。所以,该方案要求一个pod库的工程文件中不仅仅要包含源代码文件,还要包含将源代码编译成静态库或者动态库的二进制文件,切换二进制库与源码的时机应该在 pod install 的时候,而表明是构建源码还是二进制库,则需要通过install的时候,修改podspec文件中的s.source_files、s.public_header_files、s.ios.vendored_bibraries属性,来切换该pod库包含的内容。因为podspec文件本身为ruby文件,我们可以利用ENV对象,来获取命令行中执行pod install时候传入的环境变量,例如可以在podspec文件中这样写:

  if ENV['SOURCECODE']
    s.source_files = 'HBAuthentication/Classes/**'
  else
    s.source_files = 'Example/HBAuthenticationBinary/Products/Binary-universal/include/**'
    s.public_header_files = '**/*.h'
    s.ios.vendored_libraries = '**/**.a'
  end

当在命令行中传入环境变量参数的时候 SOURCECODE=1 pod install 的时候,则podspec文件中if 语句通过ENV对象来获取SOURCECODE参数来表明不同的文件包含属性,从而能够切换该pod库源码或者二进制库。
通过cocoapods的环境变量来控制组件库spec文件的配置信息,后面会详细说到。通过以上的分析,那么该方案大体上分为这几个步骤:

  1. 创建pod项目
  2. 创建对应的二进制库target
  3. 生成与源码对应的二进制文件
  4. 设置pod库的podspec文件,切换源码和二进制库的配置
  5. 发布含有源码和二进制库的pod库

创建pod项目和创建对应的二进制库target

通过 pod lib create HBAuthentication 创建出的pod库项目,目录大概如下:

.
├── Example
│   ├── HBAuthentication
│   ├── HBAuthentication.xcodeproj
│   ├── HBAuthentication.xcworkspace
│   ├── Podfile
│   ├── Podfile.lock
│   ├── Pods
│   └── Tests
├── HBAuthentication
│   ├── Assets
│   └── Classes
├── HBAuthentication.podspec
├── LICENSE
├── README.md
├── _Pods.xcodeproj -> Example/Pods/Pods.xcodeproj
└── com.touker.hbauthentication.HBAuthentication.docset
    └── Contents

项目的源码包含在Class文件中,如下:

├── Assets
│   └── content.json
└── Classes
    ├── HBAuthAPI.h
    ├── HBAuthAPI.m
    ├── HBAuthBridge.h
    ├── HBAuthBridge.m
    ├── HBAuthInfo.h
    ├── HBAuthInfo.m
    ├── HBAuthObject.h
    ├── HBAuthObject.m
    ├── HBAuthStoreManager.h
    ├── HBAuthStoreManager.m
    ├── HBAuthUtil.h
    ├── HBAuthUtil.m

一个源码的pod库项目大概就是这样,现在需要创建对应的二进制库,以静态库为例,在项目中添加对应的静态库target:file->New->Target->iOS->Framework & Library->Cocoa Touch Static Library


target命名为:HBAuthenticationBinary,创建后项目目录如下:

.
├── Example
│   ├── HBAuthentication
│   ├── HBAuthentication.xcodeproj
│   ├── HBAuthentication.xcworkspace
|   ├── HBAuthenticationBinary
│   ├── Podfile
│   ├── Podfile.lock
│   ├── Pods
│   └── Tests
├── HBAuthentication
│   ├── Assets
│   └── Classes
├── HBAuthentication.podspec
├── LICENSE
├── README.md
├── _Pods.xcodeproj -> Example/Pods/Pods.xcodeproj
└── com.touker.hbauthentication.HBAuthentication.docset
    └── Contents

紧接着将class文件夹下的源文件添加到HBAuthenticationBinary的target目录下,添加的时候选择不复制,添加文件索引。


此时项目文件如下:


紧接着设置Build Phases中的Compile Source与想要对外暴露的Headers,如下:


静态库的源码设置已经完成。

生成与源码对应的二进制文件

静态库需要考虑支持的目标架构,arm架构或者是x86架构,前者用于真机后者用于模拟器调试,一般在不考虑静态库大小的情况下,可以将几种架构打成一个静态库,方便使用。可以通过xcodebuild命令行工具进行打包和架构合并,要生成.a文件,为了支持真机和模拟器的版本构建,通过xcodebuild与lipo工具,来生成支持x86、arm64、armv7的静态库,将此操作写成脚本,通过Aggregate Target来执行,脚本如下:

set -e
set +u
### Avoid recursively calling this script.
if [[ $UF_MASTER_SCRIPT_RUNNING ]]
then
exit 0
fi
set -u
export UF_MASTER_SCRIPT_RUNNING=1
### Constants.
# RESOURCE_BUNDLE="HBAuthenticationBinary"
# 静态库target对应的scheme名称
SCHEMENAME="HBAuthenticationBinary"
# .a与头文件生成的目录,在项目中的HBAuthenticationBinary目录下的Products目录中
BASEBUILDDIR=$PWD/${SCHEMENAME}/Products
rm -fr "${BASEBUILDDIR}"
mkdir "${BASEBUILDDIR}"
# 支持全架构的二进制文件目录
UNIVERSAL_OUTPUTFOLDER=${BASEBUILDDIR}/Binary-universal
# 支持真机的二进制文件目录
IPHONE_DEVICE_BUILD_DIR=${BASEBUILDDIR}/Binary-iphoneos
# 支持模拟器的二进制文件目录
IPHONE_SIMULATOR_BUILD_DIR=${BASEBUILDDIR}/Binary-iphonesimulator
### Functions
## List files in the specified directory, storing to the specified array.
#
# @param $1 The path to list
# @param $2 The name of the array to fill
#
##
list_files ()
{
    filelist=$(ls "$1")
    while read line
    do
        eval "$2[\${#$2[*]}]=\"\$line\""
    done <<< "$filelist"
}
### Take build target.
if [[ "$SDK_NAME" =~ ([A-Za-z]+) ]]
then
SF_SDK_PLATFORM=${BASH_REMATCH[1]} # "iphoneos" or "iphonesimulator".
else
echo "Could not find platform name from SDK_NAME: $SDK_NAME"
exit 1
fi
### Build simulator platform. (i386, x86_64)
# echo "========== Build Simulator Platform =========="
# echo "===== Build Simulator Platform: i386 ====="
# xcodebuild -project "${PROJECT_FILE_PATH}" -target "${TARGET_NAME}" -configuration "${CONFIGURATION}" -sdk iphonesimulator BUILD_DIR="${BUILD_DIR}" OBJROOT="${OBJROOT}" BUILD_ROOT="${BUILD_ROOT}" CONFIGURATION_BUILD_DIR="${IPHONE_SIMULATOR_BUILD_DIR}/i386" SYMROOT="${SYMROOT}" ARCHS='i386' VALID_ARCHS='i386' $ACTION
echo "===== 构建x86_64架构 ====="
xcodebuild -workspace "${PROJECT_NAME}.xcworkspace" -scheme "${SCHEMENAME}" -configuration "${CONFIGURATION}" -sdk iphonesimulator CONFIGURATION_BUILD_DIR="${IPHONE_SIMULATOR_BUILD_DIR}/x86_64" ARCHS='x86_64' VALID_ARCHS='x86_64' $ACTION
# Build device platform. (armv7, arm64)
echo "========== Build Device Platform =========="
echo "===== Build Device Platform: armv7 ====="
xcodebuild -workspace "${PROJECT_NAME}.xcworkspace" -scheme "${SCHEMENAME}" -configuration "${CONFIGURATION}" -sdk iphoneos CONFIGURATION_BUILD_DIR="${IPHONE_DEVICE_BUILD_DIR}/armv7" ARCHS='armv7 armv7s' VALID_ARCHS='armv7 armv7s' $ACTION
echo "===== Build Device Platform: arm64 ====="
xcodebuild -workspace "${PROJECT_NAME}.xcworkspace" -scheme "${SCHEMENAME}" -configuration "${CONFIGURATION}" -sdk iphoneos CONFIGURATION_BUILD_DIR="${IPHONE_DEVICE_BUILD_DIR}/arm64" ARCHS='arm64' VALID_ARCHS='arm64' $ACTION
### Build universal platform.
echo "========== Build Universal Platform =========="
## Copy the framework structure to the universal folder (clean it first).
rm -rf "${UNIVERSAL_OUTPUTFOLDER}"
mkdir -p "${UNIVERSAL_OUTPUTFOLDER}"
## Copy the last product files of xcodebuild command.
cp -R "${IPHONE_DEVICE_BUILD_DIR}/arm64/lib${SCHEMENAME}.a" "${UNIVERSAL_OUTPUTFOLDER}/lib${SCHEMENAME}.a"
### Smash them together to combine all architectures.
lipo -create "${IPHONE_SIMULATOR_BUILD_DIR}/x86_64/lib${SCHEMENAME}.a" "${IPHONE_DEVICE_BUILD_DIR}/armv7/lib${SCHEMENAME}.a" "${IPHONE_DEVICE_BUILD_DIR}/arm64/lib${SCHEMENAME}.a" -output "${UNIVERSAL_OUTPUTFOLDER}/lib${SCHEMENAME}.a"

echo "========== Create Standard Structure =========="
cp -r "${IPHONE_DEVICE_BUILD_DIR}/arm64/usr/local/include/" "${UNIVERSAL_OUTPUTFOLDER}/include/"
# mkdir -p "${UNIVERSAL_OUTPUTFOLDER}/lib/"
# cp "${UNIVERSAL_OUTPUTFOLDER}/lib${SCHEMENAME}.a" "${UNIVERSAL_OUTPUTFOLDER}/lib/lib${SCHEMENAME}.a"

将该脚本保存问build.sh,在Aggregate Target中设置Build Phases的Run Script,如下:


将静态库文件和头文件输出到 HBAuthenticationBinary/Products 目录。
运行Aggregate Target以后,HBAuthenticationBinary目录结构如下:

├── HBAuthenticationBinary
│   └── Products
│       ├── Binary-iphoneos
│       │   ├── arm64
│       │   │   ├── libHBAuthenticationBinary.a
│       │   │   ├── libPods-HBAuthenticationBinary.a
│       │   │   └── usr
│       │   │       └── local
│       │   │           └── include
│       │   │               ├── HBAuthAPI.h
│       │   │               ├── HBAuthBridge.h
│       │   │               ├── HBAuthInfo.h
│       │   │               ├── HBAuthObject.h
│       │   │               └── HBAuthStoreManager.h
│       │   └── armv7
│       │       ├── libHBAuthenticationBinary.a
│       │       ├── libPods-HBAuthenticationBinary.a
│       │       └── usr
│       │           └── local
│       │               └── include
│       │                   ├── HBAuthAPI.h
│       │                   ├── HBAuthBridge.h
│       │                   ├── HBAuthInfo.h
│       │                   ├── HBAuthObject.h
│       │                   └── HBAuthStoreManager.h
│       ├── Binary-iphonesimulator
│       │   └── x86_64
│       │       ├── libHBAuthenticationBinary.a
│       │       ├── libPods-HBAuthenticationBinary.a
│       │       └── usr
│       │           └── local
│       │               └── include
│       │                   ├── HBAuthAPI.h
│       │                   ├── HBAuthBridge.h
│       │                   ├── HBAuthInfo.h
│       │                   ├── HBAuthObject.h
│       │                   └── HBAuthStoreManager.h
│       └── Binary-universal
│           ├── include
│           │   ├── HBAuthAPI.h
│           │   ├── HBAuthBridge.h
│           │   ├── HBAuthInfo.h
│           │   ├── HBAuthObject.h
│           │   └── HBAuthStoreManager.h
│           └── libHBAuthenticationBinary.a

其实Binary-universal就是最终的静态库的文件,其他的Binary-iphonesimulator与Binary-iphoneos目录下的文件都不需要包含到pod的git版本库中。

设置pod库的podspec文件,切换源码和二进制库的配置

接下来设置podspec文件,内容如下:

Pod::Spec.new do |s|
  s.name             = 'HBAuthentication'
  s.version          = '0.1.6-beta5'
  s.summary          = '基础认证组件'
  s.description      = <<-DESC
iOS认证组件,相关文档请访问内部wiki:
http://***.***.com/member/Auth
                       DESC

  s.homepage         = 'http://***.***.com/Hbec_IOS_common/HBAuthentication'
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  s.author           = { 'Neo' => '***@***.com' }
  s.source           = { :git => 'git@***.com:Hbec_IOS_common/HBAuthentication.git', :tag => s.version.to_s }
  # s.social_media_url = 'https://twitter.com/<TWITTER_USERNAME>'

  s.ios.deployment_target = '7.0'

  s.resource_bundles = {
    'HBAuthentication' => ['HBAuthentication/Assets/*.{png,cer,json,der,p12}']
  }
  s.source_files = 'HBAuthentication/Classes/**'
end

  if ENV['SOURCECODE']
    puts '-----------'
    puts 'HBAuthentication Source Code'
  else
    puts '+++++++++++'
    puts 'HBAuthentication Binary'
      s.source_files = 'Example/HBAuthenticationBinary/Products/Binary-universal/include/**'
      s.public_header_files = 'Example/HBAuthenticationBinary/Products/Binary-universal/include/*.h'
      s.ios.vendored_libraries = 'Example/HBAuthenticationBinary/Products/Binary-universal/libHBAuthenticationBinary.a'
  end
  s.dependency 'CocoaLumberjack'
  s.dependency 'HBWebBridge'
end

发布含有源码和二进制库的pod库

此时在Example中测试该pod库,在podfile中添加该库依赖:

  pod 'HBAuthentication', :path => '../'

然后使用 pod install 此时,会发现Pods的Development Pods目录下的HBAuthentication为下图:

说明为静态库依赖。
在Example项目中使用 SOURCECODE=1 pod install以后,则切换到源码模式。

8CE3FB66-6D1E-426E-A041-96549F2D5465.png

分别运行测试,都没有问题,可以将工程文件提交到git仓库,注意前面生成的其他架构的文件目录可以删除,不提交到git版本库中,因为是二进制文件,git对二进制文件不能做到增量更新,随着版本增加,git版本库会越来越大,所以最好精简静态库的大小,最后在自己的私有cocoapods仓库中进行pod库的发布。私有cocoapods库搭建参考官方文档

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容