淺析 Cocoapods-Packager 實(shí)現(xiàn)

介紹

CocoaPods plugin which allows you to generate a framework or static library from a podspec.

This plugin is for CocoaPods developers, who need to distribute their Pods not only via CocoaPods, but also as frameworks or static libraries for people who do not use Pods.

作為 CococaPods 的官方插件之一,CocoaPods Packager 為 Pod 提供了 package 命令來(lái)生成 framework or static library。你可以僅憑一個(gè) podspec 文件就能完成一個(gè) framework 或 library 的生成。

那 Packager 的使用場(chǎng)景有哪些?網(wǎng)上有特別多詳細(xì)的使用說(shuō)明文檔,而分析實(shí)現(xiàn)的比較少。這也是這篇文章的初衷,想知道它是如何 work 的。本身插件也不是很復(fù)雜,這里會(huì)更挖一些細(xì)節(jié)。之前簡(jiǎn)單寫(xiě)了一篇文章:淺析 Cocoapods-Binary 實(shí)現(xiàn),主要是這兩個(gè)插件的核心邏輯類(lèi)似,感興趣的可以拿來(lái)對(duì)比。它們的區(qū)別在于 package 的對(duì)象是針對(duì)單個(gè) pod 還是整個(gè) project 的。

CocoaPods Plugins

這次我們先簡(jiǎn)單談一談 CococaPods 的插件原理。作為一款社區(qū)產(chǎn)品,僅有的少數(shù)核心開(kāi)發(fā)者是無(wú)法滿足大量開(kāi)發(fā)者的各種需求。因此,在 2013 CcocoaPods 開(kāi)始支持 Plugins 擴(kuò)展,使得大家可以自行支持所需要的功能。

What can CocoaPods Plugins do?

  • 可以 Hook pod install 過(guò)程,包括 pre_install 與 post_install;
  • 可以添加新的 pod 命令;
  • Ruby 作為 dynamic language,你完全可以隨心所欲;

RubyGems

開(kāi)始解釋源碼錢(qián),需要說(shuō)一說(shuō)包管理,不得不提 RubyGems 和 Bundler。CocoaPods 背后的原型就是基于它們倆,當(dāng)然也會(huì)參照其他語(yǔ)言的包管理工具如 npm、Gradle。

Gems

RubyGems is a hosted ruby library service. It centralizes where you look for a library, and installing ruby libraries / apps.

Bundler

Bundler provides a consistent environment for Ruby projects by tracking and installing the exact gems and versions that are needed.

Bundler is an exit from dependency hell, and ensures that the gems you need are present in development, staging, and production. Starting work on a project is as simple as bundle install.

RubyGems 是為 ruby library 提供集中代碼托管的服務(wù)。Bundler 則是針對(duì)當(dāng)前項(xiàng)目來(lái)管理 Gem 版本的工具,

Bundler 依據(jù)項(xiàng)目中的 Gemfiles 文件來(lái)管理 Gem,就好比 CocoaPods 通過(guò) Podfile 來(lái)管理 Pod 的版本一樣。Gemfile 長(zhǎng)這樣:

source 'https://gems.example.com' do
  gem 'cocoapods', '1.8.4'
  gem 'another_gem', :git => 'https://looseyi.github.io.git', :branch => 'master'
end

很熟悉的感覺(jué),有木有。是的,Podfile 的 DSL 和 Gemfile 如出一轍。

那什么情況會(huì)用到 Gemfile 呢?比如,公司級(jí)項(xiàng)目中可以通過(guò) gemfile 來(lái)統(tǒng)一 CocoaPods 的版本,不然大家各自為政會(huì)導(dǎo)致提交代碼會(huì)因?yàn)?CocoaPods 版本不同導(dǎo)致對(duì)項(xiàng)目的配置產(chǎn)生各種差異,導(dǎo)致最終的 PR 有大量 conflict 或 change,當(dāng)然還可以管理 CocoaPods 的插件版本,可以指向你自己的定制版本。

Bundle 的使用也很簡(jiǎn)單,在 gem install bundler 后,通過(guò)添加 bundle exec 前綴來(lái)執(zhí)行 pod 命令。這時(shí)會(huì)讀取安裝在本地 .bundle/ 目錄或全局目錄下所指定的 Gem 包來(lái)執(zhí)行 pod 命令。

