[关闭]
@qidiandasheng 2020-10-09T15:04:49.000000Z 字数 7187 阅读 15158

Pod二进制化

使用工具


为什么要组件二进制化

二进制化指的是通过编译把组件的源码转换成静态库或动态库,以提高该组件在App项目中的编译速度。我们现在项目中除了第三方库,还有自己的很多业务组件都集成到了pod里面。

ls -lR| grep "^-" | wc -l查看了Pods目录里的所有文件差不多有5000个文件。而每次pod update的时候CocoaPods都会在你build的时候生成对应的target,然后编译成.a静态库或者.framework动态库。这样会消耗大量时间,如果简单的pod update大概花费了3分钟,如果build clear的话是5分钟。

所以我们二进制化的话就节省了很多源文件的编译时间,直接使用编译好的二进制文件。

Pod原理和说明

1、 第三方库会被编译成.a静态库或.framework动态库供我们真正的工程使用。CocoaPods会将所有的第三方库以target的方式组成一个名为Pods的工程。如果podspecs里是以s.resource_bundles的方式导入资源文件的话会有一个对应bundle的target,一般这个target命名就是podName-bundleName。最后这个Pods工程就放在刚才新生成的Pods目录下。

整个第三方库工程会生成一个名称为libPods-projectNmae.a的静态库或Pods_projectNmae.framework的静态库提供给我们自己的主工程使用(projectNmae为主工程名字)。我们能在主工程的target的General -> Linked Frameworks and Libraries里看到这个Pods工程生成的静态库。

对于资源文件,CocoaPods提供了一个名为Pods-resources.sh的bash脚本,该脚本在每次项目编译的时候都会执行,将第三方库的各种资源文件复制到目标目录中。我们能在主工程的target下的Build Phases -> Copy Pods Resources看到脚本执行Pods-resources.sh

2、 我们的工程和第三方库所在的工程会由一个新生成的workspace管理
为了方便我们直观的管理工程和第三方库,主工程和Pods工程会被以workspace的形式组织和管理,也就是我们看到的projectName.xcworkspace文件。

3、 原来的工程设置已经被更改了,这时候我们直接打开原来的工程文件去编译就会报错,只能使用新生成的workspace来进行项目管理。

4、 CocoaPods通过一个名为Pods-ProjectName.debug.xcconfig的文件来在编译时设置所有的依赖和参数(这里的debug可以是releaseschemeName)。
Pods -> Target Support Files -> Pods-ProjectName目录下可以看到。

5、 下面是CocoaPods源码sandbox.rb注释里的一个关于生成的Pods目录结构。

  1. # Pods
  2. # |
  3. # +-- Headers
  4. # | +-- Private
  5. # | | +-- [Pod Name]
  6. # | +-- Public
  7. # | +-- [Pod Name]
  8. # |
  9. # +-- Local Podspecs
  10. # | +-- External Sources
  11. # | +-- Normal Sources
  12. # |
  13. # +-- Target Support Files
  14. # | +-- [Target Name]
  15. # | +-- Pods-acknowledgements.markdown
  16. # | +-- Pods-acknowledgements.plist
  17. # | +-- Pods-dummy.m
  18. # | +-- Pods-prefix.pch
  19. # | +-- Pods.xcconfig
  20. # |
  21. # +-- [Pod Name]
  22. # |
  23. # +-- Manifest.lock
  24. # |
  25. # +-- Pods.xcodeproj

扩展知识

源码和二进制之间的切换

为了编译快我们可能会使用二进制,但是有时候我们需要切换回源码进行调试代码。我们在pod仓库中同时存放源码和生成的二进制库,然后就是让CocoaPods在安装Pod的时候判断使用源码还是二进制库。可以使用以下代码:

  1. $lib = ENV['use_lib']
  2. $lib_name = ENV["#{s.name}_use_lib"]
  3. if $lib || $lib_name
  4. puts '---------binary-------'
  5. s.ios.vendored_framework = "Framework/#{s.version}/#{s.name}.framework"
  6. //这种是帮你打包成bundle
  7. s.resource_bundles = {
  8. "#{s.name}" => ["#{s.name}/Assets/*.{png,xib,plist}"]
  9. }
  10. //这种是你已经打包好了bundle,推荐这种,可以省去每次pod帮你生成bundle的时间
  11. s.resources = "#{s.name}/Assets/*.bundle"
  12. else
  13. puts '.......source........'
  14. s.source_files = "#{s.name}/Classes/**/*"
  15. s.resources = "#{s.name}/Assets/*.bundle"
  16. s.public_header_files = "#{s.name}/Classes/**/*.h"
  17. end

使用use_lib=1 pod install安装二进制库,pod install安装源码库。

这里我们在切换的时候发现两个问题:

