Cocoapods组件二进制化的初步实践学习

2,085 阅读7分钟

背景

对于一些稳定的、不怎么需要改动的组件能够以二进制的方式引入工程,加快工程的编译速度,并且能够方便地切回源码,方便调试。

大致流程

  1. 创建私有库(这个不是本次的主要内容,不熟悉的可以先了解下如何创建Cocoapods私有库)
  2. 将源码成制作二进制文件
  3. 实现在源码和二进制文件之间快速切换

一、选择动态库or静态库?

对比维度动态库(iOS8之后支持以framework的方式创建第三方的动态框架,这种并非系统动态库,也称为Embedded Framework,如果你解压ipa包,就会发现其实被copy到Framework目录下)静态库
分发文件大小(提供的SDK厂商提供下载的的包,如微信SDK)-
ipa包大小动态库会把整个lib复制进ipa中✅默认仅将有用到的类文件link到mach-o中
对冷启动速度的影响(app启动流程里面有rebase和bind,这里指的是没有优化过的情况)多个动态库需要多次✅多个静态库只需一次
重复的代码的符号冲突✅不会冲突,只会警告,可以正常运行(但是鬼知道会出现什么意外)-
Enable Bitcode 对引用方的影响(无论是哪种方式,任一SDK不支持Bitcode,主工程就不能支持)参考资料[1]提到"动态库不含bitcode时,引用动态库的目标部署可以包含bitcode,静态库则不然",但是我测试其实和静态库是一样的(也许是我对原文理解有误)-

PS:Bitcode[2]由LLVM引入的一种中间代码,它是源代码被编译成二进制机器码过程中的中间表示形态,既不是源代码,也不是机器码。LLVM是Xcode默认的编译框架,LLVM的编译过程可以简单的分为三个部分,前端-优化-后端。前端负责把各种语言转化成bitcode,bitcode优化后由后端编译为目标架构的机器码。

这里主要结合了我们自己项目的实际情况,列出部分对比结果。关于更多动态库和静态库的区别可以看下参考资料[1]的图。

最终结合我们现在的项目,选择了静态库的方式。

二、静态库有.a和.framework两种形式,有什么区别?

.a是一个纯二进制文件,不能直接使用,需要配合.h

.framework = .a + .h + 资源文件

我一开始更偏向于.framework的形式,因为所需要的内容都整合到一起了,万一遇到哪个落后项目组需要集成(真的有。。遇到过)直接把脚本生成的framework包给他们也省事。但这次实践过程中发现.framework在cocoapods组件二进制化上遇到了几个问题,其中一个是关于头文件引入的。对于使用方是#import "ZPTestASDK.h"引入的会报错,必须都改为#import <ZPTestASDK/ZPTestASDK.h>。其它遇到的问题,后面我会提到。所以我觉得看需求吧,.a可能更合适你。

我们项目的实际情况是,使用Cocoapods集成所有的第三方库,并且也约定过引用第三方最好是#import <ZPTestASDK/ZPTestASDK.h>,再者如果要改,我觉得全局replace一下也很快。所以这次我还是头铁想用.framework的方式尝试一下。

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

最开始我是在pod lib create ZPTestASDK生成的工程中,添加一个ZPTestASDKBinary的framework静态库的TARGET。

有人可能会疑问为啥不直接起名ZPTestASDK,一开始我也是这么干的。但是会出现一些奇怪的问题,也没找到解决方法。我暂时猜测应该是和Cocoapods的ZPTestASDK冲突了(这里给自己埋一个坑吧。。由于暂时还很熟悉的了解Cocoapods的源码和原理,知识有限也不说不出啥原因,后面学习了再补一下这坑)

然后我就采用了改名字的方式。添加了ZPTestASDKBinary的静态库,打算脚本编译的时候,改一下编译PRODUCT_NAME这种操作。

avatar

avatar

avatar

实践发现,这样能编译出来ZPTestASDK.framework但是,少了一个framework少了Modules[4]文件夹,并且会有一个ZPTestASDKBinary.h文件。最后放弃了改PRODUCT_NAME的这种方案。(也许有我不知道的操作可以解决这个问题,懂的大佬指导一下)当然如果你用的是.a的方式的话,这个方案是没问题的,而且也不需要在编译脚本改PRODUCT_NAME。

最终方案:

我重新开了一个工程,专门用来编译二进制文件。目录结构如下图。

avatar

ZPTestASDKBinaryBuild是我创建在pod lib create ZPTestASDK生成的工程下面的。如果你的组件根本不需要依赖第三方,我建议不要用pod的create指令创建,直接自己搞个合适的工程,写好ZPTestASDK.podspec文件(官方都有模板[5])就好了,也没那么多麻烦事。

缺点:如上图结构图所见,ZPTestASDKBinaryBuild/ZPTestASDK是没有源码文件的,每次组件添加新的类文件、资源(bundle)等都得手动引用到这个工程里(一定要把Copy这个选项去掉)。

四、脚本生成二进制文件

脚本的主要工作就是配置一些编译选项,编译并合并一下真机和模拟器架构,方便开发使用。有些是调试过程中尝试的内容,最终没用到的我都注释了,你可以直接删掉。提几个需要注意的点:

  1. 新建项目现在默认都是使用新的构建系统,脚本编译的时候可能会遇到一个这样的错误avatar你可以在改下编译的工程设置。在File-Workspace Settings-Build System,改为旧的就不会报错了。也可以直接在脚本编译的时候加上-UseModernBuildSystem=NO。
  2. 如果你依赖了第三方,需要配置一下LIBRARY_SEARCH_PATHS路径,这个看自己项目填写吧,因为我用的Cocoapods,所以路径也和Pods有关
  3. 对于图片资源啥的,包裹在bundle内,然后可以和类一样引用进来就好了。(我也试过直接脚本copy进去。。后来感觉没必要,还是和类文件一样引用让工程编译的时候自己去copy就好了)