bundle install #安裝 gemfile 中的包
bundle exec pod install 

Gem

The software package is called a “gem” which contains a packaged Ruby application or library.

Gem 則是包含 Ruby 代碼的 application 或者 library,而它就是我們今天的主角,CocoaPods Plugin 背后的支柱。應(yīng)該說(shuō) CocoaPods Plugin 本質(zhì)上就是 Gem。

那我們來(lái)看一眼 Gem 的文件結(jié)構(gòu):

tree CocoaPods -L 2
CocoaPods
├── Rakefile
├── cocoapods.gemspec
├── bin
│   ├── pod
│   └── sandbox-pod
├── lib
│   ├── cocoapods
│   ├── ...
└── spec
│   ├── cocoapods-integration-specs
│   ...
  • bin:可執(zhí)行文件目錄,當(dāng) gem install 的時(shí)候,會(huì)被加載到用戶的 PATH 路徑下;
  • lib:gem 的源代碼目錄;
  • sepc:gem 的測(cè)試代碼目錄;
  • Rakefile:是自動(dòng)化測(cè)試程序 rake 的配置文件,也可用于生成代碼或者其他任務(wù);
  • gemspec:描述了 gem 的關(guān)鍵信息,下面會(huì)提到;

GemSpec

The gemspec specifies the information about a gem such as its name, version, description, authors and homepage.

既然 CocoaPods 也是 Gem,它的 GemSpec 包含哪些信息呢:

Gem::Specification.new do |s|
  s.name     = "cocoapods"
  s.version  = Pod::VERSION
  s.files = Dir["lib/**/*.rb"] + %w{ bin/pod bin/sandbox-pod README.md LICENSE CHANGELOG.md }
  s.executables   = %w{ pod sandbox-pod }
  s.require_paths = %w{ lib }
  s.add_runtime_dependency 'cocoapods-core',        "= #{Pod::VERSION}"
  s.add_runtime_dependency 'claide',                '>= 1.0.2', '< 2.0'
  s.add_runtime_dependency 'xcodeproj',             '>= 1.14.0', '< 2.0'
  ...
end

依據(jù)那么熟悉的感覺(jué),如果你有搞過(guò) Pod library 的話。PodSpec 類(lèi)比 Gemspec,就不多介紹了。

CocoaPods Plugins

了解完 Gem,再看 Plugins 會(huì)清晰很多。作為 CocoaPods 的 Plugin,CocoaPods 為我們提供了方便生成 plugin 模版的命令。

pod plugins create NAME [TEMPLATE_URL]

生成 plugin 模版的文件目錄與 gem 相差無(wú)幾,這里直接貼 cocoapods-packager 的文件目錄:

cocoapods-packager
├── Gemfile
├── Rakefile
├── cocoapods-packager.gemspec
├── lib
│   ├── cocoapods-packager
│   ├── cocoapods_packager.rb
│   ├── cocoapods_plugin.rb
│   └── pod
└── spec
    ├── command
    ├── fixtures
    ├── integration
    ├── spec_helper.rb
    └── unit
...

確實(shí)沒(méi)啥特別的,只不過(guò)會(huì)在 gemfile 中,幫你指定好 cocoapods 的依賴,以及根據(jù)提供的 NAME 來(lái)生成通用文件。

CocoaPods Packager

接下來(lái)我們提到的文件都是在 lib 文件夾下,即 Packager 的源代碼所在地。開(kāi)始前照例看一下腦圖,有一個(gè)整體的認(rèn)識(shí):

cocoapods-packager

這里基本對(duì)應(yīng)了 Packager 的主要文件,作者已經(jīng)分的比較清楚了,整個(gè) Package 的入口是 /lib/pod/command/package.rb 文件,對(duì)應(yīng)腦圖的 Command Run 分類(lèi)。各個(gè)分類(lèi)下面對(duì)應(yīng)的就是各個(gè)文件內(nèi)部提供的主要方法,接下來(lái)我們就從 Package 文件說(shuō)起。

Package

本文 Demo 所展示的是基于 Packager 內(nèi)部提供的測(cè)試 spec 來(lái)做示例,啟動(dòng)命令如下:

bundle exec pod package ${workspaceRoot}/cocoapods-packager/spec/fixtures/KFData.podspec --dynamic

