[iOS] 基于CocoaPods 实现对XcodeProject的管理

1. 背景

某天被人问到Cocoapods实现的原理,突然发现自己只是会用这个包管理器,但对其实现的原理并不清楚,而又刚巧我们最近在做一些代码自动化的工作,后续可能有将代码自动植入进Xcode Project的需求,那CocoaPods的workflow中也包含这种添加代码和库的操作能力。基于以上,外加探索未知事物的好奇心,决定学习一下这个CocoaPods是如何游刃有余地管理和调度工程中的第三方库的。

2. CocoaPods Source Code

老规矩:先放源码,再讲故事。
CocoaPods有64个Repositories, 这里只说几个比较核心或者比较常用的库:

2.1 CocoaPods库

GitHub: https://github.com/CocoaPods
CocoaPods 官方源码库

2.2 CocoaPods/CocoaPod

GitHub: https://github.com/CocoaPods/CocoaPods
CocoaPods的主仓, 这是是一个面向用户的组件,每当执行一个 pod 命令时,这个组件都将被激活。该组件包括了所有使用 CocoaPods 涉及到的功能,并且还能通过调用所有其它的 gems 来执行任务。

2.3 CocoaPods/Core

GitHub: https://github.com/CocoaPods/Core
Core 组件提供支持与 CocoaPods 相关文件的处理,文件主要是 Podfile 和 podspecs。

2.4 CocoaPods/Xcodeproj

GitHub: https://github.com/CocoaPods/Xcodeproj
这个 gem 组件负责所有工程文件的整合。它能够对创建并修改 .xcodeproj 和 .xcworkspace 文件。它也可以作为单独的一个 gem 包使用。如果你想要写一个脚本来方便的修改工程文件,那么可以使用这个 gem。

3. CocoaPods介绍

CocoaPods是个iOS的包管理工具,类似于Android的Gradle,当然你也可以用Gradle管理iOS的依赖包,参见Gradle Xcode Plugin。 各个语言都有自己的工具,比如管理node的npm,以及python常用的pip,easy_install. Ruby常用gems,Java的maven等。
CocoaPods是与Xcode深度耦合的一个工具,里面大量的代码是对XcodeProj的直接操作,这也导致CocoaPods也只能用于iOS项目,甚至仅仅客户端的项目。目前一些基于Swift的Server项目, 可以用Apple官方出的包管理工具:SPM: Swift Package Manager
具体的使用中,创建Podfile、pod update这些都是基本操作,但这每一步操作的背后,Cocoapods都执行了哪些操作,触发了哪些模块,很值得我们研究一下。

3.1 整体结构

基于Xcode创建的一个新project,空空荡荡,但当运行了pod install之后,结构会发生很大的改变,大致如下:

PolenTest
├── PolenTest
│   ├── PolenTest
│   ├── PolenTest.xcodeproj
│   ├── PolenTestTests
│   └── PolenTestUITests
├── PolenTest.xcworkspace
│   └── contents.xcworkspacedata
├── PodFile
├── Podfile.lock
├── Pods
│   ├── AFNetworking
│   ├── Headers
│   ├── Manifest.lock
│   ├── Pods.xcodeproj
│   └── Target\ Support\ Files
├── exportOptions.plist
└── wehere-dev-cloud.mobileprovision

会多出了很多额外的文件,比较大的是整个项目由xcodeproj上升为xcworkspace。Pod会单独作为一个xcodeproj.

详细再展开看Pods的详细目录结构及说明如下:

Pods
├── Podfile # 指向根目录下的Podfile 说明依赖的第3方库
├── Frameworks  # 文件系统并没有对应的目录 这只是1个虚拟的group 表示需要链接的frameowork
├── └── iOS     # 文件系统并没有对应的目录 这只是1个虚拟的group 这里表示是ios需要链接的framework
├──     └── Xxx.framework # 链接的frameowork列表
├── Pods        # 虚拟的group 管理所有第3方库
│   └── AFNetwoking  #AFNetworking库 虚拟group 对应文件系统Pods/AFNetworking/AFNetworking目录下的内容
│       ├── xxx.h    #AFNetworking库的头文件 对应文件系统Pods/AFNetworking/AFNetworking目录下的所有头文件
│       ├── xxx.m    #AFNetworking库的实现文件 对应文件系统Pods/AFNetworking/AFNetworking目录下的所有实现文件
│       └── Support Files  # 虚拟group 支持文件 没有直接对应的文件系统目录,该group下的文件都属于目录: Pods/Target Support Files/AFNetworking/
│           ├── AFNetworking.xcconfig  # AFNetworking编译的工程配置文件
│           ├── AFNetworking-prefix.pch # AFNetworking编译用的预编译头文件
│           └── AFNetworking-dummy.m   # 空实现文件
├── Products  # 虚拟group
│   ├── libAFNetworking.a # AFNetworking target将生成的静态库 
│   └── libPods-CardPlayer.a  # Pods-CardPlayer target将生成的静态库              
└── Targets Support Files  # 虚拟group 管理支持文件
    └── Pods-CardPlayer    # 虚拟group Pods-CardPlayer target 
        ├── Pods-CardPlayer-acknowledgements.markdown # 协议说明文档
        ├── Pods-CardPlayer-acknowledgements.plist   # 协议说明文档
        ├── Pods-CardPlayer-dummy.m  # 空实现 
        ├── Pods-CardPlayer-frameworks.sh  # 安装framework的脚本
        ├── Pods-CardPlayer-resources.sh    # 安装resource的脚本
        ├── Pods-CardPlayer.debug.xcconfig  # debug configuration 的 配置文件
        └── Pods-CardPlayer.release.xcconfig # release configuration 的 配置文件
        

-- From Cocoapods原理总结 - Cloud Chou's Tech Blog

3.2 常用文件介绍

Pod里面有几个常用的文件,需要了解一下,这样平时在遇到问题时,也大概心里有概念是什么位置/什么级别的问题。

pod.lock

当前按照的第三方库及其版本信息, 执行pod installpod update时候,有没有pod.lock还是有一些细微的差异的, 直接看一下官方的解释:

pod install
This is to be used the first time you want to retrieve the pods for the project, but also every time you edit your Podfile to add, update or remove a pod.
Every time the pod install command is run — and downloads and install new pods — it writes the version it has installed, for each pods, in the Podfile.lock file. This file keeps track of the installed version of each pod and locks those versions.
When you run pod install, it only resolves dependencies for pods that are not already listed in the Podfile.lock.
For pods listed in the Podfile.lock, it downloads the explicit version listed in the Podfile.lock without trying to check if a newer version is available
For pods not listed in the Podfile.lock yet, it searches for the version that matches what is described in the Podfile (like in pod 'MyPod', '~>1.2')
pod update
When you run pod update PODNAME, CocoaPods will try to find an updated version of the pod PODNAME, without taking into account the version listed in Podfile.lock. It will update the pod to the latest version possible (as long as it matches the version restrictions in your Podfile).
If you run pod update with no pod name, CocoaPods will update every pod listed in your Podfile to the latest version possible.

--- From 官方文档: https://guides.cocoapods.org/using/pod-install-vs-update.html

mainfest.lock

这是每次运行 pod install 命令时创建的 Podfile.lock 文件的副本。
如果你遇见过这样的错误 沙盒文件与 Podfile.lock 文件不同步 (The sandbox is not in sync with the Podfile.lock),这是因为 Manifest.lock 文件和 Podfile.lock 文件不一致所引起。由于 Pods 所在的目录并不总在版本控制之下,这样可以保证开发者运行 app 之前都能更新他们的 pods,否则 app 可能会 crash,或者在一些不太明显的地方编译失败。
pod中有一个check mainfest的命令,可以看一下这里有解释: Check Pods Manifest.lock -- SatanWoo

Target Support Files

