Cocoapods源码解析

iOS开发者多少都使用过cocoapods来管理第三方依赖, 这里就对cocoapods的源码分析一下, 深入观察它是如何管理第三方依赖包的?

DSL

要了解Cocoapods的源码, 首先还得了解下 领域特定语言 DSL(domain-specific languages)

为什么呢?

因为我们使用Cocoapods的时候编辑的Podfile文件的内容就属于DSL范畴了

DSL 大致解释是:在模型之上建立的一种更加灵活的对模型化的理解和使用方式.

翻译过来的个人理解就是: 一种mini语言,和通用编程语言不太一样,用于特定领域,特定用途的语言,能够做到简单,易于理解的解决特定问题.

对于Cocoapods来说,Podfile的编写规范就是一种DSL, 它是基于Ruby基础上制定的,所以很轻量易懂.

Cocoapods的Podfile规范在这里 https://guides.cocoapods.org/syntax/podfile.html

Cocopods源码模块

在看cocoapods源码的时候,大致总结了cocoapods的功能模块,如下图:

PS: 上面这些模块不都是在一个Gem源里,从Cocoapods的Gem配置文件cocoapods.gemspec可以看到,Cocoapods根据功能拆分了多个Gem源,做好模块化了

Cocoapods源码解析

源码阅读方式是从pod initpod install两个最常用命令入手,按照执行顺序和调用关系来进行的.

可能你会问这些Cocoapods的CLI命令是如何调用的? 嘿嘿, 你可以移步这里来帮你解读

Cocopoads命令调度

问题:Cocoapods是如何调度命令的?

首先涉及三个文件的调用:

CLAide模块的Command类: 所有指令类的基类

Pod模块Command类: Pod模块的指令基类

Pod模块Init类: 代表各种命令(install,list等), Pod模块Command的子类

CLI输入命令`pod init`
 -> 调用到Pod模块的Command类的run方法,接收一个list参数`['pod','init']`
 -> 调用CLAide模块的Command类的run方法,解析参数
 -> CLAide模块的Command类通过查找所有子类,找到Init类,执行子类的run方法

那么首先CLAide模块的Command类如何解析参数的呢?

    # @param  [Array, ARGV] argv
    #         A list of (remaining) parameters.
    #
    # @return [Command] An instance of the command class that was matched by
    #         going through the arguments in the parameters and drilling down
    #         command classes.
    #
    def self.parse(argv)
      argv = ARGV.coerce(argv)
      cmd = argv.arguments.first
      if cmd && subcommand = find_subcommand(cmd)
        argv.shift_argument
        subcommand.parse(argv)
      elsif abstract_command? && default_subcommand
        load_default_subcommand(argv)
      else
        new(argv)
      end
    end

看代码可以知道通过指令数组,循环查找到可执行的命令并解析,直到找到最终的可用命令为止, 那么这里如何查找所有子类呢?

    # @visibility private
    #
    # Automatically registers a subclass as a subcommand.
    #
    def self.inherited(subcommand)
      subcommands << subcommand
    end

还是要调用到Ruby原生API的inherited方法,该方法作用是当有子类被初始化的时候就会调用一次,在这里Command类保存了所有的子类的class.

类图

说明: 这个类图不包含所有Cocoapods的类,但是基本覆盖到了使用pod initpod install命令的类, 其他的命令原理都差不多,也是基于这里的调用.

敲黑板:

Podfile的解析

我们费尽按照一个规范编写了一个Podfile文件,那这个文件Cocoapods是怎么用的呢?

从上面的类图中可以看到有个类Pod Profile DSL,没错就是它,他是这个协议的解析者.

当执行pod的命令的时候,Cocopods会读出这个文件内容,通过eval方法按照代码老执行文件的内容(eval方法下面会再聊)

从上面的类图中,DSL类定义了很多方法,这些方法都是Podfile的规范里面的关键字,例如: 在Podfile中添加了一个依赖pod 'AFNetworking',在podfile.rb文件中执行到这一行的时候,会调用pod方法:

      def pod(name = nil, *requirements)
        unless name
          raise StandardError, 'A dependency requires a name.'
        end

        current_target_definition.store_pod(name, *requirements)
      end

可以看到这里通过store_pod方法将AFNetworking和参数一起保存到内存中的一个Hash中,类似的其他方法