PS:調(diào)試所用的工具是 VSCode,如果你沒(méi)有用過(guò),絕對(duì)不要錯(cuò)過(guò)了。

執(zhí)行上面代碼前,我們需要先在 package.rbinitialize 方法內(nèi)打個(gè)斷點(diǎn)。然后我們來(lái)看 Package 類(lèi):

module Pod
    class Command
      class Package < Command
         # functions ...
    end
end

Package Command 繼承自 CocoaPods 內(nèi)部所提供的命令工具模塊 CLAide::Command。所有擴(kuò)展 Pod 的命令都需要繼承它,同時(shí)需要重載它的 optionsvalidateinitializerun 四個(gè)方法。

options

A list of option name and description tuples.

執(zhí)行命令所需的可選參數(shù),是 [Array<Array>] 的元組,由參數(shù)和對(duì)應(yīng)的描述組成,選項(xiàng)如下:

def self.options
    [
        ['--force',     'Overwrite existing files.'],
        ['--no-mangle', 'Do not mangle symbols of depedendant Pods.'],
        ['--embedded',  'Generate embedded frameworks.'],
        ['--library',   'Generate static libraries.'],
        ['--dynamic',   'Generate dynamic framework.'],
        ['--local',     'Use local state rather than published versions.'],
        ['--bundle-identifier', 'Bundle identifier for dynamic framework'],
        ['--exclude-deps', 'Exclude symbols from dependencies.'],
        ['--configuration', 'Build the specified configuration (e.g. Debug). Defaults to Release'],
        ['--subspecs', 'Only include the given subspecs'],
        ['--spec-sources=private,https://github.com/CocoaPods/Specs.git', 'The sources to pull dependent ' \
            'pods from (defaults to https://github.com/CocoaPods/Specs.git)']
    ]
end

validate

校驗(yàn)所傳參數(shù)有效性,如果參數(shù)中帶有 --help 選項(xiàng),則會(huì)直接拋出幫助提示。它會(huì)在 run 方法執(zhí)行前被調(diào)用。重載前需要先調(diào)用 super,代碼如下:

def validate!
    super
    help! 'A podspec name or path is required.' unless @spec
    help! 'podspec has binary-only depedencies, mangling not possible.' if @mangle && binary_only?(@spec)
    help! '--bundle-identifier option can only be used for dynamic frameworks' if @bundle_identifier && !@dynamic
    ...
end

initialize

解析參數(shù),然后初始化打包所需的變量。這里著重介紹幾個(gè)核心參數(shù):

  • @config:xcodebuild configuration,值為 DebugRelease,當(dāng)然也可以自定義,默認(rèn)為 Release
  • @package_type:所生成的包類(lèi)型,可以是靜態(tài)庫(kù)或者動(dòng)態(tài)庫(kù),默認(rèn)為 static_framework;
  • @subspecs:支持所打出的包僅包含部分的 subspec,如果對(duì) subspes 有興趣,請(qǐng)轉(zhuǎn)官方文檔
  • @exclude_deps:打包的時(shí)候踢除 dependencies 的符號(hào)表,配合 --no-mangle 一起使用,解決靜態(tài)庫(kù)打包時(shí)有其他靜態(tài)庫(kù)依賴的問(wèn)題;
  • @mangle:是否開(kāi)自動(dòng)修改類(lèi)名等符號(hào),默認(rèn)開(kāi)啟。

這里稍微展開(kāi)一下 @package_type ,它其實(shí)是由多個(gè)參數(shù)來(lái)決定的,--embeded--dynamic--library

if @embedded
    :static_framework
elsif @dynamic
    :dynamic_framework
elsif @library
    :static_library
else
    :static_framework
end

可能有部分同學(xué)對(duì)于 framework 和 libray 的概念比較模糊,這里再說(shuō)一句,詳細(xì)可以看 Apple Doc:

A framework is a hierarchical directory that encapsulates shared resources, such as a dynamic shared library, nib files, image files, localized strings, header files, and reference documentation in a single package.