在Target Support Files目录下每1个第3方库都会有1个对应的文件夹,比如AFNetworking,该目录下有一个空实现文件,也有预定义头文件用来优化头文件编译速度,还会有1个xcconfig文件,该文件会在工程配置中使用,主要存放头文件搜索目录,链接的Flag(比如链接哪些库)
在Target Support Files目录下还会有1个Pods-XXX的文件夹,该文件夹存放了第3方库声明文档markdown文档和plist文件,还有1个dummy的空实现文件,还有debug和release各自对应的xcconfig配置文件,另外还有2个脚本文件,Pods-XXX-frameworks.sh脚本用于实现framework库的链接,当依赖的第3方库是framework形式才会用到该脚本,另外1个脚本文件: Pods-XXX-resources.sh用于编译storyboard类的资源文件或者拷贝*.xcassets之类的资源文件
--- From Cocoapods原理总结 - Cloud Chou's Tech Blog

Podspec

这个是pod的版本一些基本信息说明:比如比如版本号、source、license等。 如果要自己写一个开源库,那么需要执行:pod spec create xxxOPEN_SOURCEXXX, 自己创建一个.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.source_files = 'Reachability.{h,m}'
  spec.framework    = 'SystemConfiguration'
end

当然也有详细的版本,可以看Podspec Syntax Reference

大概知道以上这些文件之后,然后再来看真个CocoaPods从零开始的一个完整的流程:

4. CocoaPods的Workflow

简单来说,CocoaPods就是先识别下Podfile文件,然后基于Podfile文件,下载对应的库,然后将这些库插入到我们的项目中,各种依赖关系设置一下,然后更新一下本地记录就差不多了。
具体来看:

4.1 命令识别

当你输入pod init或者pod update的时候,cocosPods会有个command模块负责识别命令,常用命令如下:

Commands:

    + cache         Manipulate the CocoaPods cache
    + deintegrate   Deintegrate CocoaPods from your project
    + env           Display pod environment
    + init          Generate a Podfile for the current directory
    + install       Install project dependencies according to versions from a
                    Podfile.lock
    + ipc           Inter-process communication
    + lib           Develop pods
    + list          List pods
    + outdated      Show outdated project dependencies
    + plugins       Show available CocoaPods plugins
    + repo          Manage spec-repositories
    + search        Search for pods
    + setup         Setup the CocoaPods environment
    + spec          Manage pod specs
    + trunk         Interact with the CocoaPods API (e.g. publishing new specs)
    + try           Try a Pod!
    + update        Update outdated project dependencies and create new Podfile.lock

当我们输入一个命令后,会由command模块识别不同的命令,并转化为相应的响应者去执行这条命令,对于pod init这条命令,最终会触发 install.rb执行如下代码:

def run
    verify_podfile_exists!
    installer = installer_for_config
    installer.repo_update = repo_update?(:default => false)
    installer.update = false
    installer.deployment = @deployment
    installer.install!
end

上面的代码就是一个install的基本过程,会new一个installer,
installer_for_config会初始化一个config,同时会解析Podfile文件.
pod update也会走这样一段代码只是,update属性会设置为true
new 好installer之后,这个installer会开始接下来的install的工作:

    def install!
      prepare
      resolve_dependencies
      download_dependencies
      validate_targets
      generate_pods_project
      if installation_options.integrate_targets?
        integrate_user_project
      else
        UI.section 'Skipping User Project Integration'
      end
      perform_post_install_actions
    end

4.2 解析Podfile

首先会检查Podfile是否存在,不存在直接报错

def verify_podfile_exists!
  unless config.podfile
    raise Informative, "No `Podfile' found in the project directory."
  end
end

具体解析的过程是在installer_for_config里面开始的,
但想看具体代码,需要在CocoaPods/Core中的代码中,才能看到读取文件加解析的详细过程, 如下:

podfile = Podfile.new(path) do
    # rubocop:disable Lint/RescueException
    begin
        # rubocop:disable Eval
        eval(contents, nil, path.to_s)
        # rubocop:enable Eval
    rescue Exception => e
        message = "Invalid `#{path.basename}` file: #{e.message}"
        raise DSLError.new(message, path, e, contents)
    end
    # rubocop:enable Lint/RescueException
