@qidiandasheng
2022-01-04T14:04:32.000000Z
字数 14692
阅读 1441
Cocoapods
CocoaPods-Core 用于CocoaPods中配置文件的解析,包括Podfile、Podspec以及解析后的依赖锁存文件,如Podfile.lock等。
我们先通过入口文件 lib/cocoapods-core.rb 来一窥 Core 项目的主要文件:
module Podrequire 'cocoapods-core/gem_version'class PlainInformative < StandardError; endclass Informative < PlainInformative; endrequire 'pathname'require 'cocoapods-core/vendor'# 用于存储 PodSpec 中的版本号autoload :Version, 'cocoapods-core/version'# pod 的版本限制autoload :Requirement, 'cocoapods-core/requirement'# 配置 Podfile 或 PodSpec 中的 pod 依赖autoload :Dependency, 'cocoapods-core/dependency'# 获取 Github 仓库信息autoload :GitHub, 'cocoapods-core/github'# 处理 HTTP 请求autoload :HTTP, 'cocoapods-core/http'# 记录最终 pod 的依赖信息autoload :Lockfile, 'cocoapods-core/lockfile'# 记录 SDK 的名称和 target 版本autoload :Platform, 'cocoapods-core/platform'# 对应 Podfile 文件的 classautoload :Podfile, 'cocoapods-core/podfile'# 管理 PodSpec 的集合autoload :Source, 'cocoapods-core/source'# 管理基于 CDN 来源的 PodSpec 集合autoload :CDNSource, 'cocoapods-core/cdn_source'# 管理基于 Trunk 来源的 PodSpec 集合autoload :TrunkSource, 'cocoapods-core/trunk_source'# 对应 PodSpec 文件的 classautoload :Specification, 'cocoapods-core/specification'# 将 pod 信息转为 .yml 文件,用于 lockfile 的序列化autoload :YAMLHelper, 'cocoapods-core/yaml_helper'# 记录 pod 依赖类型,是静态库/动态库autoload :BuildType, 'cocoapods-core/build_type'...Spec = Specificationend
将这些 Model 类按照对应的依赖关系进行划分,层级如下:

TargetDefinition 是一个多叉树结构,每个节点记录着 Podfile 中定义的 Pod 的 Source 来源、Build Setting、Pod 子依赖等。该树的根节点指向 Podfile,而 Podfile 中的 root_target_definitions 则记录着所有的 TargetDefinition 的根节点,正常情况下该 list 中只有一个 root 即 Pods.project。
为了便于阅读,简化了大量的 DSL 配置相关的方法和属性并对代码顺序做了调整,大致结构如下:
module Podclass Podfileclass TargetDefinition# 父节点: TargetDefinition 或者 Podfileattr_reader :parent# 子节点: TargetDefinitionattr_reader :children# 记录 tareget 的配置信息attr_accessor :internal_hashdef root?parent.is_a?(Podfile) || parent.nil?enddef rootif root?selfelseparent.rootendenddef podfileroot.parentend# ...endend
在初始化Podfile对象时就会初始化根TargetDefinition:
# podfile.rbdef initialize(defined_in_file = nil, internal_hash = {}, &block)self.defined_in_file = defined_in_file@internal_hash = internal_hashif block# TargetDefinition名为Pods,parent父节点为podfiledefault_target_def = TargetDefinition.new('Pods', self)default_target_def.abstract = true@root_target_definitions = [default_target_def]@current_target_definition = default_target_definstance_eval(&block)else@root_target_definitions = []endend
然后在Podfile文件里的每一个target声明都会调用dsl.rb里对应的target函数,初始化对应的子TargetDefinition:


Specification即存储PodSpec的内容,是用于描述一个Pod库的源代码和资源将如何被打包编译成链接库或framework。
Podspec 支持的文件格式为 .podspec 和 .json 两种,而 .podspec 本质是 Ruby 文件。
在数据结构上Specification与TargetDefinition是类似的,同为多叉树结构。这里的parent是为 subspec保留的,用于指向其父节点的Spec。简化后的Spec的类如下:
require 'active_support/core_ext/string/strip.rb'# 记录对应 platform 上 Spec 的其他 pod 依赖require 'cocoapods-core/specification/consumer'# 解析 DSLrequire 'cocoapods-core/specification/dsl'# 校验 Spec 的正确性,并抛出对应的错误和警告require 'cocoapods-core/specification/linter'# 用于解析 DSL 内容包含的配置信息require 'cocoapods-core/specification/root_attribute_accessors'# 记录一个 Pod 所有依赖的 Spec 来源信息require 'cocoapods-core/specification/set'# json 格式数据解析require 'cocoapods-core/specification/json'module Podclass Specificationinclude Pod::Specification::DSLinclude Pod::Specification::DSL::Deprecationsinclude Pod::Specification::RootAttributesAccessorsinclude Pod::Specification::JSONSupport# `subspec` 的父节点attr_reader :parent# `Spec` 的唯一 id,由 name + version 的 hash 构成attr_reader :hash_value# 记录 `Spec` 的配置信息attr_accessor :attributes_hash# `Spec` 包含的 `subspec`attr_accessor :subspecs# 递归调用获取 Specification 的根节点def rootparent ? parent.root : selfenddef hashif @hash_value.nil?@hash_value = (name.hash * 53) ^ version.hashend@hash_valueend# ...endend
Specification 同样用 map attributes_hash 来记录配置信息。
Podfile 是用于描述一个或多个 Xcode Project 中各个 Targets 之间的依赖关系。
这些 Targets 的依赖关系对应的就是 TargetDefinition 树中的各子节点的层级关系。如前面所说,有了 Podfile 这个根节点的指向,仅需对依赖树进行遍历,就能轻松获取完整的依赖关系。
有了这层依赖树,对于某个 Pod 库的更新即是对树节点的更新,便可轻松的分析出此次更新涉及的影响。
简化调整后的 Podfile 代码如下:
require 'cocoapods-core/podfile/dsl'require 'cocoapods-core/podfile/target_definition'module Podclass Podfileinclude Pod::Podfile::DSL# podfile 路径attr_accessor :defined_in_file# 所有的 TargetDefinition 的根节点, 正常只有一个,即 Pods.project targetattr_accessor :root_target_definitions# 记录 Pods.project 项目的配置信息attr_accessor :internal_hash# 当前 DSL 解析使用的 TargetDefinitionattr_accessor :current_target_definition# ...endend
直接看 dsl.rb,该文件内部定义了 Podfile DSL 支持的所有方法。通过 include 的使用将 Pod::Podfile::DSL 模块 Mix-in 后插入到 Podfile 类中。
Lockfile,顾名思义是用于记录最后一次 CocoaPods所安装的Pod依赖库版本的信息快照。也就是生成的 Podfile.lock。
在 pod install 过程,Podfile 会结合它来确认最终所安装的 Pod 版本,固定 Pod 依赖库版本防止其自动更新。Lockfile 也作为 Pods 状态清单(mainfest),用于记录安装过程的中哪些 Pod 需要被删除或安装或更新等。
Lockfile可获取的信息:
pod install 命令执行后的 verify_podfile_exists!中读取podfile:
def verify_podfile_exists!unless config.podfileraise Informative, "No `Podfile' found in the project directory."endend
Podfile文件的读取就是config.podfile里触发的,代码在 CocoaPods 的 config.rb 文件中:
def podfile_path_in_dir(dir)PODFILE_NAMES.each do |filename|candidate = dir + filenameif candidate.file?return candidateendendnilenddef podfile_path@podfile_path ||= podfile_path_in_dir(installation_root)enddef podfile@podfile ||= Podfile.from_file(podfile_path) if podfile_pathend
最后 Core 里的podfile.rb的from_file函数将依据目录下的 Podfile 文件类型选择调用 from_yaml 或者 from_ruby:
def self.from_file(path)path = Pathname.new(path)unless path.exist?raise Informative, "No Podfile exists at path `#{path}`."endcase path.extnamewhen '', '.podfile', '.rb'Podfile.from_ruby(path)when '.yaml'Podfile.from_yaml(path)elseraise Informative, "Unsupported Podfile format `#{path}`."endend