簡(jiǎn)單來(lái)說(shuō) framework 就一文件夾,你要說(shuō)和 Bundle 一樣也無(wú)妨。它是靜態(tài)庫(kù)還是動(dòng)態(tài)庫(kù),重點(diǎn)看它所包含的 library 類(lèi)型。如果包含的是 static share library 那就是 static_framework。如果包含的是 dynamic share library 則是 dynamic_framework。而我們平時(shí)說(shuō)的 libxxx.a 則是靜態(tài)庫(kù)。

run

 if @spec.nil?
     help! "Unable to find a podspec with path or name `#{@name}`."
     return
 end

 target_dir, work_dir = create_working_directory
 return if target_dir.nil?
 build_package

 `mv "#{work_dir}" "#{target_dir}"`
 Dir.chdir(@source_dir)

run 方法作為入口比較簡(jiǎn)單,首先就是檢查是否取到所要編譯的 podspec 文件。然后針對(duì)它創(chuàng)建對(duì)應(yīng)的 working_directorytarget_directory

working_directory 為打包所在的臨時(shí)目錄,like this:

/var/folders/zn/1p8f0yls66b5788lshsjrd6c0000gn/T/cocoapods-rp699asa

target_directory 為最終生成 package 的所在目錄,是當(dāng)前 source code 目錄下新開(kāi)的:

${workspaceRoot}/KFData-1.0.5

create_working_directory 后,會(huì)主動(dòng)切換當(dāng)前 ruby 的運(yùn)行目錄至 working_directory 目錄下,正式開(kāi)始編譯。build_package 結(jié)束后,將編譯產(chǎn)物 copy 到 target_directory 下,同時(shí)切換回最初執(zhí)行命令所在目錄 @source_dir

build_package

builder = SpecBuilder.new(@spec, @source, @embedded, @dynamic)
newspec = builder.spec_metadata

@spec.available_platforms.each do |platform|
    build_in_sandbox(platform)

    newspec += builder.spec_platform(platform)
end

newspec += builder.spec_close
File.open(@spec.name + '.podspec', 'w') { |file| file.write(newspec) }

首先,創(chuàng)建一個(gè) SpecBuilder,其作用是用于生成描述最終產(chǎn)物的 podspec 文件,SpecBuilder 就是一個(gè)模版文件生成器。bundler 調(diào)用 spec_metadata 方法遍歷指定的 podspec 文件復(fù)刻出對(duì)應(yīng)的配置并返回新生成的 podspec 文件。

然后,根據(jù) target 所支持的 platform,iOS / Mac / Watch 依次執(zhí)行 build_in_sandbox 編譯,同時(shí)將 platform 信息寫(xiě)入 newspec,以 iOS 為例:

s.ios.deployment_target    = '8.0'
s.ios.vendored_framework   = 'ios/A.embeddedframework/A.framework'

最后,將 podspec 寫(xiě)入 target_directory 編譯結(jié)束。

build_in_sandbox

config.installation_root  = Pathname.new(Dir.pwd)
config.sandbox_root       = 'Pods'

static_sandbox = build_static_sandbox(@dynamic)
static_installer = install_pod(platform.name, static_sandbox)

if @dynamic
    dynamic_sandbox = build_dynamic_sandbox(static_sandbox, static_installer)
    install_dynamic_pod(dynamic_sandbox, static_sandbox, static_installer, platform)
end

begin
    perform_build(platform, static_sandbox, dynamic_sandbox, static_installer)
ensure # in case the build fails; see Builder#xcodebuild.
    Pathname.new(config.sandbox_root).rmtree
    FileUtils.rm_f('Podfile.lock')
end

Config

首先, config 與 initialize 的屬性 @config 完全是兩個(gè)東西,@config 只是一個(gè) 'Debug' or 'Release' 的字符串,而這里的 config 是 Pod::Config 的實(shí)例,默認(rèn)讀取自 ~/.cocoapods/config.yaml 目錄下的配置。

Package 在這里指定了 config 的安裝目錄 working_dirctory 和沙盒目錄 ./PodsConfig 類(lèi)是保存在 /path/to/cocoapods/lib/cocoapods/config.rb 中。它非常重要,包括我們 CocoaPods 平時(shí)的 install 過(guò)程以及這個(gè) Package 的 build 過(guò)程均是讀取的全局的 config。作為全局需要訪問(wèn)的對(duì)象,一定是個(gè) share instance 咯。在 CocoaPods 中是這么實(shí)現(xiàn)的;

def self.instance
    @instance ||= new
