[关闭]
@qidiandasheng 2022-01-04T14:04:32.000000Z 字数 14692 阅读 683

Cocoapods核心组件:CocoaPods-Core

Cocoapods


介绍

CocoaPods-Core 用于CocoaPods中配置文件的解析,包括PodfilePodspec以及解析后的依赖锁存文件,如Podfile.lock等。

CocoaPods-Core 的文件构成

我们先通过入口文件 lib/cocoapods-core.rb 来一窥 Core 项目的主要文件:

  1. module Pod
  2. require 'cocoapods-core/gem_version'
  3. class PlainInformative < StandardError; end
  4. class Informative < PlainInformative; end
  5. require 'pathname'
  6. require 'cocoapods-core/vendor'
  7. # 用于存储 PodSpec 中的版本号
  8. autoload :Version, 'cocoapods-core/version'
  9. # pod 的版本限制
  10. autoload :Requirement, 'cocoapods-core/requirement'
  11. # 配置 Podfile 或 PodSpec 中的 pod 依赖
  12. autoload :Dependency, 'cocoapods-core/dependency'
  13. # 获取 Github 仓库信息
  14. autoload :GitHub, 'cocoapods-core/github'
  15. # 处理 HTTP 请求
  16. autoload :HTTP, 'cocoapods-core/http'
  17. # 记录最终 pod 的依赖信息
  18. autoload :Lockfile, 'cocoapods-core/lockfile'
  19. # 记录 SDK 的名称和 target 版本
  20. autoload :Platform, 'cocoapods-core/platform'
  21. # 对应 Podfile 文件的 class
  22. autoload :Podfile, 'cocoapods-core/podfile'
  23. # 管理 PodSpec 的集合
  24. autoload :Source, 'cocoapods-core/source'
  25. # 管理基于 CDN 来源的 PodSpec 集合
  26. autoload :CDNSource, 'cocoapods-core/cdn_source'
  27. # 管理基于 Trunk 来源的 PodSpec 集合
  28. autoload :TrunkSource, 'cocoapods-core/trunk_source'
  29. # 对应 PodSpec 文件的 class
  30. autoload :Specification, 'cocoapods-core/specification'
  31. # 将 pod 信息转为 .yml 文件,用于 lockfile 的序列化
  32. autoload :YAMLHelper, 'cocoapods-core/yaml_helper'
  33. # 记录 pod 依赖类型,是静态库/动态库
  34. autoload :BuildType, 'cocoapods-core/build_type'
  35. ...
  36. Spec = Specification
  37. end

将这些 Model 类按照对应的依赖关系进行划分,层级如下:

guardia1599993134206-3a7f8fdb-611d-44c1-a2c4-026365408d5e.jpeg-131.6kB

主要数据结构

TargetDefinition(target_definition.rb)

TargetDefinition 是一个多叉树结构,每个节点记录着 Podfile 中定义的 PodSource 来源、Build SettingPod 子依赖等。该树的根节点指向 Podfile,而 Podfile 中的 root_target_definitions 则记录着所有的 TargetDefinition 的根节点,正常情况下该 list 中只有一个 rootPods.project

为了便于阅读,简化了大量的 DSL 配置相关的方法和属性并对代码顺序做了调整,大致结构如下:

  1. module Pod
  2. class Podfile
  3. class TargetDefinition
  4. # 父节点: TargetDefinition 或者 Podfile
  5. attr_reader :parent
  6. # 子节点: TargetDefinition
  7. attr_reader :children
  8. # 记录 tareget 的配置信息
  9. attr_accessor :internal_hash
  10. def root?
  11. parent.is_a?(Podfile) || parent.nil?
  12. end
  13. def root
  14. if root?
  15. self
  16. else
  17. parent.root
  18. end
  19. end
  20. def podfile
  21. root.parent
  22. end
  23. # ...
  24. end
  25. end

