前言
本文主要从补充网上的二进制重排方案开始,探讨Xcode工程目录和CocoaPods,理解CocoaPods到底为我们做了什么。
一、问题
抖音之前发表了一个利用二进制重排的方案去优化启动速度。然后这两年关于二进制重排的文章已经有很多了。
其核心是记录在启动时刻获取调用了哪些文件的哪些方法,然后将这些方法按顺序放在.order文件中,系统在加载时根据这些内容优先加载,以减少page fault次数,提高启动速度。
很多文章中的方案都是基于Clang插桩的方式去实现。以下以一篇我认为很好的文章 启动优化之Clang插桩实现二进制重排 一文为例
文中步骤如下
- 打开工程,为主工程target配置build settings,增加
-fsanitize-coverage=func,trace-pc-guard这个Other C Flags,sanitize=undefined -sanitize-coverage=func这个swift Flags - 添加
__sanitizer_cov_trace_pc_guard函数。这个函数会hook方法,在每一个方法调用前都会调用。 - 在
__sanitizer_cov_trace_pc_guard记录调用的方法,并存入数组 - 将数组里面的方法按顺序写入到
.order文件中
但文中只提及到了在主工程中target插桩,这样的话只能记录主工程中的方法调用顺序。对于我们通过CocoaPods引入的库的方法是记录不到的。
下面我们将详细分析Xcode工程和CocoaPods,来完善这个通过Clang插桩实现二进制重排的方案。
二、Xcode工程
常规Xcode工程结构
首先看一下常规的Xcode工程,如下
可以看到标记的地方图中标记了四个地方,分别是WorkSpace、Project、Target和Scheme。
以下分别一一讲述
Target
Product是产出物,产出物可以是Application,Extension,Test,Framework,Libary等。 Target是一个产出一个Product的最小编译单元,每一个Target对应着一个Product。
其关系如下图所示
配置
对于每一个Target,都有自己的独立配置,如上图中绿框所示,分别是
- General:配置基础的信息,如Product的名字,bundle ID等信息。
- Signing & Capailities:签名,能力(如推送能力)配置
- Resource Tags:按需加载资源配置
- Info:info文件配置,如权限配置等
- Build Settings:配置Target,如指定使用的编译器,目标平台、编译参数、头文件搜索路径等
- Build Phases:build阶段配置,如前置依赖、执行的脚本文件
- Build Rules:配置自定义构建规则
要新建一个Target也很容易,在菜单栏点击File-> New ->Target即可,就会弹出要新建什么类型的Target,创建成功后,可以在Products文件夹中看到对应的Product。
依赖
一个Target是可以依赖其他Target的。如果Target A依赖与Target B,那么 会先处理Target B,生成B的Product,再去生成A的Product.拿CocoaPods举例子。当我们执行pod install后,可以看到
主工程中的Target依赖了Pod工程的framework产物。主工程的中的Target SFMainPrject会依赖Pod工程的Target Pods-SFMainProject。这时候在构建过程中会先把Target Pods-SFMainProject编译成一个framework,再去构建App。
Project
Target是最小的编译单元,Project是Target的载体。也是Xcode可以直接打开的工程结构。 Project是无法直接被编译的,也无法输出Product。我们开发过程中编译的是Target。所以对于一个Project而言,至少包含一个Target。 Project还可以包含其他的Project
如上图可以看出,一个Project可以包含多个Target。其中一个Project还有Build Settings等配置。如果Target中的Build Settings有相同的配置,则Target中的配置会继承或覆盖Project的配置
WorkSpace
WorkSpace就是Project容器。一个WorkSpace可以装载多个Project。当我们打开一个WorkSpace的时候,WorkSpace中的Project是相互可见的。 对xxx.xcworkspace文件单击右键,显示包内容,如下。
可以看出,WorkSpace只是简单的把Project组织起来。Target,Project,Workspace关系如下
Scheme
Scheme是一个理解为一个构建流程。定义了构建的Target,构建配置,以及测试配置。每一次构建,只能选择一个Scheme。点击下图位置即可配置和新建Scheme.
每一个Scheme都会对应一个Target。指明Target的各个构建流程的配置是怎样的,包括了Build、Run、Test、Profile、Analyze、Archive等操作每一个过程都可以单独配置。如下
Build Settings
在说了Target,Project以后,Build Settings就不得不说了。Build Setting是一个构建变量,指定了Target在构建中的信息。如指定Xcode传给编译器的变量。 除了上面在Project,Target中的Build Settings,我们也可以去自定义一个Build Settings。在Xcode工程中点击
File-> New -> File -> Configuration Settings File
在弹出的窗口中,指定配置文件Config.xcconfig的所分配的project和Target,存放的位置,点击【crate】即可在工程中看到对应的配置文件。
然后Xcode工程在构建过程中,会按以下的顺序读取配置
- .xcconfig文件中的配置
- Target的Build Settings
- Project的Build Settings
- 平台的默认值
三、CocoaPods
CocoaPods与Xcode工程
在我们平常开发中,我们调用pod init以后就会生成一个Podfile文件。然后填入依赖的库,调用pod install就会生成一个WorkSpace和一个Pods文件夹,Pods文件夹里面有一个名为Pods的Project
打开WorkSpace,可以看到如下
通过pod引入的库会被当作一个Target,每个Target的产物是framework。而在Pods-SFMainProject中的Target中,有依赖于其他几个Target。如下
结合在【Target】-【依赖】小节中主工程的Target依赖于Pods-SFMainProject这个Target的产物Pods-SFMainProject.framwork,可以得知Target与Target之间的关系,如下
在主Target构建前,会把依赖的各个Target先构建,在Target再依赖其他的Target产出的framework去构建。
CocoaPods
上面说到,调用pod install后会生成Pods文件夹和WorkSpace,到底是怎么做的呢?
CocoaPods是什么
Ruby工具链
CocoaPods其实是一个基于Ruby实现的库管理工具。先介绍一下Ruby常用的开发环境。
- Ruby:一种开发语言,类似于JAVA,Python等
- RVM:用于帮你安装Ruby环境,帮你管理多个Ruby环境,帮你管理你开发的每个Ruby应用使用机器上哪个Ruby环境
- RubyGems是一个Ruby程序包管理器。它将一个Ruby应用程序打包到一个gem里,作为一个安装单元。
- Gem:是封装起来的Ruby应用程序或代码库。
- Gemfile:定义你的应用依赖哪些第三方包,bundle根据该配置去寻找这些包。
- Bundler:是管理 Gem 依赖的工具。在配置文件Gemfile里说明你的应用依赖哪些第三方包,他自动帮你下载安装多个包,并且会下载这些包依赖的包
大致关系如下
对于CocoaPods,其实也是一个Gem。所以我们可以通过添加一个Gemfile文件为项目指定CocoaPods版本。
CocoaPods也借鉴了这种模式。结合上方设计如下
CocoaPods架构
CocoaPods其实是一个架构设计清晰的框架,将功能模块一个个划分。概览设计如下
其中各个功能模块如下
CALide
负责处理在终端输入的命令,如pod init,将终端命令转换成需要执行的ruby代码
CocoaPods-core
负责解析DSL模版,也就是我们的Podfile,.podSpec文件。我们的Podfile文件中编写的内容其实是Ruby,可以通过eval特性将Podfile中字符串解析成Ruby代码。
CocoaPods-Downloader
负责下载源码。经过解析Podfile后中得到Ruby代码,会将每一个依赖的存入到数组,然后把这些代码下载下来
XcodeProj
负责操作Xcode工程。下载完代码以后生成Pods工程和WorkSpace,为依赖的库生成target,并根据库与库之间的关系为Target添加依赖。
有发布过库到Cocopods的同学会知道,我们的库会有一个.podspec文件,这个文件是库的描述信息,如下
Pod::Spec.new do |spec|
spec.name = 'Reachability'
spec.version = '3.1.0'
spec.license = { :type => 'BSD' }
spec.homepage = 'https://github.com/tonymillion/Reachability'
spec.authors = { 'Tony Million' => 'tonymillion@gmail.com' }
spec.summary = 'ARC and GCD Compatible Reachability Class for iOS and OS X.'
spec.source = { :git => 'https://github.com/tonymillion/Reachability.git', :tag => 'v3.1.0' }
spec.module_name = 'Rich'
spec.swift_version = '4.0'
spec.ios.deployment_target = '9.0'
spec.osx.deployment_target = '10.10'
spec.source_files = 'Reachability/common/*.swift'
spec.ios.source_files = 'Reachability/ios/*.swift', 'Reachability/extensions/*.swift'
spec.osx.source_files = 'Reachability/osx/*.swift'
spec.framework = 'SystemConfiguration'
spec.ios.framework = 'UIKit'
spec.osx.framework = 'AppKit'
spec.dependency 'SomeOtherPod'
end
这些描述信息不单单包含库的作者,库名等基本信息,还包括了库所需要依赖的库,参与编译的源文件,支持版本等信息。这些信息会CocoaPods在生成target时,放入到Build Settings中。
Cocoapods-plugins
CocoaPods中部分功能以plugin的形式提供,可以通过 pod plugins installed获取已经安装的plugin。如下
当然我们也可以定义自己的plugin,如图中的 cocoapods-test_plugin 就是自定义的plugin。
CocoaPods小结
上述可以理解为,CocoaPods是基于Ruby实现的库管理工具,而我们的podfile文件就是Ruby代码,CocoaPods解析了这些Ruby代码,为我们生成和处理target,project,和workspace,并为这些target设置依赖关系。
四、完善二进制重排
理解完CocoaPods与Xcode工程后,我们可以再去分析二进制重排的方案。在开头提到在主工程的build settings中添加了-fsanitize-coverage=func,trace-pc-guard,nitize=undefined -sanitize-coverage=fun后,然后添加__sanitizer_cov_trace_pc_guard函数,就可以实现代码插桩。
但是这样是无法覆盖到通过podfile文件引入的库的。处理的思路也很简单,只需要为工程中的每一个target都设置-fsanitize-coverage=func,trace-pc-guard,并提供__sanitizer_cov_trace_pc_guard函数即可。
我们不能去修改每一个库的源码去添加函数。但是我们可以新建一个库,提供了__sanitizer_cov_trace_pc_guard的实现,再让每一个库都依赖于这个库即可。总体思路如下
- 新建库A,提供
__sanitizer_cov_trace_pc_guard函数的实现 - 让主工程target,和所有第三方库的target都依赖这个库A
- 操作每一个target,修改其设置Other C Flags和Other Swift Flags。这里通过
.xcconfig为target设置这些Flags,这样做的好处是每一次pod install的时候都会生成新的.xcconfig,换言之就是,每一次pod intall以后,这些Flags都会被清除。
关于第1步,可以从启动优化之Clang插桩实现二进制重排获取,对于第2,3步,可以通过以下脚本实现
require "xcodeproj"
class PodProjectSetting
def change(project,className,methodName)
Pod::UI.puts "插桩----- 开始"
linkFramework(project)
changeProjectBuildSetting(project,false)
project.save
Pod::UI.puts "插桩----- 完成"
end
# 为pod进来的每一个target设置依赖
def linkFramework(project)
project.targets.each do |target|
ret = target.add_dependency(project.targets.find { |t| t.name == "SFBinaryOrderSort" })
end
end
# 修改pod进来的每一个target添加Flags
def changeProjectBuildSetting(project, justAddClang)
project.targets.each do |target|
target.build_configurations.each do |configuration|
if target.name == "SFBinaryOrderSort"
next
end
if !target.product_type.include?("framework")
next
end
configuration.build_settings.each do |key, value|
# Pod::UI.puts "----aa- --#{target} --key: #{key}---#{value}"
if configuration.base_configuration_reference.nil?
next
end
xcconfig_path = configuration.base_configuration_reference.real_path
xcconfig = File.read(xcconfig_path)
#修改xcconfig other_cflags
if xcconfig.include?("OTHER_CFLAGS") == false
xcconfig = xcconfig + "\n" + "OTHER_CFLAGS = $(inherited) -fsanitize-coverage=func,trace-pc-guard"
else
if xcconfig.include?("OTHER_CFLAGS = $(inherited)") == false
xcconfig = xcconfig.sub("OTHER_CFLAGS", "OTHER_LDFLAGS = $(inherited)")
end
if xcconfig.include?("-fsanitize-coverage=func,trace-pc-guard") == false
xcconfig = xcconfig.sub("OTHER_CFLAGS = $(inherited)", "OTHER_CFLAGS = $(inherited) -fsanitize-coverage=func,trace-pc-guard")
end
end
#修改xcconfig other_swift_flags
if xcconfig.include?("OTHER_SWIFT_FLAGS") == false
xcconfig = xcconfig + "\n" + "OTHER_SWIFT_FLAGS = $(inherited) -sanitize=undefined -sanitize-coverage=func"
else
if xcconfig.include?("OTHER_SWIFT_FLAGS = $(inherited)") == false
xcconfig = xcconfig.sub("OTHER_SWIFT_FLAGS", "OTHER_LDFLAGS = $(inherited)")
end
if xcconfig.include?("-sanitize=undefined -sanitize-coverage=func") == false
xcconfig = xcconfig.sub("OTHER_SWIFT_FLAGS = $(inherited)", "OTHER_SWIFT_FLAGS = $(inherited) -sanitize=undefined -sanitize-coverage=func")
end
end
unless justAddClang == true
#修改xcconfig other_ldflags
if xcconfig.include?("OTHER_LDFLAGS") == false
xcconfig = xcconfig + "\n" + 'OTHER_LDFLAGS = $(inherited) -framework "SFBinaryOrderSort"'
else
if xcconfig.include?("OTHER_LDFLAGS = $(inherited)") == false
xcconfig = xcconfig.sub("OTHER_LDFLAGS", "OTHER_LDFLAGS = $(inherited)")
end
if xcconfig.include?('-framework "SFBinaryOrderSort"') == false
xcconfig = xcconfig.sub("OTHER_LDFLAGS = $(inherited)", 'OTHER_LDFLAGS = $(inherited) -framework "SFBinaryOrderSort"')
end
end
#修改xcconfig framework_search_paths
if xcconfig.include?("FRAMEWORK_SEARCH_PATHS") == false
xcconfig = xcconfig + "\n" + 'FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SFBinaryOrderSort"'
else
if xcconfig.include?("FRAMEWORK_SEARCH_PATHS = $(inherited)") == false
xcconfig = xcconfig.sub("FRAMEWORK_SEARCH_PATHS", "FRAMEWORK_SEARCH_PATHS = $(inherited)")
end
if xcconfig.include?("${PODS_CONFIGURATION_BUILD_DIR}/SFBinaryOrderSort") == false
xcconfig = xcconfig.sub("FRAMEWORK_SEARCH_PATHS = $(inherited)", 'FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/SFBinaryOrderSort"')
end
end
end
File.open(xcconfig_path, "w") { |file| file << xcconfig }
end
end
end
end
end
其中的脚本中的‘SFBinaryOrderSort’是我们步骤1中提到的库A,读者们可以自行修改为自己的库名。将这个脚本保存起来,命名为pod_project_setting.rb,放到和podfile同一目录,然后在podfile文件的最后添加一下代码
post_install do |installer|
#引入脚本,
require "./pod_project_setting.rb"
pods_project = installer.pods_project
PodProjectSetting.new().change(pods_project,"","")
end
然后这段脚本会在pod istall流程结束后修改每一个target的Flags。获取到.order后,把这段脚本从podfile中删除即可,到此,我们通过CocoaPods完成了二进制重排方案的完善。
总结
本文分析了CocoaPods和Xcode工程之间的关系,但CocoaPods厉害之处远不止这些。读者可去深入探究,