end

module Mixin
    def config
      Config.instance
    end
end

各個(gè)模塊通過(guò) include 來(lái)引入,比如 Command 類(lèi)中:

include Config::Mixin

然后就能愉快的使用了。

有了 config 之后,就可以創(chuàng)建沙盒來(lái)執(zhí)行 pod install。install 過(guò)程會(huì)針對(duì) static 和 dynamic 分別 install 一次。最后,基于安裝后的工程,創(chuàng)建 builder 開(kāi)始最終的構(gòu)建操作。沙盒創(chuàng)建和 install 是在 pod_utils.rb ,build 在 builder.rb 中,接下來(lái)會(huì)單獨(dú)展開(kāi)。

Pod Utils

整個(gè) pods_utils 文件均聲明為 Package 的 private 方法,主要做的是 build sandbox 和 pod install。install 會(huì)區(qū)分 static 和 dynamic。按照 build_in_sandbox 調(diào)用順序展開(kāi)聊聊。

build_static_sandbox

通過(guò) Pathname 先生成 static_sandbox_root ,然后返回 Sandbox.new(static_sandbox_root)。static_sandbox_root 會(huì)根據(jù)參數(shù) dynamic 來(lái)判斷,是否需要?jiǎng)?chuàng)建二級(jí)目錄 /static

if dynamic
    Pathname.new(config.sandbox_root + '/Static')
else
    Pathname.new(config.sandbox_root)
end

這么區(qū)分是由于,如果為動(dòng)態(tài)庫(kù),還會(huì)生成一個(gè) dynamic sandbox,其 path 為:

dynamic_sandbox_root = Pathname.new(config.sandbox_root + '/Dynamic')

介紹一下 SandBox ,其定義如下:

The sandbox provides support for the directory that CocoaPods uses for an installation. In this directory the Pods projects, the support files and the sources of the Pods are stored.

所以,sandbox 就是用于管理 pod install 目錄。

install_pod

既然是 pod install 當(dāng)然需要來(lái)一個(gè) podfile 了。package 會(huì)根據(jù)指定的 spec 來(lái)手動(dòng)創(chuàng)建 podfile。

def podfile_from_spec(path, spec_name, platform_name, deployment_target, subspecs, sources)
  options = {}
  if path
    if @local
        options[:path] = path
    else
        options[:podspec] = path
    end
  end
  options[:subspecs] = subspecs if subspecs
  Pod::Podfile.new do
    sources.each { |s| source s }
    platform(platform_name, deployment_target)
    pod(spec_name, options)

    install!('cocoapods', integrate_targets: false, deterministic_uuids: false)
    target('packager') do 
        inherit! :complete 
    end
  end
end

我們知道 Podfile 作為 DSL 文件,其每一行配置背后都對(duì)應(yīng)具體的方法,上面的這個(gè)就是簡(jiǎn)化版的 podfile 對(duì)應(yīng)的命令,options 就是我們配置的 pod 方法所需的參數(shù),對(duì)應(yīng)的是:pod(spec_name, options)。反轉(zhuǎn)過(guò)來(lái)對(duì)對(duì)應(yīng)的 podfile DSL 應(yīng)該是這樣的:

platform :ios, '8.0'
target 'packager' do
    pod "#{spec_name}", :path => "#{path}"# or :podspec => "#{path}"
end

這里如果參數(shù)中指定了 path 則不會(huì)通過(guò) spec 指定的 source 去download 源碼下來(lái)。

接著就是 pod install 了:

# podfile = podfile_from_spec  
...
static_installer = Installer.new(sandbox, podfile)
static_installer.install!

unless static_installer.nil?
    static_installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
        config.build_settings['CLANG_MODULES_AUTOLINK'] = 'NO'
        config.build_settings['GCC_GENERATE_DEBUGGING_SYMBOLS'] = 'NO'
    end
    end
    static_installer.pods_project.save
end

static_installer

這里重點(diǎn)就是 build_settings 的兩個(gè)配置。當(dāng) static_installer install 成功后會(huì)修改它們,作用是為了支持后續(xù)dynamic_framework 的編譯。GCC_GENERATE_DEBUGGING_SYMBOLS 這個(gè) config 應(yīng)該都知道吧?