在初始化Podfile对象时就会初始化根TargetDefinition

  1. # podfile.rb
  2. def initialize(defined_in_file = nil, internal_hash = {}, &block)
  3. self.defined_in_file = defined_in_file
  4. @internal_hash = internal_hash
  5. if block
  6. # TargetDefinition名为Pods,parent父节点为podfile
  7. default_target_def = TargetDefinition.new('Pods', self)
  8. default_target_def.abstract = true
  9. @root_target_definitions = [default_target_def]
  10. @current_target_definition = default_target_def
  11. instance_eval(&block)
  12. else
  13. @root_target_definitions = []
  14. end
  15. end

然后在Podfile文件里的每一个target声明都会调用dsl.rb里对应的target函数,初始化对应的子TargetDefinition

截屏2022-01-03 下午10.31.59.png-118.7kB

截屏2022-01-03 下午10.32.13.png-216.7kB

Specification(specification.rb)

Specification即存储PodSpec的内容,是用于描述一个Pod库的源代码和资源将如何被打包编译成链接库或framework

Podspec 支持的文件格式为 .podspec.json 两种,而 .podspec 本质是 Ruby 文件。

在数据结构上SpecificationTargetDefinition是类似的,同为多叉树结构。这里的parent是为 subspec保留的,用于指向其父节点的Spec。简化后的Spec的类如下:

  1. require 'active_support/core_ext/string/strip.rb'
  2. # 记录对应 platform 上 Spec 的其他 pod 依赖
  3. require 'cocoapods-core/specification/consumer'
  4. # 解析 DSL
  5. require 'cocoapods-core/specification/dsl'
  6. # 校验 Spec 的正确性,并抛出对应的错误和警告
  7. require 'cocoapods-core/specification/linter'
  8. # 用于解析 DSL 内容包含的配置信息
  9. require 'cocoapods-core/specification/root_attribute_accessors'
  10. # 记录一个 Pod 所有依赖的 Spec 来源信息
  11. require 'cocoapods-core/specification/set'
  12. # json 格式数据解析
  13. require 'cocoapods-core/specification/json'
  14. module Pod
  15. class Specification
  16. include Pod::Specification::DSL
  17. include Pod::Specification::DSL::Deprecations
  18. include Pod::Specification::RootAttributesAccessors
  19. include Pod::Specification::JSONSupport
  20. # `subspec` 的父节点
  21. attr_reader :parent
  22. # `Spec` 的唯一 id,由 name + version 的 hash 构成
  23. attr_reader :hash_value
  24. # 记录 `Spec` 的配置信息
  25. attr_accessor :attributes_hash
  26. # `Spec` 包含的 `subspec`
  27. attr_accessor :subspecs
  28. # 递归调用获取 Specification 的根节点
  29. def root
  30. parent ? parent.root : self
  31. end
  32. def hash
  33. if @hash_value.nil?
  34. @hash_value = (name.hash * 53) ^ version.hash
  35. end
  36. @hash_value
  37. end
  38. # ...
  39. end
  40. end

Specification 同样用 map attributes_hash 来记录配置信息。

Podfile(podfile.rb)

Podfile 是用于描述一个或多个 Xcode Project 中各个 Targets 之间的依赖关系。

这些 Targets 的依赖关系对应的就是 TargetDefinition 树中的各子节点的层级关系。如前面所说,有了 Podfile 这个根节点的指向,仅需对依赖树进行遍历,就能轻松获取完整的依赖关系。

有了这层依赖树,对于某个 Pod 库的更新即是对树节点的更新,便可轻松的分析出此次更新涉及的影响。

简化调整后的 Podfile 代码如下:

  1. require 'cocoapods-core/podfile/dsl'
  2. require 'cocoapods-core/podfile/target_definition'
  3. module Pod
  4. class Podfile
  5. include Pod::Podfile::DSL
  6. # podfile 路径
  7. attr_accessor :defined_in_file
  8. # 所有的 TargetDefinition 的根节点, 正常只有一个,即 Pods.project target
  9. attr_accessor :root_target_definitions
  10. # 记录 Pods.project 项目的配置信息
  11. attr_accessor :internal_hash
  12. # 当前 DSL 解析使用的 TargetDefinition
  13. attr_accessor :current_target_definition
  14. # ...
  15. end
  16. end