end

这里需要注意的是,eval直接执行podfile中的代码,因为是DSL语言,且每个方法都已定义清楚了为前提, 所以这一步其实只有很简单的一行eval(contents, nil, path.to_s).

module Pod
  class Podfile
    module DSL
      def pod(name = nil, *requirements) end
      def target(name, options = nil) end
      def platform(name, target = nil) end
      def inhibit_all_warnings! end
      def use_frameworks!(flag = true) end
      def source(source) end
      ...
    end
  end
end

关于DSL想进步一了解的,可以看一下:

其实解析文件,这些都属于准备工作,当然准备工作还包括一些路径检查、sandbox prepare、插件安装等。这些不是重点,就不做深入分析了,接下来才是真正的install工作:

4.3 解析依赖关系(resolve dependencies)

首先理清楚依赖关系:

    def resolve_dependencies
      plugin_sources = run_source_provider_hooks
      analyzer = create_analyzer(plugin_sources)

      UI.section 'Updating local specs repositories' do
        analyzer.update_repositories
      end if repo_update?

      UI.section 'Analyzing dependencies' do
        analyze(analyzer)
        validate_build_configurations
        clean_sandbox
      end

      UI.section 'Verifying no changes' do
        verify_no_podfile_changes!
        verify_no_lockfile_changes!
      end if deployment?

      analyzer
    end

因为很多依赖库里面,自身又有一些其他依赖库,比如常见的,很多库会对图片的处理会使用SDWebImage, 或者基础数据的处理,都会用Protobuf. 这就需要我们在下载所有依赖库之前,先递归的对所有要下载的库进行依赖关系的整理,确保不需要重复下载相同的库,确保每个依赖库的依赖库的依赖库都能被有效的下载和管理。
对于这样一套机制,CocoaPods使用了一套Milinillo算法进行实现。
算法实现:Milinillo
算法源码: https://github.com/CocoaPods/Molinillo
算法说明:核心算法是采用了backtrackingforward checking。 会维护了一个栈进行处理,栈中记录和跟踪了两个状态:依赖、可能性。每个依赖库的最新状态会push到栈上。每次一个已开库成功激活时,会有个新状态push到站上代表激活状态。基于栈实现的算法是backtracking(也叫unwinding)算法与出入栈的实现很类似。

具体流程其官方文档有了很详细的介绍,有兴趣的可以去实现以下。

这里面目前存在的一个问题是,虽然这个算法解决了依赖递归的问题,但是当依赖的依赖递归层级非常深,其同一个库,在不同的依赖中有不同的版本场景下,这个算法的效率会有一定的下降。

所以在使用中,尽量避免依赖递归太深的模式出现。这个问题美团在处理移动端的组件架构过程中遇到过,他们对此也做了一些优化工作:

但是如果对依赖树叶子节点的版本号控制不够严密,或中间出现了循环依赖的情况,会导致回溯算法重复执行了很多压栈和出栈操作耗费时间。美团针对此类问题的做法是维护一套“去依赖的podspec源”,这个源中的dependency节点被清空了(下图中间)。实际的所需依赖的全集在壳工程Podfile里平铺,统一维护。这么做的好处是将之前的树状依赖(下图左)压平成一层(下图右)

-- From: 美团外卖iOS多端复用的推动、支撑与思考

4.4 下载依赖库(download dependencies)