我們來(lái)看另一個(gè) CLANG_MODULES_AUTOLINK,在 stackoverflow 上有一個(gè)解釋:

When you create a new project, Xcode will set the Link Frameworks Automatically flag to YES. Then the clang linking flag will be set to CLANG_MODULES_AUTOLINK = YES. With this option, clang will linking the framework for you automatically.

就是為了避免 install 后,項(xiàng)目 autolink。

install_dynamic_pod

build_dynamic_sandbox 前面已經(jīng)說(shuō)過(guò)了,我們直接看動(dòng)態(tài)庫(kù)的 install:

# 1 Create a dynamic target for only the spec pod.
dynamic_target = build_dynamic_target(dynamic_sandbox, static_installer, platform)

# 2. Build a new xcodeproj in the dynamic_sandbox with only the spec pod as a target.
project = prepare_pods_project(dynamic_sandbox, dynamic_target.name, static_installer)

# 3. Copy the source directory for the dynamic framework from the static sandbox.
copy_dynamic_target(static_sandbox, dynamic_target, dynamic_sandbox)

# 4. Create the file references.
install_file_references(dynamic_sandbox, [dynamic_target], project)

# 5. Install the target.
install_library(dynamic_sandbox, dynamic_target, project)

# 6. Write the actual .xcodeproj to the dynamic sandbox.
write_pod_project(project, dynamic_sandbox)

內(nèi)部分了 6 個(gè)方法,還附帶了詳細(xì)的注釋。需要注意的是,整個(gè) install_dynamic_pod 是基于 install_pod 所生成的 static_installer 來(lái)修改的。這里比較復(fù)雜,一步步來(lái)。

build_dynamic_target

# 1
spec_targets = static_installer.pod_targets.select do |target|
target.name == @spec.name
end
static_target = spec_targets[0]
# 2
file_accessors = create_file_accessors(static_target, dynamic_sandbox)
# 3
archs = []
dynamic_target = Pod::PodTarget.new(dynamic_sandbox, true, static_target.user_build_configurations, archs, platform, static_target.specs, static_target.target_definitions, file_accessors)
dynamic_target
  1. 通過(guò) select static_installer.pod_targets 篩選出 static_target,所過(guò)濾掉的是 target 是 spec 的第三方依賴。這里是過(guò)濾了 QueryKit

  2. 針對(duì)每個(gè) subspec 生成相應(yīng)的 FileAccessory :

    <Pod::Sandbox::FileAccessor spec=KFData platform=osx root=...>
    <Pod::Sandbox::FileAccessor spec=KFData/Attribute platform=osx root=...>
    <Pod::Sandbox::FileAccessor spec=KFData/Core platform=osx root=...>
    <Pod::Sandbox::FileAccessor spec=KFData/Essentials platform=osx root=...>
    <Pod::Sandbox::FileAccessor spec=KFData/Manager platform=osx root=...>
    <Pod::Sandbox::FileAccessor spec=KFData/Store platform=osx root=...>
    
  3. 創(chuàng)建 PodTarget。

prepare_pods_project

# Create a new pods project
pods_project = Pod::Project.new(dynamic_sandbox.project_path)

# Update build configurations
installer.analysis_result.all_user_build_configurations.each do |name, type|
  pods_project.add_build_configuration(name, type)
end

# Add the pod group for only the dynamic framework
local = dynamic_sandbox.local?(spec_name)
path = dynamic_sandbox.pod_dir(spec_name)
was_absolute = dynamic_sandbox.local_path_was_absolute?(spec_name)
pods_project.add_pod_group(spec_name, path, local, was_absolute)
pods_project

創(chuàng)建 Pod::Project,然后將 static project 中的 user configuration 復(fù)制過(guò)來(lái),最后為 target 新建 Pods Group,這里的 group 是 PBXGroup。什么是 PBXGroup 呢?看下面的分組就明白的了:

pbxgroup

我們平時(shí)說(shuō)看到的 Dependencies、Frameworks、Pods、Products、Target Support Files 他們?cè)?CocoaPods 中都是對(duì)應(yīng)了 PBXGroup。

copy_dynamic_target

從 static sandbox 中 cp 到 dynamic sandbox 目錄下。

install_file_references

installer = Pod::Installer::Xcode::PodsProjectGenerator::FileReferencesInstaller.new(dynamic_sandbox, pod_targets, pods_project)
installer.install!