直接看 dsl.rb,该文件内部定义了 Podfile DSL 支持的所有方法。通过 include 的使用将 Pod::Podfile::DSL 模块 Mix-in 后插入到 Podfile 类中。

Lockfile(lockfile.rb)

Lockfile,顾名思义是用于记录最后一次 CocoaPods所安装的Pod依赖库版本的信息快照。也就是生成的 Podfile.lock

pod install 过程,Podfile 会结合它来确认最终所安装的 Pod 版本,固定 Pod 依赖库版本防止其自动更新。Lockfile 也作为 Pods 状态清单(mainfest),用于记录安装过程的中哪些 Pod 需要被删除或安装或更新等。

Lockfile可获取的信息:

Podfile 内容加载

podfile文件读取

pod install 命令执行后的 verify_podfile_exists!中读取podfile:

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

Podfile文件的读取就是config.podfile里触发的,代码在 CocoaPodsconfig.rb 文件中:

  1. def podfile_path_in_dir(dir)
  2. PODFILE_NAMES.each do |filename|
  3. candidate = dir + filename
  4. if candidate.file?
  5. return candidate
  6. end
  7. end
  8. nil
  9. end
  10. def podfile_path
  11. @podfile_path ||= podfile_path_in_dir(installation_root)
  12. end
  13. def podfile
  14. @podfile ||= Podfile.from_file(podfile_path) if podfile_path
  15. end

最后 Core 里的podfile.rbfrom_file函数将依据目录下的 Podfile 文件类型选择调用 from_yaml 或者 from_ruby

  1. def self.from_file(path)
  2. path = Pathname.new(path)
  3. unless path.exist?
  4. raise Informative, "No Podfile exists at path `#{path}`."
  5. end
  6. case path.extname
  7. when '', '.podfile', '.rb'
  8. Podfile.from_ruby(path)
  9. when '.yaml'
  10. Podfile.from_yaml(path)
  11. else
  12. raise Informative, "Unsupported Podfile format `#{path}`."
  13. end
  14. end

guardia1599993134203-782e510d-c084-4b57-98fe-970f5a38cc79.jpeg-144.7kB

Podfile From Ruby 解析

读取到文件之后我们需要对Podfile内容进行解析,这里主要看Core下面的podfile.rb文件下的from_ruby函数:

  1. # podfile.rb
  2. def self.from_ruby(path, contents = nil)
  3. contents ||= File.open(path, 'r:utf-8', &:read)
  4. ...
  5. # 初始化生成Podfile对象,eval执行自己定义的dsl语句
  6. podfile = Podfile.new(path) do
  7. # rubocop:disable Lint/RescueException
  8. begin
  9. # rubocop:disable Security/Eval
  10. eval(contents, nil, path.to_s)
  11. # rubocop:enable Security/Eval
  12. rescue Exception => e
  13. message = "Invalid `#{path.basename}` file: #{e.message}"
  14. raise DSLError.new(message, path, e, contents)
  15. end
  16. # rubocop:enable Lint/RescueException
  17. end
  18. podfile
  19. end

其中核心的一段代码是eval(contents, nil, path.to_s),它将Podfile中的文本内容转化为方法执行,也就是说里面的参数是一段Ruby 的代码字符串,通过eval方法可以直接执行。

Podfile From YAML 解析

YAML 格式的 Podfile 加载需要借助 YAMLHelper 类来完成,YAMLHelper 则是基于 yaml 的简单封装。

  1. # podfile.rb
  2. def self.from_yaml(path)
  3. string = File.open(path, 'r:utf-8', &:read)
  4. # 为了解决 Rubinius incomplete encoding in 1.9 mode
  5. # https://github.com/rubinius/rubinius/issues/1539
  6. if string.respond_to?(:encoding) && string.encoding.name != 'UTF-8'
  7. string.encode!('UTF-8')
  8. end
  9. hash = YAMLHelper.load_string(string)
  10. from_hash(hash, path)
  11. end
  12. def self.from_hash(hash, path = nil)
  13. internal_hash = hash.dup
  14. target_definitions = internal_hash.delete('target_definitions') || []
  15. podfile = Podfile.new(path, internal_hash)
  16. target_definitions.each do |definition_hash|
  17. definition = TargetDefinition.from_hash(definition_hash, podfile)
  18. podfile.root_target_definitions << definition
  19. end
  20. podfile
  21. end

