CocoaPods 组件平滑二进制化解决方案

阅读数:4548 2016 年 7 月 24 日

什么是组件二进制化?

在 iOS 开发中,事实标准是我们使用 CocoaPods 生成、管理和使用 library。这里的 library 就是一个模块、组件或库。二进制化指的是通过编译把组件的源码转换成静态库或动态库,以提高该组件在 App 项目中的编译速度。

我们的方案是转换成静态库,也就是.a 格式的文件加上暴露出来的头文件。

为什么我们需要二进制化呢?

在我们 App 开发中,我们逐渐的抽象了很多模块、业务、UI 等把他转换成私有 CocoaPod 库。其中有一个是用 C++ 和 Objective-C 混写的,源码格式为.mm。在 app 项目编译时.mm 部分代码编译非常慢。这作为一个契机让我们去考虑如何加快编译速度。

这个混写的 CocoaPod 库叫做 YTXChart,之后会以此库为例反复提到。

另外随着业务的扩展,私有 CocoaPod 库和第三方 CocoaPod 库越来越多,App 项目中的文件也越来越多。每次 pod install 安装新库或 pod update 更新库的时候,重新编译的过程需要等待很长时间。这也向我们提出了加快编译速度的需求。

另外如果想要做组件化的话,一定要做二进制化。

所以我们想到了二进制化的方案来解决这个问题,并且很多大公司也是这么做的。

这带来一个新问题:一步就位还是平滑过度

对我们来说,这是一个尝试,不可能开始就决定把所有的私有 CocoaPod 库二进制化,也不可能决定把所有第三方 CocoaPod 库二进制化。当务之急的情况是加快 YTXChart 库编译速度。所以必须找到一个方案平滑过度。

我们的 App 中的 podflie 是这样的

(点击放大图像)

(点击放大图像)

平滑二进制方案需求点

  • 其他的 CocoaPod 库都还是源码。YTXChart 为二进制化。

  • 以后能够逐步迭代把更多的以 YTX 开头的 CocoaPod 库进行二进制化,而不影响主 App。

  • 能够提供一种方式把二进制化 CocoaPod 库切换回源码 CocoaPod 库以便调试。尽量做的方便。

  • 解决 YTXChart 引用依赖的问题。(YTXChart 还依赖了第三方 AFNetworking 和私有 YTXServerId。保证生成的静态库中不会含有 AFNetworking 的内容和 YTXServerId 的内容并且能够编译通过)

  • 利用原来的 YTXChart.git,不创建新项目,不创建新的 git 库。因为我们的二进制化库的生成还是来自于源码,当源码更新时,我们需要一种非常快捷的方式去生成二进制的东西,不希望 copy 源码到某处,或者增加一个 git submodule。

  • 希望 App 源码和 YTXChart 中的源码尽量少或者没有改动。

  • 希望 App 中的 Podfile 尽量少或者没有改动。

  • 希望 Podfile 中的版本号保持风格一致,不会出现'~> 2.2.1.binary'这种情况。

  • 用原来的那一个 CocoaPods Repo Spec。

以下这个解决方案的教程满足了以上所有需求点

注意,以下的例子基于 Cocoapods@1.0.1,而且目前只能是 1.0.1

第一步:源码生成静态库

如果你是通过命令 pod lipo create 创建的 CocoaPod 库并且 pod install 的话,它的目录结构应该像这样子(只列出重要的):

YTXChart  
  |-Example
    |-YTXChart
    |-Pods
    |-YTXChart.xcodeproj
    |-YTXChart.xcworkspace
    |-Podfile
    \-Podfile.lock
  |-Pod
    |-Assets
    \-Classes
  \-YTXChart.podspec

在 xcode 中创建新 Target:

YTXChartBinaryFile->New->Target->Framework & Library->Cocoa Touch Static Library

如果你们的项目最低支持到 iOS8 可以创建 Dynamic Framework

注意在 Podfile 中加入以下这段


 

target 'YTXChartBinary' doend 

然后 pod install

解释:Cocoapods@1.0.1 会在 Header Search Path 自动加入内容。如果你用 CocoaPods@0.39.0 则需要自己加 Header Search Path 保证依赖库 YTXServerId 和 AFNetwork 能够被找到。如图:

(点击放大图像)

然后把 Pod/Classes 中的源码拖入到 YTXChartBinary 中,这样选择(这样会 link 源码而不是复制):

(点击放大图像)

然后变成这样子:

(点击放大图像)

Headers 需要自己加,里面是你需要暴露的头文件

在 YTXChartBinary Target 中的 Build Settings 下找到 iOS Deployment Target 选择和 YTXChart.podspec 中的 s.platform 保持一致。这里是 7.0:

YTXChartBinary Target->Build Settings->iOS Deployment Target

在根目录创建 shell 脚本 buildbinary.sh

你也可以创建一个 Aggregate Target 用来执行 shell 脚本

代码如下:

(点击放大图像)

这个脚本写的并不是很好。说说主要做了什么。 Release 不同的静态库,真机和模拟器的。只构建 x86_64,不构建 i386 加快速度

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}"  