通過(guò) FileReferencesInstaller 為 dynamic target 生成文件引用,為最后的 project 寫(xiě)入做準(zhǔn)備。FileReferencesInstaller 說(shuō)明:

Controller class responsible of installing the file references of the specifications in the Pods project.

install_library

將 dynamic_target 寫(xiě)入新建的 project,同時(shí)會(huì) install 依賴的 system framework。

write_pod_project

dynamic_project.pods.remove_from_project if dynamic_project.pods.empty?
dynamic_project.development_pods.remove_from_project if dynamic_project.development_pods.empty?
dynamic_project.sort(:groups_position => :below)
dynamic_project.recreate_user_schemes(false)

# Edit search paths so that we can find our dependency headers
dynamic_project.targets.first.build_configuration_list.build_configurations.each do |config|
config.build_settings['HEADER_SEARCH_PATHS'] = "$(inherited) #{Dir.pwd}/Pods/Static/Headers/**"
config.build_settings['USER_HEADER_SEARCH_PATHS'] = "$(inherited) #{Dir.pwd}/Pods/Static/Headers/**"
config.build_settings['OTHER_LDFLAGS'] = '$(inherited) -ObjC'
end
dynamic_project.save

最后,將 project 寫(xiě)入 dynamic sandbox,修改 Search Path 保證能查詢到依賴的 header 引用。

Builder

繼 project 創(chuàng)建和 pod install 后,Package 中的 perform_build 先將所需參數(shù)傳入以創(chuàng)建 Builder,開(kāi)始執(zhí)行 builder.build(@package_type)。build 會(huì)依據(jù) package_type 分為三種:

  • build_static_library
  • build_static_framework
  • build_dynamic_framework

這三個(gè)方法區(qū)別其實(shí)不大,就是 framework 的方法多了資源 copy 的過(guò)程。這里以 build_dynamic_framework 為例。

build_dynamic_framework

defines = compile
build_sim_libraries(defines)

if @bundle_identifier
defines = "#{defines} PRODUCT_BUNDLE_IDENTIFIER='#{@bundle_identifier}'"
end

output = "#{@dynamic_sandbox_root}/build/#{@spec.name}.framework/#{@spec.name}"

clean_directory_for_dynamic_build
if @platform.name == :ios
build_dynamic_framework_for_ios(defines, output)
else
build_dynamic_framework_for_mac(defines, output)
end

copy_resources

首先是先執(zhí)行 compile 構(gòu)建 Pods-packager ,成功后返回 defines。compile 實(shí)現(xiàn)如下:

defines = "GCC_PREPROCESSOR_DEFINITIONS='$(inherited) PodsDummy_Pods_#{@spec.name}=PodsDummy_PodPackage_#{@spec.name}'"
defines << ' ' << @spec.consumer(@platform).compiler_flags.join(' ')
if @platform.name == :ios
    options = ios_build_options
end
xcodebuild(defines, options)
if @mangle
    return build_with_mangling(options)
end
defines

如果沒(méi)有指定 --no-mangle 則正常執(zhí)行 xcodebuild,否則轉(zhuǎn)入 build_with_mangling

build_with_mangling 內(nèi)部也是調(diào)用 xcodebuild,區(qū)別在于它生成的 defines 不同。mangling 會(huì)遍歷 sandbox build 目錄下的 libxxx.a 靜態(tài)庫(kù)來(lái)查找,核心方法如下:

defines = Symbols.mangle_for_pod_dependencies(@spec.name, @static_sandbox_root)

mangle.rb 的描述如下:

performs symbol aliasing,for each dependency:

? - determine symbols for classes and global constants

? - alias each symbol to Pod#{pod_name}_#{symbol}

? - put defines into GCC_PREPROCESSOR_DEFINITIONS for passing to Xcode

mangling 也稱為 namespacing,會(huì)把類(lèi)名和全局常量改成 Pod#{pod_name}_#{symbol} 的形式。以我們調(diào)試的 KFData 的 PodsDummy_KFData 類(lèi)為例:

no-mangle: PodsDummy_KFData
mangling: PodKFData_PodsDummy_KFData