通过from_yaml将文件内容转成Ruby hash后转入from_hash方法。

区别于from_ruby,这里调用的initialize将读取的hash直接存入internal_hash,然后利用TargetDefinition.from_hash 来完成的hash内容到targets的转换,因此,这里无需传入block进行DSL解析和方法转换。

初始化Podfile对象

Podfileinitialize方法:

  1. def initialize(defined_in_file = nil, internal_hash = {}, &block)
  2. self.defined_in_file = defined_in_file
  3. @internal_hash = internal_hash
  4. if block
  5. default_target_def = TargetDefinition.new('Pods', self)
  6. default_target_def.abstract = true
  7. @root_target_definitions = [default_target_def]
  8. @current_target_definition = default_target_def
  9. instance_eval(&block)
  10. else
  11. @root_target_definitions = []
  12. end
  13. end

它定义了三个参数:

block 存在,会初始化名为PodsTargetDefinition对象,用于保存Pods project的相关信息和 Pod 依赖。然后调用instance_eval执行传入的block,将 PodfileDSL 内容转换成对应的方法和参数,最终将参数存入 internal_hash 和对应的 target_definitions 中。

Podfile 内容解析

Podfile 的内容最终保存在 internal_hashtarget_definitions 中,本质上都是使用了 hash 来保存数据。由于 YAML 文件格式的 Podfile 加载后就是 hash 对象,无需过多加工。唯一需要处理的是递归调用 TargetDefinitionfrom_hash 方法来解析 target 子节点的数据。

因此,接下来的内容解析主要针对 Ruby 文件格式的 DSL 解析,我们以 pod 方法为例:

  1. target 'Example' do
  2. pod 'Alamofire'
  3. end

当解析到 pod 'Alamofire' 时,会先通过 eval(contents, nil, path.to_s) 将其转换为 dsl.rb 中的方法:

  1. def pod(name = nil, *requirements)
  2. unless name
  3. raise StandardError, 'A dependency requires a name.'
  4. end
  5. current_target_definition.store_pod(name, *requirements)
  6. end

name 为 Alamofire,由于我们没有指定对应的 Alamofire 版本,默认会使用最新版本。requirements 是控制 该 pod 来源获取或者 pod target 的编译选项等,例如:

  1. pod 'Alamofire', '0.9'
  2. pod 'Alamofire', :modular_headers => true
  3. pod 'Alamofire', :configurations => ['Debug', 'Beta']
  4. pod 'Alamofire', :source => 'https://github.com/CocoaPods/Specs.git'
  5. pod 'Alamofire', :subspecs => ['Attribute', 'QuerySet']
  6. pod 'Alamofire', :testspecs => ['UnitTests', 'SomeOtherTests']
  7. pod 'Alamofire', :path => '~/Documents/AFNetworking'
  8. pod 'Alamofire', :podspec => 'https://example.com/Alamofire.podspec'
  9. pod 'Alamofire', :git => 'https://github.com/looseyi/Alamofire.git', :tag => '0.7.0'

对 name 进行校验后,直接转入 current_target_definition 毕竟 Pod 库都是存在 Pods.project 之下:

  1. # target_definition.rb
  2. def store_pod(name, *requirements)
  3. return if parse_subspecs(name, requirements) # This parse method must be called first
  4. parse_inhibit_warnings(name, requirements)
  5. parse_modular_headers(name, requirements)
  6. parse_configuration_whitelist(name, requirements)
  7. parse_project_name(name, requirements)
  8. if requirements && !requirements.empty?
  9. pod = { name => requirements }
  10. else
  11. pod = name
  12. end
  13. get_hash_value('dependencies', []) << pod
  14. nil
  15. end
  16. def get_hash_value(key, base_value = nil)
  17. unless HASH_KEYS.include?(key)
  18. raise StandardError, "Unsupported hash key `#{key}`"
  19. end
  20. internal_hash[key] = base_value if internal_hash[key].nil?
  21. internal_hash[key]
  22. end
  23. def set_hash_value(key, value)
  24. unless HASH_KEYS.include?(key)
  25. raise StandardError, "Unsupported hash key `#{key}`"
  26. end
  27. internal_hash[key] = value
  28. end