* 通过 lipo 命令合并。新.a 使用 project name 是因为要和 App 项目的 OTHER_LDFLAGS 兼容 -l"YTXChart"

lipo -create "${DEVICE_DIR}" "${SIMULATOR_DIR}" 
-output "${INSTALL_LIB_DIR}/lib${PROJECT_NAME}.a" 

结果:

(点击放大图像)

为什么要删除 i386

实际上,二进制化方案就是以空间换时间。我们这个 YTXChart 库生成的.a 去除 i386 之后大小有 166.3M。上传到 git 仓库后,git 会压缩。增加了 33M 左右。而作为二进制文件,git 是没法做增量的。所以每次上传.a 都会大大增加 git 库大小,增加硬盘使用量。考虑到我们的服务器硬盘只有 60 个 G 能用,以后还会二进制化很多组件。

所以得出:尽量压缩二进制文件大小;尽量不上传.a,直到发布某个版本时才上传。

当然,如果你的服务器硬盘是 1T 的话,我觉得你也可以随便搞。

现在文件目录是这样子的:

YTXChart  
  |-Example
    |-YTXChart
    |-Pods
    |-YTXChart.xcodeproj
    |-YTXChart.xcworkspace
    |-YTXChartBinary // 空的 
    |-Podfile
    \-Podfile.lock
  |-Pod
    |-Assets
    |-Classes // 里面是源码 
    \-Products
      |-include
         |-xxx.h
         |-...
         \-xxx.h
      \-lib
         \- libYTXChartBinary.a
  \-YTXChart.podspec

第二步:测试生成的静态库

修改 YTXChart.podspec 如下:

(点击放大图像)

注意 s.sourcefiles 和 s.publicheaderfiles 和 s.ios.vendoredlibraries 的路径

Exampl/Podfile 是长这样子的:

(点击放大图像)

执行 pod install 后应该是这样子的,然后跑起来没问题

(点击放大图像)

执行 pod lib lint --sources='http://gitlab.baidao.com/ios/ytx-pod-specs.git,master'--verbose --use-libraries --fail-fast 也是好的

至此我们构建出一个静态库,只包含 YTXChart 的内容,不包含依赖 AFNetwork 和 YTXServerId 的内容

证明:把 s.dependency 'AFNetworking', '~> 2.0'去除再执行 pod lib lint 'http://gitlab.baidao.com/ios/ytx-pod-specs.git,master' --verbose --use-libraries --fail-fast 会报出找不到 AFNetwork 相关文件。

题外话:因为 CocoaPods1.0.1 不支持 C++ 项目的 lint(这是一个 defect ),所以这个时候我会切回 CocoaPods@0.39.0 来 lint 和 publish。而前面 pod instal 增加 Search Path 是依靠 CocoaPods@1.0.1。如果你不是.mm 混写的,是不会有这个问题的。尽管使用 CocoaPods@1.0.1。强行当作没看到这个题外话。

下一步解决如何在源码和二进制中切换 修改 YTXChart.podspec 为以下内容:

(点击放大图像)

注意这段 if ENV['IS_SOURCE']。我们的需求是优先使用二进制,偶尔才会切回源码。

删除 Example/Pods 目录。

执行 IS_SOURCE=1 pod install。你会看到 Example/Pods/YTXChart/ 里面都是源码

输出 Notice:YTXChart Now is source

进一步跑起模拟器,因为是源码编译用了很长时间,模拟器起来,一切也是好的

再试下 pod cache clean --all && IS_SOURCE=1 pod lib lint 也是好的

再试下 pod cache clean --all && pod lib lint 也是好的

现在我们通过 if else 简单地实现了本地 Example App 项目切换源码和二进制。

发布到自己的 pod repo spec

发布就和正常发布没有任何区别。

检查从 spec repo 的 source 中安装

Podfile 修改为 pod 'YTXChart', '~> 0.17.7'

以下两步很重要

pod cache clean --all

删除 Example/Pods

然后 pod install

检查 Example/Pods/YTXServerId/ 和 Example/Pods/AFNetwork/ 发现都是.h .m 源码。

检查 Example/Pods/YTXChart/ 里的是二进制.a 和头文件。跑起 App 并没有问题。

尝试切回源码

如果你直接 IS_SOURCE=1 pod install 你会发现 Example/Pods/YTXChart/ 里的内容都变成了空。

这是为什么呢,因为 pod cache 了一个 podspec.json。可以通过 pod cache list 查看。他 cache 了一个描述如何从 s.source 中找到相关文件。现在的描述还是从 Pod/Products/ 下去找,自然为空。

为了避免这个问题,所以必须执行上面两步。这个是唯一的问题,目前我还找不到更好的解决方案。切换的行为只是偶尔发生,这是可以接受的。

执行 2 步。再次 IS_SOURCE=1 pod install 你就发现 Example/Pods/YTXChart/ 里的内容都变成了.h .mm 源码。跑起 App 也是好的。

