是时候理解Xcode工程与CocoaPods了

1,386 阅读10分钟

前言

本文主要从补充网上的二进制重排方案开始,探讨Xcode工程目录和CocoaPods,理解CocoaPods到底为我们做了什么。

一、问题

抖音之前发表了一个利用二进制重排的方案去优化启动速度。然后这两年关于二进制重排的文章已经有很多了。

其核心是记录在启动时刻获取调用了哪些文件的哪些方法,然后将这些方法按顺序放在.order文件中,系统在加载时根据这些内容优先加载,以减少page fault次数,提高启动速度。  很多文章中的方案都是基于Clang插桩的方式去实现。以下以一篇我认为很好的文章 启动优化之Clang插桩实现二进制重排 一文为例

文中步骤如下

  1. 打开工程,为主工程target配置build settings,增加-fsanitize-coverage=func,trace-pc-guard这个Other C Flags,sanitize=undefined -sanitize-coverage=func这个swift Flags
  2. 添加__sanitizer_cov_trace_pc_guard函数。这个函数会hook方法,在每一个方法调用前都会调用。
  3. __sanitizer_cov_trace_pc_guard记录调用的方法,并存入数组
  4. 将数组里面的方法按顺序写入到.order文件中

但文中只提及到了在主工程中target插桩,这样的话只能记录主工程中的方法调用顺序。对于我们通过CocoaPods引入的库的方法是记录不到的。

下面我们将详细分析Xcode工程和CocoaPods,来完善这个通过Clang插桩实现二进制重排的方案。


二、Xcode工程

常规Xcode工程结构

首先看一下常规的Xcode工程,如下

2.png 可以看到标记的地方图中标记了四个地方,分别是WorkSpace、Project、Target和Scheme。

以下分别一一讲述

Target

Product是产出物,产出物可以是Application,Extension,Test,Framework,Libary等。 Target是一个产出一个Product的最小编译单元,每一个Target对应着一个Product。

其关系如下图所示

image.png

配置

对于每一个Target,都有自己的独立配置,如上图中绿框所示,分别是

  1. General:配置基础的信息,如Product的名字,bundle ID等信息。
  2. Signing & Capailities:签名,能力(如推送能力)配置
  3. Resource Tags:按需加载资源配置
  4. Info:info文件配置,如权限配置等
  5. Build Settings:配置Target,如指定使用的编译器,目标平台、编译参数、头文件搜索路径等
  6. Build Phases:build阶段配置,如前置依赖、执行的脚本文件
  7. Build Rules:配置自定义构建规则

要新建一个Target也很容易,在菜单栏点击File-> New ->Target即可,就会弹出要新建什么类型的Target,创建成功后,可以在Products文件夹中看到对应的Product。

image.png

依赖

一个Target是可以依赖其他Target的。如果Target A依赖与Target B,那么 会先处理Target B,生成B的Product,再去生成A的Product.拿CocoaPods举例子。当我们执行pod install后,可以看到

image.png 主工程中的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

image.png 如上图可以看出,一个Project可以包含多个Target。其中一个Project还有Build Settings等配置。如果Target中的Build Settings有相同的配置,则Target中的配置会继承或覆盖Project的配置

WorkSpace

WorkSpace就是Project容器。一个WorkSpace可以装载多个Project。当我们打开一个WorkSpace的时候,WorkSpace中的Project是相互可见的。 对xxx.xcworkspace文件单击右键,显示包内容,如下。

image.png

可以看出,WorkSpace只是简单的把Project组织起来。Target,Project,Workspace关系如下

image.png

Scheme

Scheme是一个理解为一个构建流程。定义了构建的Target,构建配置,以及测试配置。每一次构建,只能选择一个Scheme。点击下图位置即可配置和新建Scheme.

image.png 每一个Scheme都会对应一个Target。指明Target的各个构建流程的配置是怎样的,包括了Build、Run、Test、Profile、Analyze、Archive等操作每一个过程都可以单独配置。如下

image.png

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】即可在工程中看到对应的配置文件。

image.png 然后Xcode工程在构建过程中,会按以下的顺序读取配置

  1. .xcconfig文件中的配置
  2. Target的Build Settings
  3. Project的Build Settings
  4. 平台的默认值

三、CocoaPods

CocoaPods与Xcode工程

在我们平常开发中,我们调用pod init以后就会生成一个Podfile文件。然后填入依赖的库,调用pod install就会生成一个WorkSpace和一个Pods文件夹,Pods文件夹里面有一个名为Pods的Project

image.png 打开WorkSpace,可以看到如下

image.png

通过pod引入的库会被当作一个Target,每个Target的产物是framework。而在Pods-SFMainProject中的Target中,有依赖于其他几个Target。如下

image.png 结合在【Target】-【依赖】小节中主工程的Target依赖于Pods-SFMainProject这个Target的产物Pods-SFMainProject.framwork,可以得知Target与Target之间的关系,如下

image.png

在主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里说明你的应用依赖哪些第三方包,他自动帮你下载安装多个包,并且会下载这些包依赖的包

大致关系如下

image.png

对于CocoaPods,其实也是一个Gem。所以我们可以通过添加一个Gemfile文件为项目指定CocoaPods版本。

CocoaPods也借鉴了这种模式。结合上方设计如下

image.png

CocoaPods架构

CocoaPods其实是一个架构设计清晰的框架,将功能模块一个个划分。概览设计如下

image.png

其中各个功能模块如下

CALide

负责处理在终端输入的命令,如pod init,将终端命令转换成需要执行的ruby代码

CocoaPods-core

负责解析DSL模版,也就是我们的Podfile.podSpec文件。我们的Podfile文件中编写的内容其实是Ruby,可以通过eval特性将Podfile中字符串解析成Ruby代码。

image.png

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。如下

image.png

当然我们也可以定义自己的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的实现,再让每一个库都依赖于这个库即可。总体思路如下

  1. 新建库A,提供__sanitizer_cov_trace_pc_guard函数的实现
  2. 让主工程target,和所有第三方库的target都依赖这个库A
  3. 操作每一个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厉害之处远不止这些。读者可去深入探究,

参考

xcode工程 CocoaPods 启动优化之Clang插桩实现二进制重排 Resource Tags