经过一系列检查之后,调用 get_hash_value 获取 internal_hashdependencies,并将 namerequirements 选项存入。

我们能看到当前的current_target_definitioninternal_hash['dependencies']存的就是pod podname声明的所有依赖,是一个数组,数组里面的值如果有requirements则是Hash,没有则是pod名的字符串:

截屏2022-01-04 上午9.54.27.png-99.1kB

整个映射过程如下:

guardia1599993134247-591416bf-8bbb-46cb-a8b9-86c41721acde.jpeg-40kB

Podspec内容加载

Podspec文件读取

  1. # source.rb
  2. def specification(name, version)
  3. Specification.from_file(specification_path(name, version))
  4. end
  5. # 根据name和version读取到podspec的路径
  6. def specification_path(name, version)
  7. raise ArgumentError, 'No name' unless name
  8. raise ArgumentError, 'No version' unless version
  9. path = pod_path(name) + version.to_s
  10. specification_path = path + "#{name}.podspec.json"
  11. unless specification_path.exist?
  12. specification_path = path + "#{name}.podspec"
  13. end
  14. unless specification_path.exist?
  15. raise StandardError, "Unable to find the specification #{name} " \
  16. "(#{version}) in the #{self.name} source."
  17. end
  18. specification_path
  19. end

Podspec内容读取

  1. def self.from_file(path, subspec_name = nil)
  2. path = Pathname.new(path)
  3. unless path.exist?
  4. raise Informative, "No podspec exists at path `#{path}`."
  5. end
  6. string = File.open(path, 'r:utf-8', &:read)
  7. # Work around for Rubinius incomplete encoding in 1.9 mode
  8. if string.respond_to?(:encoding) && string.encoding.name != 'UTF-8'
  9. string.encode!('UTF-8')
  10. end
  11. from_string(string, path, subspec_name)
  12. end
  13. def self.from_string(spec_contents, path, subspec_name = nil)
  14. path = Pathname.new(path).expand_path
  15. spec = nil
  16. case path.extname
  17. when '.podspec'
  18. Dir.chdir(path.parent.directory? ? path.parent : Dir.pwd) do
  19. # 直接通过eval执行podspec里的ruby语句,返回Specifiction对象
  20. spec = ::Pod._eval_podspec(spec_contents, path)
  21. unless spec.is_a?(Specification)
  22. raise Informative, "Invalid podspec file at path `#{path}`."
  23. end
  24. end
  25. when '.json'
  26. spec = Specification.from_json(spec_contents)
  27. else
  28. raise Informative, "Unsupported specification format `#{path.extname}` for spec at `#{path}`."
  29. end
  30. spec.defined_in_file = path
  31. spec.subspec_by_name(subspec_name, true)
  32. end

AFNetworking为例,.podspec文件就是简单直接地声明了一个Specifiction对象,然后通过 block 块定制来完成配置:

  1. Pod::Spec.new do |s|
  2. s.name = 'AFNetworking'
  3. s.version = '2.7.2'
  4. s.license = 'MIT'
  5. s.summary = 'A delightful iOS and OS X networking framework.'
  6. s.homepage = 'https://github.com/AFNetworking/AFNetworking'
  7. .
  8. .
  9. .

Podspec内容解析

类似于podfile的内容保存在internal_hashhash里,像podspec的内容namesource_files 这些配置参数最终都会转换为方法调用并将值存入attributes_hashhash中。

这些方法调用的实现方式分两种:

方法包装器(attribute wrappter)

  1. # lib/cocoapods-core/specification/dsl.rb
  2. module Pod
  3. class Specification
  4. module DSL
  5. extend Pod::Specification::DSL::AttributeSupport
  6. # Deprecations must be required after include AttributeSupport
  7. require 'cocoapods-core/specification/dsl/deprecations'
  8. attribute :name,
  9. :required => true,
  10. :inherited => false,
  11. :multi_platform => false
  12. root_attribute :version,
  13. :required => true
  14. # ...
  15. end
  16. end
  17. end