上一步最终会返回一个需要下载的依赖库列表root_specs,是个Specification泛型的数组[Array<Specification>]。接下来我们需要将这些依赖库和本地版本进行对比,然后将新版本下拉更新。代码如下:

    def install_pod_sources
      @installed_specs = []
      pods_to_install = sandbox_state.added | sandbox_state.changed
      title_options = { :verbose_prefix => '-> '.green }
      root_specs.sort_by(&:name).each do |spec|
        if pods_to_install.include?(spec.name)
          if sandbox_state.changed.include?(spec.name) && sandbox.manifest
            current_version = spec.version
            previous_version = sandbox.manifest.version(spec.name)
            has_changed_version = current_version != previous_version
            current_repo = analysis_result.specs_by_source.detect { |key, values| break key if values.map(&:name).include?(spec.name) }
            current_repo &&= current_repo.url || current_repo.name
            previous_spec_repo = sandbox.manifest.spec_repo(spec.name)
            has_changed_repo = !previous_spec_repo.nil? && current_repo && !current_repo.casecmp(previous_spec_repo).zero?
            title = "Installing #{spec.name} #{spec.version}"
            title << " (was #{previous_version} and source changed to `#{current_repo}` from `#{previous_spec_repo}`)" if has_changed_version && has_changed_repo
            title << " (was #{previous_version})" if has_changed_version && !has_changed_repo
            title << " (source changed to `#{current_repo}` from `#{previous_spec_repo}`)" if !has_changed_version && has_changed_repo
          else
            title = "Installing #{spec}"
          end
          UI.titled_section(title.green, title_options) do
            install_source_of_pod(spec.name)
          end
        else
          UI.titled_section("Using #{spec}", title_options) do
            create_pod_installer(spec.name)
          end
        end
      end
    end

对于已经下载过的依赖库,会进行版本比较,如需下载会执行install_source_of_pod(spec.name), 如果是首次下载的依赖库,则会执行create_pod_installer(spec.name), 别看这几个方法简单,但是调用栈看起来,其实很复杂, draveness同学的博客提供了完整的调用栈:

installer.install_source_of_pod
|-- create_pod_installer
|   `-- PodSourceInstaller.new
`-- podSourceInstaller.install!
    `-- download_source
       `-- Downloader.download
           `-- Downloader.download_request
               `-- Downloader.download_source
                   |-- Downloader.for_target
                   |   |-- Downloader.class_for_options
                   |   `-- Git/HTTP/Mercurial/Subversion.new
                   |-- Git/HTTP/Mercurial/Subversion.download
                   `-- Git/HTTP/Mercurial/Subversion.download!
                       `-- Git.clone

下载的实现是基于Downloader这个类实现的,CocoaPods专门讲这个库开源了CocoaPods/cocoapods-downloader,
从Cocoapods的代码我们只能看到最终的下载代码就2行:

downloader = Downloader.for_target(target, params)
downloader.download

如果继续看cocoapods-downloader源码,可以看到他支持了多种下载方式包括Git、Http等。基于不同的设置属性,定义了不同的strategy。目前支持的下载strategy有BazaarGitMercurialHttpScpSubversion.

# @return [Hash{Symbol=>Class}] The concrete classes of the supported
#         strategies by key.
#
def self.downloader_class_by_key
    {
        :bzr  => Bazaar,
        :git  => Git,
        :hg   => Mercurial,
        :http => Http,
        :scp  => Scp,
        :svn  => Subversion,
    }
end

基于工厂模式,每种下载方式,实现了自己的def download!方法, 以Class Git为例,其方法实现如下:

def download!
    clone
    checkout_commit if options[:commit]
end

def clone_arguments(force_head, shallow_clone)
    command = ['clone', url, target_path, '--template=']

    if shallow_clone && !options[:commit]
        command += %w(--single-branch --depth 1)
    end

    unless force_head
        if tag_or_branch = options[:tag] || options[:branch]
        command += ['--branch', tag_or_branch]
        end
    end

    command
end

可以看到Git就做了clone和checkout操作。我们经常使用git clone其ruby实现是先组装一个command,然后Hooks.execute_with_check("git", command, false)执行git 命令。

以上就是从代码调用角度看到的下载流程。另外也有看到其他童鞋将下载的步骤整体画了一个流程图:


cocoapods_download_1499312461568920.png

这里就不展开分析, 可以直接去看这篇博客: Cocoapods私有库管理实现

4.5 校验依赖库(validate targets)

下载好之后,接下来是对本地Xcodeproj的操作。
日常规则: 任何事之前,先验证一下,这里也不意外,Xcode的target需要先做一些校验工作

    def validate_targets
      validator = Xcode::TargetValidator.new(aggregate_targets, pod_targets)
      validator.validate!
    end

验证的工作主要就是没有duplicate的frame或者library,一些静态库的校验、Swift库的版本校验等。

4.6 创建Pod.xcodeproj (generate pods project)

首先有个很重要的前提是:我们需要了解Xcode的project.pbxproj的内部结构,这个我之前在XcodeProject的内部结构分析 中有过介绍。
那么,如何修改Xcodeproj的文件呢?
除了之前介绍的GitHub: mjmsmith/pbxplorer, Cocoapods自己实现了一个开源库CocoaPods/Xcodeproj

创建的过程大体流程基本是:创建Pod.xcodeproj工程、添加文件、添加库、设置库依赖。代码如下:

  def generate!
    prepare
    install_file_references
    @target_installation_results = install_targets
    integrate_targets(@target_installation_results.pod_target_installation_results)
    app_hosts_by_host_key = install_app_hosts
    wire_target_dependencies(@target_installation_results, app_hosts_by_host_key)
    @target_installation_results
  end

CocoaPods在第一步prepare中通过Pod::Project.new创建了一个pod的project

def create_project
  if object_version = aggregate_targets.map(&:user_project).compact.map { |p| p.object_version.to_i }.min
    Pod::Project.new(sandbox.project_path, false, object_version)
  else
    Pod::Project.new(sandbox.project_path)
  end
end

然后将第三库的代码和资源文件加入工程(实际就是修改PBXFileReference), 这里自己可以打开Xcode看一下Pod增加的文件是引用模式,而非create group模式(就是目录与物理目录不对应)。
然后添加Target、设置Target依赖关系。添加的Target除了本身Podfile读取的依赖库,还添加了一个Pods-xxx你的项目名xxx Target。这个Target依赖了其他全部的第三方库。

4.7 创建Xcode.workspace

    def integrate_user_project
      UI.section "Integrating client #{'project'.pluralize(aggregate_targets.map(&:user_project_path).uniq.count)}" do
        installation_root = config.installation_root
        integrator = UserProjectIntegrator.new(podfile, sandbox, installation_root, aggregate_targets)
        integrator.integrate!
      end
    end
    
    ...
    
     def integrate!
        create_workspace
        integrate_user_targets
        warn_about_xcconfig_overrides
        save_projects
     end

这一步是创建workspace, 如果已经创建过了,逻辑会去判断,就不会走到这里来。
然后会重新整理一下项目的Target,将不用的依赖删除,移除引用等。deintegrator.deintegrate_target(target)
然后检查一些xcconfig文件,是否有写$(inherited), 没写的话,会覆盖掉系统的一些config条件,所以会出一些警告。我们之前项目中,在CI脚本中,因为Config没写$(inherited), 也导致出现了依赖库的宏定义不可用的情况。

    def perform_post_install_actions
      run_plugins_post_install_hooks
      warn_for_deprecations
      warn_for_installed_script_phases
      print_post_install_message
    end

最后一波收尾操作,run一些plugin,script, 对于deprecated的pod做一些警告⚠️之类的。
然后告诉你:

Pod installation complete!

4.8 总结

看了一波又一波的代码,应该是有心理有个大致的印象和流程了,我们再简单整理下,CocoaPods的完整过程, 其实再回头看前面放的一段代码:

    def install!
      prepare
      resolve_dependencies
      download_dependencies
      validate_targets
      generate_pods_project
      if installation_options.integrate_targets?
        integrate_user_project
      else
        UI.section 'Skipping User Project Integration'
      end
      perform_post_install_actions
    end
  • CocoaPods先读取了Podfile的文件,知道了用户需要下载哪些依赖库,
  • Analyzer基于Milinillo算法处理复杂的递归依赖关系,得到依赖图谱DependencyGraph.
  • 开始下载这些依赖,下载支持多种strategy,本质上讲就是,执行各种git clone命令。
  • 下载成功后,然后生成pods.project,讲文件和资源加入项目工程目录.
  • 生成 xcode的workspace.
  • 设置Target的依赖关系,Config文件,framework的路径等,完成这个xcode的配置工作.

以上就是CocoaPods的workflow, 关于这部分想深入学习的同学,可以看CocoaPods 都做了什么?, 目前看过的博客里介绍的最深度的一篇。

5. 背后的逻辑

5.1 如果我们自己来实现

看完了CocoaPods的完整流程,想一想,步骤1,2,3,4,5... 让我们闭上眼思考下:如果要我们自己去实现一个CocoaPods,那我们需要哪些步骤,以及会遇到哪些问题呢?

  1. 首先我们需要定制一个DSL语言,面向开发者可以写出很简单的依赖库的定制需求。如果你对ruby比较熟悉,那么这应该不会是一个难点。
  2. 然后解析和下载,以及依赖关系的处理,虽然看起来麻烦,假设不考虑最优算法的话,我们用简单的逻辑也可以实现,如果你写递归算法得心应手,写个网络请求和下载管理轻车熟路,那么这应该也可以解决。
  3. 面对下载好的代码和资源文件,如何让他一键加入到工程中,这个对很多人来说,可能略有麻烦,首先,你需要深入理解project.pbxproj内部复杂的层级结构以及定义的各种引用和依赖关系(不知道的看这里)。然后知道如何为每个文件生成唯一的UUID, 并在相应project工程文件中添加文件信息,路径信息等。这一步假设纯徒手开发的话,可能需要一定的时间和功底去研究,遇到的困难也会多一些。
  4. 那假设以上步骤,最终都看似跌跌撞撞,但总归顺利般的完成了。那也已经很厉害了。
    接下来。已有projectXXX,是我们的当前的原始项目,他想依赖3-4个第三方库(并已经下载到本地),如何让他可以找到这几个三方库。这一步同样需要对project.pbxproj的操作,但是我们再第3步,已经对这个操作游刃有余,这一步需要考虑的是Xcode中静态库/动态库与Project之前的依赖引用问题。这一步的复杂与否,核心在于你对Xcode中workspace、Project、Target、Framework/Library之间的关系理解程度,如果很清晰的理解,就可以很快的实现,如果理解不深,就需要研究清楚才能知道怎么做。最后的临门一脚,就是基于我们之前已经完成的这个一个个节点,将这些节点串起来成为一条完成的workflow

5.2 关于Workspace/Target依赖的想法

说一下,自己对最后一步的理解。需求是: 已有projectXXX,是我们的当前的原始项目,他想依赖3-4个第三方库(并已经下载到本地),如何让他可以找到这几个三方库。
如果头脑风暴一下,可以产生一些基本思路:

方案A:

我们可以直接将三方库的代码加入到项目中,完全避开Target的概念,就像开发者自己多写了几个文件一样,要用到这些代码,直接import即可(如果是swift,甚至都不用import,直接用)

方案B:

建立一个新的Target, 这个Target直接添加在原始项目中,其内部添加所有的第三方库。

方案C:

为每个第三方库都建议一个独立的Target,然后这些Target再统一与原始项目建立依赖关系。

方案D:

为每个Target建立一个独立的Project,基于workspace对于所有proj建立依赖关系。

方案E:

...

很显然,方案A过于简单粗暴,且不方便后续的第三方库的版本管理,不是一个很好的选择。方案B是个可接受的方案,但是所有的第三方库都在一个Target中,那么需要很复杂的去修改config building中Link Binary With Libraries以及部分search path,所有第三方库都只能共用同一个target的config,扩展性受限。那对比方案C和方案D, 先说方案D,假设我们引用了20多个第三方库,那么就有20多个project,这个想想是个多么可怕的事情,所有projec直接的引用和依赖关系,以及整个项目的复杂度,都会复杂很多,所以方案D显然有点“用力过猛”。那么方案C是看似可行的一个方案,每个第三方库建里一个自己的Target的好处是,每个库可以配置自己的config,然后target彼此之间的依赖关系,可以通过配置Build Phases中的Target Dependencies实现。那么接下来的问题是这些Target放到哪里?
最简单的是将这些Target是全部放到原始的Project中,虽然可行,但是和原始Project耦合度太高,尤其是对于原始Project本身就有多个自有Target的场景,所以更好的策略是: 可以参考方案D,我们在更高的层面上,建立一个workspace,内部新建一个第三方的Project,将这些第三方的Target统一放在一起。最后只需要将2个Project(原始项目的Project和我们新建的第三方Project)建立关联就可以。目前CocoaPods实现的方式是,会建立一个Pods-xxxx你项目名xxx的Target,这个Target依赖了其他所有的第三方库,然后基于这个Target的framework:Pods_xxxx你项目名xxx.framework,会嵌入到原始Project的Link Binary With Libraries中。
简单点说,就是所有第三方库都放在一个Pod的Project中,然后建立一个中间件Target,承上启下,关联起了2个Project之间的引用。
基于我们对方案C的扩展实现,最终串联起来了整个第三方库在项目中的依赖结构。

想进一步学习,可以参看:

6. 有用的轮子

6.1 XcodeProject

研究完CocoaPods, 看看里面有用的轮子,首当其冲的就是XcodeProject了。这里直接说怎么用吧。
(注意: 以下都是ruby代码哈)

6.1.1 读取我们的project:

require 'xcodeproj'

project_path = ''    # 工程的全路径
project = Xcodeproj::Project.open(project_path)

6.1.2 读取所有的Target

project.targets.each do |target|
  puts target.name
end

6.1.3 group中添加文件

target = project.targets.first
group = project.main_group.find_subpath(File.join('testXcodeproj','newGroup'), true)
group.set_source_tree('SOURCE_ROOT')
# 文件加入到reference中
file_ref = group.new_reference("xxxPath/xx.h")
# 文件加入到build phase中 (.h文件只加到reference中即可,.m或者.swift文件还需要加入到build phase才能编译)
target.add_file_references([file_ref])
project.save

6.1.4 引入framework或静态库

#添加xx.framework的引用
file_ref = project.frameworks_group.new_file('xxPath/xx.framework')
target.frameworks_build_phases.add_file_reference(file_ref)

#添加xx.a的引用
file_ref = project.frameworks_group.new_file('xxPath/xx.a')
target.frameworks_build_phases.add_file_reference(file_ref)

#添加xx.bundle的引用
file_ref = project.frameworks_group.new_file('xxPath/xx.bundle')
target.resources_build_phase.add_file_reference(file_ref)

project.save

进一步学习可参考:

以上只介绍了XcodeProject这个轮子,当然CocoaPods也有很多其他优秀的轮子,以后如果用到了,我会在这里补充更新。

7. One more

Cocoapods 的亲历者说:The Road to CocoaPods 1.0
这个是CocoaPods的开发者之一Samuel E. Giddins在2016年发布CocoaPods1.0版本时的一篇总结性博客,记录了开发中的一些小故事, 有兴趣的同学可以去看看。


参考资料

  1. GitHub: CocoaPods 官方源码
  2. GitHub: mjmsmith/pbxplorer
  3. Xcode Project File Format: 对.pbxproj文件每个参数的详细介绍
  4. CocoaPods 都做了什么?- 目前看过的博客里介绍的最深度的一篇
  5. XCode工程文件结构及Xcodeproj框架的使用( 二 )
  6. 使用代码为 Xcode 工程添加文件
  7. 添加iOS文件链接,操作.xcodeproj
  8. Xcode: Frameworks, By Thomas, January 23, 2017
  9. What is the difference between Embedded Binaries and Linked Frameworks
  10. XCODE, FRAMEWORKS, AND EMBEDDED FRAMEWORKS
  11. 细聊 Cocoapods 与 Xcode 工程配置
  12. 深入理解 CocoaPods
  13. CocoaPods建立自己的Podspec(三)
  14. Cocoapods原理总结 - 掘金
  15. Podfile.lock背后的那点事 - 2015 Startry
  16. XcodeProject的内部结构分析
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345