为什么 lint 之前要 cache clean。原理是一样的。如果 YTXChart 依赖的 YTXServerId 也被做成了二进制化就需要 cache clean。不过你也可以这样 pod cache clean YTXServerId

特别注意 IS_SOURCE 应当作为一个所有非二进制化 Pod 库的统一标识,并且通知你们的项目组里所有成员。pod install 可能会有某几个已经二进制化的库使用二进制的内容。IS_SOURCE=1 pod install 时,所有的库都将会是源码的内容。

版本管理

请参考这篇我的文章 CocoaPod 版本规范:

http://www.yiqixiabigao.com/2016/07/14/yin-tian-xia-cocoapodshi-yong-gui-fan/

完整分析

当你发布完成之后,查看。我们发现在 Spec Repo 中对应版本的 podspec 就是我们的 YTXChart/podspec。 CocoaPod 从 s.sourcegit 地址和 tag 下载对应的代码,Pod/Products 和 Pod/Classes 里的内容都存在 当你使用 IS_SOURCE=1 时 ENV['IS_SOURCE'] 会为 true。CocoaPods 通过 s.source_files 从下载代码的路径找到源码构建 Example/Pods 和 YTXChart.xcworkspace

(点击放大图像)

明白了上面的过程,来再分析下为什么要在切换源码和二进制化时删除 cache 和 Pods 目录。放几张图就明白了

(点击放大图像)

(点击放大图像)

删除 cache 和 Pods 目录。IS_SOURCE=1 pod install 观察 json。

(点击放大图像)

总结:

  • 没有使用 submodule 或新的 git 仓库来构建出一个不包含依赖内容的静态库。一份原来的 git 仓库。

  • 没有因为构建二进制库而需要增加冗余的源代码。所以当你修改 Pod/Classes 中的源码,可以方便简单地执行 buildbinary.sh 脚本来构建出静态库。一份源码。

  • 共用了一份 YTXChart.podspec

  • 没有大量修改 YTXChart.podspec

  • 使用 pod lib lint 和 IS_SOURCE=1 pod lib lint 检查通过。

  • 没有修改 Podfile。这个 Example 的 Podflie 只是测试需要才改的。主 App 项目中的 Podfile 可以一行都不改。不会出现'~> 2.2.1.binary'。

  • App 中的源码不会因为使用了二进制 CocoaPods 组件而做任何修改。

  • 没有手动配置 Search Path,这样更容易。

  • 在 Example App 中可以通过 IS_SOURCE 灵活地切换源码和二进制静态库。唯一一个问题每次切换要删除 Pods 目录和 pod cache clean --all

  • 跑起 Example App 总是好的。

  • 没有影响到其他库,我可以逐步平滑地把 YTXXXX 一个一个做成二进制。

下一步目标

逐步平滑地把 YTXXXX 一个一个做成二进制;

进一步的把第三方如 AFNetwork 在私有 spec repo 中做份镜像也提供二进制化;

把 Podfile 中绝大部分组件都做成二进制(RN 这种本地安装模式和有 sub spec 的库目前不打算二进制化)。

关于资源文件,资源文件在二进制化中的配置是一样的。

另外,使用二进制化的 CocoaPods 库不会增加 ipa 的大小。所以我们应当优先用二进制化的东西,这可以加快 Archive 速度。

关于有 sub spec 的 CocoaPods 组件

两个方案:

  • 提供一个全集

  • 对每一个 sub spec 都做份二进制并保持它们之间依赖的相互关系

走过的弯路!!!

现在这个解决方案看起来简单,但在当初的探索过程中并不是那么顺利。以下是不成功的尝试!

创建另一个 YTXChartBinary.podspec

  • 把生成的 Products 目录放到 YTXChartBinary 下

  • 把 YTXChartBinary.podspec 目录放到 YTXChartBinary 下

  • Podfile 中通过增加 Binary 字段安装二进制化如 pod 'YTXChartBinary', '~> 0.17.7'

问题

  • 要维护 2 个 podspec。版本号很可能不统一。

  • 当 pod spec lint 报错:找不到相关文件。

  • Podfile 中通过增加 Binary 字段来切换,非常不方便。

  • 要改 App 源码。当安装二进制的时候 <YTXChart/YTXChart.h> 需要改成 <YTXChartBinary/YTXChart.h>。来回切换都需要改,极不方便。

  • 要改 App 源码。这次一劳永逸。直接这样使用"YTXChart.h"。但这样也不好。

创建另一个专门放二进制化的 Spec Repo,通过不同的 Source 来区分

解决了要改 App 源码的问题。只需要在 Podfile 中加个 source。

不同的 source 例子:

source 'http://gitlab.baidao.com/ios/ytx-binary-pod-specs.git'

source 'http://gitlab.baidao.com/ios/ytx-pod-specs.git' 

问题

  • 发布两次,lint 两次。

  • 创建了 2 个 Spec Repo。


感谢徐川对本文的审校。

给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

收藏

评论

微博

发表评论

注册/登录 InfoQ 发表评论