可以看出 nameversion 的方法声明与普通的不太一样,其实 attributeroot_attribute 是通过 Ruby 的方法包装器来实现的,这些装饰器所声明的方法会在其模块被加载时动态生成,来看其实现:

  1. # lib/cocoapods-core/specification/attribute_support.rb
  2. module Pod
  3. class Specification
  4. module DSL
  5. class << self
  6. attr_reader :attributes
  7. end
  8. module AttributeSupport
  9. def root_attribute(name, options = {})
  10. options[:root_only] = true
  11. options[:multi_platform] = false
  12. store_attribute(name, options)
  13. end
  14. def attribute(name, options = {})
  15. store_attribute(name, options)
  16. end
  17. def store_attribute(name, options)
  18. attr = Attribute.new(name, options)
  19. @attributes ||= {}
  20. @attributes[name] = attr
  21. end
  22. end
  23. end
  24. end
  25. end

attributeroot_attribute 最终都走到了 store_attribute 保存在创建的 Attribute 对象内,并以配置的 Symbol名称作为 KEY 存入 @attributes,用于生成最终的 attributes setter 方法。

最关键的一步,让我们回到 specification 文件:

  1. # /lib/coocapods-core/specification
  2. module Pod
  3. class Specification
  4. # ...
  5. def store_attribute(name, value, platform_name = nil)
  6. name = name.to_s
  7. value = Specification.convert_keys_to_string(value) if value.is_a?(Hash)
  8. value = value.strip_heredoc.strip if value.respond_to?(:strip_heredoc)
  9. if platform_name
  10. platform_name = platform_name.to_s
  11. attributes_hash[platform_name] ||= {}
  12. attributes_hash[platform_name][name] = value
  13. else
  14. attributes_hash[name] = value
  15. end
  16. end
  17. DSL.attributes.values.each do |a|
  18. define_method(a.writer_name) do |value|
  19. store_attribute(a.name, value)
  20. end
  21. if a.writer_singular_form
  22. alias_method(a.writer_singular_form, a.writer_name)
  23. end
  24. end
  25. end
  26. end

Specification 类被加载时,会先遍历 DSL module 加载后所保存的 attributes,再通过 define_method 动态生成对应的配置方法。最终数据还是保存在 attributes_hash 中。

方法声明

除了 attribute 装饰器声明的 setter 方法,还有几个自定义的方法是直接通过 eval 调用的:

  1. # lib/cocoapods-core/specification/dsl.rb
  2. # 三种不同类型的 Subspec 经 eval 转换为对应的 Specification 对象,注意这里初始化后都将 parent 节点指向 self 同时存入 @subspecs 数组中,完成 SubSpec 依赖链的构造。
  3. def subspec(name, &block)
  4. subspec = Specification.new(self, name, &block)
  5. @subspecs << subspec
  6. subspec
  7. end
  8. def test_spec(name = 'Tests', &block)
  9. subspec = Specification.new(self, name, true, &block)
  10. @subspecs << subspec
  11. subspec
  12. end
  13. def app_spec(name = 'App', &block)
  14. appspec = Specification.new(self, name, :app_specification => true, &block)
  15. @subspecs << appspec
  16. appspec
  17. end
  18. # 对于其他 pod 依赖的添加我们通过 dependency 方法来实现
  19. def dependency(*args)
  20. name, *version_requirements = args
  21. # dependency args 有效性校验 ...
  22. attributes_hash['dependencies'] ||= {}
  23. attributes_hash['dependencies'][name] = version_requirements
  24. unless whitelisted_configurations.nil?
  25. # configuration 白名单过滤和校验 ...
  26. attributes_hash['configuration_pod_whitelist'] ||= {}
  27. attributes_hash['configuration_pod_whitelist'][name] = whitelisted_configurations
  28. end
  29. end

参考

文件几乎完全一样摘抄自Podfile 的解析逻辑,这篇文章条理已经很清晰,本人只是为了个人查阅方便记录一下。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注