就是統(tǒng)一在類(lèi)前面添加了前綴,目的是為了避免靜態(tài)庫(kù)中的符號(hào)表沖突。比如,我們打包的 KFData 依賴了 QueryKit 那生成的 libKFData.a 靜態(tài)庫(kù)會(huì)有一份 QueryKit 的 copy,而此時(shí)如果在主工程里也直接引用了 QueryKit 那就會(huì)產(chǎn)生類(lèi)似 duplicate symbols for architecture x86_64 的錯(cuò)誤。

你可以 nm 工具來(lái)查看 class 的 symbol:

nm -gU KFData.framework/KFData | grep "_OBJC_CLASS_\$.*KF.*"

在查這個(gè)問(wèn)題時(shí),還填補(bǔ)了一個(gè)疑惑很久的問(wèn)題:Why do cocoapod create a dummy class for every pod? CocoPods 上 issue_3410 也有詳細(xì)討論

build_sim_libraries

由于 simulator 自由 iOS 有,實(shí)現(xiàn)比較簡(jiǎn)單,參數(shù)是前面 compile 返回的 defines。

if @platform.name == :ios
    xcodebuild(defines, '-sdk iphonesimulator', 'build-sim')
end

build_dynamic_framework_for_ios

# Specify frameworks to link and search paths
linker_flags = static_linker_flags_in_sandbox
defines = "#{defines} OTHER_LDFLAGS='$(inherited) #{linker_flags.join(' ')}'"

# Build Target Dynamic Framework for both device and Simulator
device_defines = "#{defines} LIBRARY_SEARCH_PATHS=\"#{Dir.pwd}/#{@static_sandbox_root}/build\""
device_options = ios_build_options << ' -sdk iphoneos'
xcodebuild(device_defines, device_options, 'build', @spec.name.to_s, @dynamic_sandbox_root.to_s)

sim_defines = "#{defines} LIBRARY_SEARCH_PATHS=\"#{Dir.pwd}/#{@static_sandbox_root}/build-sim\" ONLY_ACTIVE_ARCH=NO"
xcodebuild(sim_defines, '-sdk iphonesimulator', 'build-sim', @spec.name.to_s, @dynamic_sandbox_root.to_s)

# Combine architectures
`lipo #{@dynamic_sandbox_root}/build/#{@spec.name}.framework/#{@spec.name} #{@dynamic_sandbox_root}/build-sim/#{@spec.name}.framework/#{@spec.name} -create -output #{output}`

FileUtils.mkdir(@platform.name.to_s)
`mv #{@dynamic_sandbox_root}/build/#{@spec.name}.framework #{@platform.name}`
`mv #{@dynamic_sandbox_root}/build/#{@spec.name}.framework.dSYM #{@platform.name}`

上面貼的代碼看去很多,更多的是參數(shù)拼接的工作,主要做了三件事:

  • xcodebuild 模擬器支持的 framework;
  • xcodebuild 真機(jī)支持的 framework;
  • lipo 合并??生成的 framework 輸出到 #{working_directory}/ios ;

build_dynamic_framework_for_mac 省去 simulator build 這一步,也就沒(méi)有 lipo 操作,更簡(jiǎn)單。這里就不列出了。

最后就是 resource、header、license 等相關(guān)資源的 copy。至此大致流程就結(jié)束了,用簡(jiǎn)單流程來(lái)回顧一下:

cocoapods-packager

總結(jié)

cocoapods-packager 的邏輯還是比較簡(jiǎn)單的,整個(gè)熟悉過(guò)程困難還是在于對(duì) CocoaPods 的 API 和作用并不是和了解,導(dǎo)致很多調(diào)用需要查資料或者看 CocoaPods 源碼才能了大致理解。內(nèi)部實(shí)現(xiàn)還是圍繞 xcodebuild 來(lái)展開(kāi)的,通過(guò)生成 podfile 及 pod install 來(lái)構(gòu)建 project 環(huán)境,算是對(duì) CocoaPods 的深度集成了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,739評(píng)論 6 534
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,634評(píng)論 3 419
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,653評(píng)論 0 377
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,063評(píng)論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,835評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,235評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,315評(píng)論 3 442
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,459評(píng)論 0 289
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,000評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,819評(píng)論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,004評(píng)論 1 370
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,560評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,257評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,676評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,937評(píng)論 1 288
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,717評(píng)論 3 393
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,003評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容