读取到文件之后我们需要对Podfile内容进行解析,这里主要看Core下面的podfile.rb文件下的from_ruby函数:
# podfile.rbdef self.from_ruby(path, contents = nil)contents ||= File.open(path, 'r:utf-8', &:read)...# 初始化生成Podfile对象,eval执行自己定义的dsl语句podfile = Podfile.new(path) do# rubocop:disable Lint/RescueExceptionbegin# rubocop:disable Security/Evaleval(contents, nil, path.to_s)# rubocop:enable Security/Evalrescue Exception => emessage = "Invalid `#{path.basename}` file: #{e.message}"raise DSLError.new(message, path, e, contents)end# rubocop:enable Lint/RescueExceptionendpodfileend
其中核心的一段代码是eval(contents, nil, path.to_s),它将Podfile中的文本内容转化为方法执行,也就是说里面的参数是一段Ruby 的代码字符串,通过eval方法可以直接执行。
YAML 格式的 Podfile 加载需要借助 YAMLHelper 类来完成,YAMLHelper 则是基于 yaml 的简单封装。
# podfile.rbdef self.from_yaml(path)string = File.open(path, 'r:utf-8', &:read)# 为了解决 Rubinius incomplete encoding in 1.9 mode# https://github.com/rubinius/rubinius/issues/1539if string.respond_to?(:encoding) && string.encoding.name != 'UTF-8'string.encode!('UTF-8')endhash = YAMLHelper.load_string(string)from_hash(hash, path)enddef self.from_hash(hash, path = nil)internal_hash = hash.duptarget_definitions = internal_hash.delete('target_definitions') || []podfile = Podfile.new(path, internal_hash)target_definitions.each do |definition_hash|definition = TargetDefinition.from_hash(definition_hash, podfile)podfile.root_target_definitions << definitionendpodfileend
通过from_yaml将文件内容转成Ruby hash后转入from_hash方法。
区别于from_ruby,这里调用的initialize将读取的hash直接存入internal_hash,然后利用TargetDefinition.from_hash 来完成的hash内容到targets的转换,因此,这里无需传入block进行DSL解析和方法转换。
Podfile的initialize方法:
def initialize(defined_in_file = nil, internal_hash = {}, &block)self.defined_in_file = defined_in_file@internal_hash = internal_hashif blockdefault_target_def = TargetDefinition.new('Pods', self)default_target_def.abstract = true@root_target_definitions = [default_target_def]@current_target_definition = default_target_definstance_eval(&block)else@root_target_definitions = []endend
它定义了三个参数:
Podfile 文件路径yaml 序列化得到的 Podfile 配置信息,保存在internal_hash中(Podfile From YAML使用)Podfile 的 DSL 配置(Podfile From Ruby使用)当 block 存在,会初始化名为Pods的TargetDefinition对象,用于保存Pods project的相关信息和 Pod 依赖。然后调用instance_eval执行传入的block,将 Podfile 的 DSL 内容转换成对应的方法和参数,最终将参数存入 internal_hash 和对应的 target_definitions 中。
Podfile 的内容最终保存在 internal_hash 和 target_definitions 中,本质上都是使用了 hash 来保存数据。由于 YAML 文件格式的 Podfile 加载后就是 hash 对象,无需过多加工。唯一需要处理的是递归调用 TargetDefinition 的 from_hash 方法来解析 target 子节点的数据。
因此,接下来的内容解析主要针对 Ruby 文件格式的 DSL 解析,我们以 pod 方法为例:
target 'Example' dopod 'Alamofire'end
当解析到 pod 'Alamofire' 时,会先通过 eval(contents, nil, path.to_s) 将其转换为 dsl.rb 中的方法:
def pod(name = nil, *requirements)unless nameraise StandardError, 'A dependency requires a name.'endcurrent_target_definition.store_pod(name, *requirements)end
name 为 Alamofire,由于我们没有指定对应的 Alamofire 版本,默认会使用最新版本。requirements 是控制 该 pod 来源获取或者 pod target 的编译选项等,例如:
pod 'Alamofire', '0.9'pod 'Alamofire', :modular_headers => truepod 'Alamofire', :configurations => ['Debug', 'Beta']pod 'Alamofire', :source => 'https://github.com/CocoaPods/Specs.git'pod 'Alamofire', :subspecs => ['Attribute', 'QuerySet']pod 'Alamofire', :testspecs => ['UnitTests', 'SomeOtherTests']pod 'Alamofire', :path => '~/Documents/AFNetworking'pod 'Alamofire', :podspec => 'https://example.com/Alamofire.podspec'pod 'Alamofire', :git => 'https://github.com/looseyi/Alamofire.git', :tag => '0.7.0'
对 name 进行校验后,直接转入 current_target_definition 毕竟 Pod 库都是存在 Pods.project 之下:
# target_definition.rbdef store_pod(name, *requirements)return if parse_subspecs(name, requirements) # This parse method must be called firstparse_inhibit_warnings(name, requirements)parse_modular_headers(name, requirements)parse_configuration_whitelist(name, requirements)parse_project_name(name, requirements)if requirements && !requirements.empty?pod = { name => requirements }elsepod = nameendget_hash_value('dependencies', []) << podnilenddef get_hash_value(key, base_value = nil)unless HASH_KEYS.include?(key)raise StandardError, "Unsupported hash key `#{key}`"endinternal_hash[key] = base_value if internal_hash[key].nil?internal_hash[key]enddef set_hash_value(key, value)unless HASH_KEYS.include?(key)raise StandardError, "Unsupported hash key `#{key}`"endinternal_hash[key] = valueend
经过一系列检查之后,调用 get_hash_value 获取 internal_hash 的 dependencies,并将 name 和 requirements 选项存入。
我们能看到当前的current_target_definition的internal_hash['dependencies']存的就是pod podname声明的所有依赖,是一个数组,数组里面的值如果有requirements则是Hash,没有则是pod名的字符串:

整个映射过程如下:

# source.rbdef specification(name, version)Specification.from_file(specification_path(name, version))end# 根据name和version读取到podspec的路径def specification_path(name, version)raise ArgumentError, 'No name' unless nameraise ArgumentError, 'No version' unless versionpath = pod_path(name) + version.to_sspecification_path = path + "#{name}.podspec.json"unless specification_path.exist?specification_path = path + "#{name}.podspec"endunless specification_path.exist?raise StandardError, "Unable to find the specification #{name} " \"(#{version}) in the #{self.name} source."endspecification_pathend
def self.from_file(path, subspec_name = nil)path = Pathname.new(path)unless path.exist?raise Informative, "No podspec exists at path `#{path}`."endstring = File.open(path, 'r:utf-8', &:read)# Work around for Rubinius incomplete encoding in 1.9 modeif string.respond_to?(:encoding) && string.encoding.name != 'UTF-8'string.encode!('UTF-8')endfrom_string(string, path, subspec_name)enddef self.from_string(spec_contents, path, subspec_name = nil)path = Pathname.new(path).expand_pathspec = nilcase path.extnamewhen '.podspec'Dir.chdir(path.parent.directory? ? path.parent : Dir.pwd) do# 直接通过eval执行podspec里的ruby语句,返回Specifiction对象spec = ::Pod._eval_podspec(spec_contents, path)unless spec.is_a?(Specification)raise Informative, "Invalid podspec file at path `#{path}`."endendwhen '.json'spec = Specification.from_json(spec_contents)elseraise Informative, "Unsupported specification format `#{path.extname}` for spec at `#{path}`."endspec.defined_in_file = pathspec.subspec_by_name(subspec_name, true)end
以AFNetworking为例,.podspec文件就是简单直接地声明了一个Specifiction对象,然后通过 block 块定制来完成配置:
Pod::Spec.new do |s|s.name = 'AFNetworking's.version = '2.7.2's.license = 'MIT's.summary = 'A delightful iOS and OS X networking framework.'s.homepage = 'https://github.com/AFNetworking/AFNetworking'...
类似于podfile的内容保存在internal_hash的hash里,像podspec的内容name、source_files 这些配置参数最终都会转换为方法调用并将值存入attributes_hash的hash中。
这些方法调用的实现方式分两种:
attribute 和 root_attribute 来动态添加的 setter 方法;subspec 、dependency 方法等
# lib/cocoapods-core/specification/dsl.rbmodule Podclass Specificationmodule DSLextend Pod::Specification::DSL::AttributeSupport# Deprecations must be required after include AttributeSupportrequire 'cocoapods-core/specification/dsl/deprecations'attribute :name,:required => true,:inherited => false,:multi_platform => falseroot_attribute :version,:required => true# ...endendend
可以看出 name和 version 的方法声明与普通的不太一样,其实 attribute 和 root_attribute 是通过 Ruby 的方法包装器来实现的,这些装饰器所声明的方法会在其模块被加载时动态生成,来看其实现:
# lib/cocoapods-core/specification/attribute_support.rbmodule Podclass Specificationmodule DSLclass << selfattr_reader :attributesendmodule AttributeSupportdef root_attribute(name, options = {})options[:root_only] = trueoptions[:multi_platform] = falsestore_attribute(name, options)enddef attribute(name, options = {})store_attribute(name, options)enddef store_attribute(name, options)attr = Attribute.new(name, options)@attributes ||= {}@attributes[name] = attrendendendendend
attribute 和 root_attribute 最终都走到了 store_attribute 保存在创建的 Attribute 对象内,并以配置的 Symbol名称作为 KEY 存入 @attributes,用于生成最终的 attributes setter 方法。
最关键的一步,让我们回到 specification 文件:
# /lib/coocapods-core/specificationmodule Podclass Specification# ...def store_attribute(name, value, platform_name = nil)name = name.to_svalue = Specification.convert_keys_to_string(value) if value.is_a?(Hash)value = value.strip_heredoc.strip if value.respond_to?(:strip_heredoc)if platform_nameplatform_name = platform_name.to_sattributes_hash[platform_name] ||= {}attributes_hash[platform_name][name] = valueelseattributes_hash[name] = valueendendDSL.attributes.values.each do |a|define_method(a.writer_name) do |value|store_attribute(a.name, value)endif a.writer_singular_formalias_method(a.writer_singular_form, a.writer_name)endendendend
Specification 类被加载时,会先遍历 DSL module 加载后所保存的 attributes,再通过 define_method 动态生成对应的配置方法。最终数据还是保存在 attributes_hash 中。
source、static_framework、module_name 等;source_files除了 attribute 装饰器声明的 setter 方法,还有几个自定义的方法是直接通过 eval 调用的:
# lib/cocoapods-core/specification/dsl.rb# 三种不同类型的 Subspec 经 eval 转换为对应的 Specification 对象,注意这里初始化后都将 parent 节点指向 self 同时存入 @subspecs 数组中,完成 SubSpec 依赖链的构造。def subspec(name, &block)subspec = Specification.new(self, name, &block)@subspecs << subspecsubspecenddef test_spec(name = 'Tests', &block)subspec = Specification.new(self, name, true, &block)@subspecs << subspecsubspecenddef app_spec(name = 'App', &block)appspec = Specification.new(self, name, :app_specification => true, &block)@subspecs << appspecappspecend# 对于其他 pod 依赖的添加我们通过 dependency 方法来实现def dependency(*args)name, *version_requirements = args# dependency args 有效性校验 ...attributes_hash['dependencies'] ||= {}attributes_hash['dependencies'][name] = version_requirementsunless whitelisted_configurations.nil?# configuration 白名单过滤和校验 ...attributes_hash['configuration_pod_whitelist'] ||= {}attributes_hash['configuration_pod_whitelist'][name] = whitelisted_configurationsendend
文件几乎完全一样摘抄自Podfile 的解析逻辑,这篇文章条理已经很清晰,本人只是为了个人查阅方便记录一下。