pod package生成Framework

pod package是cocoapods的一个插件,如果没有的话使用以下命令安装:

  1. sudo gem install cocoapods-packager

命令参数

//强制覆盖之前已经生成过的二进制库
--force

//生成静态.framework
--embedded

//生成静态.a
--library

//生成动态.framework
--dynamic

//动态.framework是需要签名的,所以只有生成动态库的时候需要这个BundleId
--bundle-identifier

//不包含依赖的符号表,生成动态库的时候不能包含这个命令,动态库一定需要包含依赖的符号表。
--exclude-deps

//表示生成的库是debug还是release,默认是release。--configuration=Debug
--configuration

是否包含依赖库符号

依赖类不重命名(编译通过):

  1. pod package DSAudio.podspec --force --exclude-deps --no-mangle

--exclude-deps表示不包含依赖库的符号,生成的静态库里就不会包含符号,我们静态库里使用了对应的依赖库的类的话,能看到为undefined。比如我这里使用了AFNetworking:

截屏2020-10-09 下午2.18.39.png-1049kB

所以podspec里要依赖对应的三方库:

  1. Pod::Spec.new do |s|
  2. .....
  3. s.ios.vendored_framework = 'ios/DSAudio.framework'
  4. s.dependency 'AFNetworking', '~> 2.3'
  5. end

依赖类重命名(编译不通过):

  1. pod package DSAudio.podspec --force --exclude-deps

不使用--no-mangle,会把依赖类重命名为PodName_类名,但这个符号依旧为undefined,就算依赖的对应的库,静态链接的时候依然会找不到PodName_类名,比如我这里使用了AFNetworking,同时依赖了AFNetworkingAFNetworking编译生成的目标文件并没有PodDSAudio_AFHTTPRequestOperationManager,就算静态链接了依然找不到对应的重命名过的类。
截屏2020-10-09 下午2.26.57.png-1048.4kB

包含依赖类符号并重命名(编译通过):

  1. pod package DSAudio.podspec --force

pod package默认会把依赖库的类重命名为PodName_类名,同时不使用--exclude-deps会把依赖库的类都编译进来,这时我们能看到那个类不为undefined了。这样的话如果主工程也依赖了这个三方库的话就不会出现符号冲突的问题了。
截屏2020-10-09 下午2.51.23.png-486.9kB

但如果三方库又依赖了系统库,对应的系统库是不会编译进来并且重命名的,这时如果没有依赖对应的系统库的话会出现符号找不到的报错:
截屏2020-10-09 下午2.42.46.png-1059.2kB

所以要主动依赖一下那个三方库依赖的系统库:

  1. Pod::Spec.new do |s|
  2. .....
  3. s.ios.deployment_target = '8.0'
  4. s.ios.vendored_framework = 'ios/DSAudio.framework'
  5. s.frameworks = 'MobileCoreServices', 'SystemConfiguration'
  6. end

优点:这样就不会跟别人依赖的三方库出现版本不统一而出现问题的情况了
缺点:因为把依赖库都打进了静态库,会导致静态库的体积大了很多(以我这里为例,原来只有79kb,依赖库打进来之后有7.5M了)

创建静态Framework

创建好的Framework安装Version自动存放到不同目录脚本

  1. pod package podName.podspec --force --embedded --no-mangle --exclude-deps --spec-sources=private,https://github.com/CocoaPods/Specs.git

pod package 打包不需要下载源码,只需要对应tag的.podspec文件,它是根据.podspec文件去对应tag直接下载源码进行打包的。

安装二进制库

  1. use_lib=1 pod install

use_framework!生成Framework

在swift出来之后,因为swift是需要动态库的,原来用CocoaPods集成的第三方库默认都是生成.a的静态库给主工程用的。所以CocoaPods在Podfile里加了一个use_framework!表示默认生成的是动态库。Build之后我们能在Pods工程下面的Products目录看到生成的动态Framework。如果想要静态Framework,可以修改Pods工程下对应的target的Build Settings -> Mach-O TypeStatic Library

生成的FrameworkCocoaPods会在主工程target下的Build Phases -> Embed Pods Frameworks执行脚本嵌入到.app下的Framework文件夹下。

如果你在模拟器下Build那么生成的Framework支持的CPU架构就只有i386 和x86_64。如果是在Generic iOS Device下Build的话那就只支持armv7和arm64。所以我们需要合成一个支持所有架构的Framework。

具体如何创建合成Framework可以参考iOS里的动态库和静态库

二进制里面宏的问题

可参考iOS App组件化开发实践里关于Debug/Release宏的部分。

建议

参考

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

embedded framework and dynamic framework

Pod Authors Guide to CocoaPods Frameworks

有赞iOS-基于二进制的编译提效策略

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注