# 合并在真机和模拟器上编译出的ZPTestASDK
# 项目名称
PROJECT_NAME="ZPTestASDK"
# 库名称
FMK_NAME=${PROJECT_NAME}
# 编译的TARGET名称
TARGET_NAME=${FMK_NAME}
# INSTALL_DIR 是导出framework的路径
# 在工程的根目录创建framework的文件夹.
INSTALL_DIR=Products
# 合成framework后,WRK_DIR会被删除
WRK_DIR=build
DEVICE_DIR=${WRK_DIR}/Release-iphoneos
SIMULATOR_DIR=${WRK_DIR}/Release-iphonesimulator
# framework资源文件
# FMK_NAME_BUNDLE="$PWD/${FMK_NAME}/Assets"
#cd ZPTestASDKBinaryBuild
# 生成两个架构的framework
# -UseModernBuildSystem=NO 不是用新构建系统
# LIBRARY_SEARCH_PATHS 如果你的组件以来了某个第三方,需要设置一下这个
DEVICE_LIBRARY_SEARCH_PATHS="Pods/build/Release-iphoneos"
SIMULATOR_LIBRARY_SEARCH_PATHS="Pods/build/Release-iphonesimulator"
xcodebuild -configuration "Release" -workspace "${PROJECT_NAME}.xcworkspace" -scheme "${TARGET_NAME}" -sdk iphoneos clean build CONFIGURATION_BUILD_DIR="${DEVICE_DIR}" LIBRARY_SEARCH_PATHS="${DEVICE_LIBRARY_SEARCH_PATHS}" -UseModernBuildSystem=NO 
xcodebuild -configuration "Release" -workspace "${PROJECT_NAME}.xcworkspace" -scheme "${TARGET_NAME}" -sdk iphonesimulator clean build build CONFIGURATION_BUILD_DIR="${SIMULATOR_DIR}" LIBRARY_SEARCH_PATHS="${SIMULATOR_LIBRARY_SEARCH_PATHS}" -UseModernBuildSystem=NO
#删除之前生成的framework
if [ -d "${INSTALL_DIR}" ]
then
rm -rf "${INSTALL_DIR}"
fi
mkdir -p "${INSTALL_DIR}"
cp -R "${DEVICE_DIR}/" "${INSTALL_DIR}/"
# 找到所有bundle资源复制过去
# for file in `ls | find ${FMK_NAME_BUNDLE} -name '*.bundle'` ; do
# 	cp -R $file "${INSTALL_DIR}/${FMK_NAME}.framework"
# done
# 合成
lipo -create "${DEVICE_DIR}/${FMK_NAME}.framework/${FMK_NAME}" "${SIMULATOR_DIR}/${FMK_NAME}.framework/${FMK_NAME}" -output "${INSTALL_DIR}/${FMK_NAME}.framework/${FMK_NAME}"
# 删除 WRK_DIR
rm -r "${WRK_DIR}"
# 打开 INSTALL_DIR
# open "${INSTALL_DIR}"

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

使用同份源代码和同一个podspec文件,发布两个pod版本(也就是打两个tag),组件使用方自行切换一下版本即可。 avatar

podspec提供了prepare_command设置。官方文档描述如下

This command is executed before the Pod is cleaned and before the Pods project is created. The working directory is the root of the Pod.

大致意思是会在创建pod之前执行,也告知了我们脚本的执行环境和podspec文件同级。

ZPTestASDK.podspec

Pod::Spec.new do |s|
  s.name             = 'ZPTestASDK'
  s.version          = '0.1.2.Binary'
  s.summary          = 'A short description of ZPTestASDK.'
  s.description      = <<-DESC
TODO: Add long description of the pod here.
                       DESC

  s.homepage         = 'https://github.com/lzp0717/ZPTestASDK'
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  s.author           = { 'zp' => '379357324@qq.com' }
  s.source           = { :git => 'https://github.com/lzp0717/ZPTestASDK.git', :tag => s.version.to_s }

  s.ios.deployment_target = '8.0'
  s.dependency 'AFNetworking'
  
  if s.version.to_s.include?'Binary'
      s.prepare_command = <<-CMD
        cd ZPTestASDKBinaryBuild
        sh ZPTestASDKBinary.sh
      CMD
      s.ios.vendored_frameworks = 'ZPTestASDKBinaryBuild/Products/**/*.framework'
      s.resource = 'ZPTestASDKBinaryBuild/Products/**/*.bundle'
      else
      s.source_files = 'ZPTestASDK/Classes/**/*'
      s.resource = 'ZPTestASDK/Assets/*.bundle'
  end
end

s.version修改一下,然后发布两个tag版本即可。对于二进制文件,git会在每个提交都备份一次导致增加了git服务器的压力,有了prepare_command后,我们可以把脚本编译产物的文件夹添加到git忽略就好了。

对于编译framework的脚本,pod install选择环境的时候,应该考虑一下发布生产的场景,不应该包含模拟器的架构(上述脚本合成包含了模拟器),不影响生产ipa的大小,这个可以根据实际情况改下脚本生成只有真机版本的并且在ZPTestASDK.podspec中加一些判断

参考和学习资料

[1]iOS动态库VS静态库

[2]关于bitcode, 知道这些就够了

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

[4]iOS - Umbrella Header在framework中的应用

[5]Podspec Syntax Reference

[6]iOS New Build System下framework打包脚本适配

[7]iOS CocoaPods组件平滑二进制化解决方案

[8]知乎 iOS 基于 CocoaPods 实现的二进制化方案

[8]基于 CocoaPods 的组件二进制化实践