source 'https://github.com/CocoaPods/Specs.git'

target 'MyApp'

都会调用Podfile中的相应方法,把信息转换成对象存储下来,供之后的命令来使用.

pod init

init命令的功能就是会创建Podfile模板,流程比较简单如下:

  1. 初始化全局config,当然config是懒加载的,使用的时候才会创建
  2. 初始化Podfile文件目标路径
  3. 先调用validate!方法(这个是父类CLAide模块的Command类调用的),检查命令执行目录是否有.xcodeproj文件
  4. 如果找到.xcodeproj文件后打开这个文件读入到内存中
  5. 调用podfile_template方法编写Podfile模板文件,通过.xcodeproj文件获取target信息
  6. Podfile的模板文件写入目标路径

pod install

pod install这个命令执行的时候Cocoapods是如何运作的?

Installer类主要负责将Podfile文件转换成Pods库,生成.xcworkspace工程文件,配置好第三方库的依赖, 它主要从三个文件获取配置信息:

当install的时候会执行以下动作:

  1. 初始化全局config, 将Podfile执行解析成对象, 通过eval执行
  2. prepare 准备工作
    • 检查安装目录,必须在项目根目录
    • 检查Podfile.lock文件cocoapods版本,如果主版本不一样, 会重新集成cocoapods
    • 创建安装目录Pods及子目录
    • 检查Podfile中的plugin插件都已经安装并加载
    • 加载插件
  3. resolve_dependencies 解决依赖
    • 检查是否需要更新podsource源
    • 如果Podfile中有删除的库, 进行清理文件
  4. download_dependencies 下载依赖库
    • 下载各个pod
    • 执行Podfilepre_install钩子方法
  5. validate_targets 验证targetpod正确
  6. generate_pods_project 生成'Pods/Pods.xcodeproj工程
    • 调用Podfilepost_install钩子方法
    • 生成Pods工程
    • 生成Podfile.lock文件和Manifest.lock文件
  7. integrate_user_project 集成
    • 创建.xcworkspace文件
    • 集成Target
    • 警告检查
    • 保存.xcworkspace文件到目录
  8. 调用pluginpost_install钩子方法

这里是如何执行读取到的Podfile文件内容呢? 这个就要说道eval方法了,它的作用就是将字符串内容按照代码来执行, 例如

$ eval "1+1"
$ > 2

那在Cocoapods中真正执行Podfile内容的位置就在Pod模块的Podfile类中:

# Configures a new Podfile from the given ruby string.
    #
    # @param  [Pathname] path
    #         The path from which the Podfile is loaded.
    #
    # @param  [String] contents
    #         The ruby string which will configure the Podfile with the DSL.
    #
    # @return [Podfile] the new Podfile
    #
    def self.from_ruby(path, contents = nil)
        contents ||= File.open(path, 'r:utf-8', &:read)
      # ...省略部分
      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
      podfile
    end

上段代码中参数contents的内容就是从Podfile读出来的字符串,然后通过eval方法执行这段字符串

pod update

pod update命令就比较简单了, 内部调用了pod install的逻辑,唯一的区别是:

update会跳过Podfile.lock文件,更新所有repo源, 官方说明在这里

在代码中是通过update参数控制的:

Install的run方法:

      def run
        puts 'install file run'
        verify_podfile_exists!
        installer = installer_for_config
        installer.repo_update = repo_update?(:default => false)
        installer.update = false
        installer.install!
      end

Update的run方法:

def run
        verify_podfile_exists!

        installer = installer_for_config
        installer.repo_update = repo_update?(:default => true)
        if @pods
          verify_lockfile_exists!
          verify_pods_are_installed!
          installer.update = { :pods => @pods }
        else
          UI.puts 'Update all pods'.yellow
          installer.update = true
        end
        installer.install!
      end

在Pod模块的Installer类中进行判断是否进行更新所有repo源.

总结

Cocoapods的库源代码的代码量还是比较大的, 这里只是窥视了主流程, 而且Cocoapods也拆分了很多模块到独立的Gem源,希望能让大家理解Pod指令的运行过程.

另外Cocoapods的创建独立的Cocoapods源和私有Pod没有涉及到,这部分也非常有用,可以用于工程模块化开发的基础,后面再来研究.