深入了解iOS依赖管理器CocoaPods

3,111 阅读12分钟

前言

8月14日,CocoaPods 官方发布了一则公告,宣布这个已经陪伴我们多年的工具将进入维护模式。

CocoaPods 的未来维护计划总结如下:

短期计划:

  1. 处理系统性安全问题。
  2. 每年至少发布两次更新,以跟上 Xcode 的更新节奏。
  3. 每六个月查看一次 trunk 支持请求。
  4. 确保网站基础设施保持稳定。
  5. 对贡献者开放,接受 PR 提交。

不再执行的事项:

  1. 不再积极处理 GitHub 上的个人支持请求。
  2. 不计划积极开发新功能。
  3. 不保证处理添加新功能或应用级别错误的 PR。

长期计划:

  1. 讨论将 Specs 仓库转换为只读状态,以简化 trunk 的安全性。

  2. Specs 仓库和 CDN 基础设施将在 GitHub 和 jsDelivr 存在的情况下持续运行,确保现有构建不会受影响。

  3. Specs 仓库的只读时间节点尚未确定,预计将在几年后实施,开发者将有足够时间适应

前面我们介绍了CocoaPods官方的一条重要公告,下面我们正式进入今天主题,本文主要从当前主流的包管理器来分析和对比CocoaPods的优势和劣势,然后从源码入手来探究CocoaPods的核心原理,最后介绍一下基于CocoaPods的原理,在货拉拉落地的几个应用案例

在iOS中常用的包管理器是:

1. CocoaPods

*   **描述**:CocoaPods 是 iOS 和 macOS 平台上最流行的依赖管理工具,用于管理项目中的第三方库和工具。

*   **优点**:

    *   社区支持广泛,拥有大量的开源库和插件。
    *   集成简单,通过简单的配置文件 (Podfile) 就可以管理依赖。
    *   支持版本控制,可以指定具体的库版本,支持commit依赖,分支依赖等。

*   **使用示例**:Podfile

<!---->

    use_frameworks!
    source "ssh://git@gitlab.xxx.cn:56358/group-wp-sdk/hd-wp-specs-ios.git"
    source "https://cdn.cocoapods.org"
    platform :ios, '12.0'
    target 'HLLConfig_Example' do
      pod 'XXX', :path => '../'
      pod 'Masonry'
      pod 'HWPFoundation'
    end

2. Swift Package Manager (SPM)

*   **描述**:SPM 是苹果官方推出的用于 Swift 项目的包管理工具,支持在 macOSiOS 平台上管理 Swift 包。

*   **优点**:

    *   官方支持,与 Swift 编程语言紧密集成。
    *   支持 Swift 包的依赖管理和版本控制。
    *   可以将包的依赖关系直接写入 Swift 包描述文件 (Package.swift)。
    *   去中心化,不集中维护依赖索引

*   **使用示例**:

<!---->

    swift
    // 在 Package.swift 文件中添加依赖import PackageDescription

    let package = Package(
      name: "YourPackage",
      dependencies: [
        .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.0.0")
      ],
      targets: [
        .target(name: "YourTarget", dependencies: ["Alamofire"])
      ]
    )

选择适合的包管理器

中心化

# 中心化# 去中心化
  • CocoaPods 适合于需要在 iOS 和 macOS 应用中集成大量第三方库的情况,特别是那些已经通过 CocoaPods 管理的流行库,目前OC的项目还是比较建议使用CocoaPods。

  • Swift Package Manager (SPM) 更适合于纯 Swift 项目,尤其是当你需要管理和分发自己编写的 Swift 模块时。

探究CocoaPods原理

1. CocoaPods源码调试-VSCode

1. 配置Gemfile执行bundle install

source 'https://rubygems.org'
// 下载的CocoaPods代码
gem 'cocoapods',path: './cocoapods' 
gem 'debase', '0.2.5.beta2'
gem "ruby-lsp", require: false
gem 'rexml'
// 自定义插件
gem 'cocoapods-lucifer' ,path: './cocoapods-lucifer'
2. 配置调试文件launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "lucifer",
            "type": "ruby_lsp",
            "request": "launch",
            "program": "/Users/xxxx/cocoapods-lucifer/cocoapods/bin/pod install --project-directory=/Users/admin/hll-cdriver-ios/Example",
            "env": {
                "LANG": "en_US.UTF-8",
                "LUCIFER_OPEN": "true",
                "https_proxy": "http://127.0.0.1:7890",
                "http_proxy": "http://127.0.0.1:7890",
                "all_proxy": "socks5://127.0.0.1:7890"
            }
        }
    ]
}

2. CocoaPods源码分析

CLAideCocoaPods 使用的命令行接口(CLI)框架。它提供了命令行工具的基础功能,包括解析命令、处理输入参数、生成帮助文档等。
CocoaPods-Core功能: CocoaPods 的核心库,负责处理 Podspec 文件和依赖管理的核心逻辑。用途: 解析和处理 Podspec、执行依赖解析、生成目标配置等。
CocoaPods-Downloader功能: 用于下载 Pods 的库,支持从 Git、HTTP、Mercurial 等不同来源下载代码。用途: 管理 Pods 的下载过程,确保依赖包可以被正确获取和缓存。
Molinillo功能: 通用的依赖解析器,用于解决依赖关系中的版本冲突。用途: 在 CocoaPods 中用于解析 Pods 的依赖关系,确保各依赖项之间的版本兼容性。
Xcodeproj功能: 处理 Xcode 项目文件 (.xcodeproj) 的库。用途: 允许 CocoaPods 读取、修改和生成 Xcode 项目文件,从而在项目中集成 Pods。
CocoaPods-Plugin功能: CocoaPods 的插件系统,允许开发者扩展 CocoaPods 的功能。用途: 开发者可以通过编写插件,增加或自定义 CocoaPods 的行为和功能。
1. CLAide 命令解析
# command.rb
module CLAide
  class Command
    class << self
      # 命令是否为抽象命令(非叶子命令)
      attr_accessor :abstract_command
      # 为抽象命令进行代理的命令
      attr_accessor :default_subcommand
      # 命令功能摘要
      attr_accessor :summary
      # 命令功能描述
      attr_accessor :description

      # 命令的参数
      def arguments
        @arguments ||= []
      end
      
      # 命令的名字
      def command
        @command ||= name.split('::').last.gsub(/[A-Z]+[a-z]*/) do |part|
          part.downcase << '-'
        end[0..-2]
      end
      
      # 命令的选项(包括标志、选项)
      def self.options
        if root_command?
          DEFAULT_ROOT_OPTIONS + DEFAULT_OPTIONS
        else
          DEFAULT_OPTIONS
        end
      end
      
      DEFAULT_ROOT_OPTIONS = [
        ['--version', 'Show the version of the tool'],
      ]

      DEFAULT_OPTIONS = [
        ['--verbose', 'Show more debugging information'],
        ['--no-ansi', 'Show output without ANSI codes'],
        ['--help',    'Show help banner of specified command'],
      ]
    end
  end
end

命令解析过程

示例:pod update Alamofire --no-repo-update --exclude-pods=RxSwift

下面我们看看CLAide是如何把我们的命令一步步转化为最终的执行流程的

image.png

2. 功能解析

image.png

3. pod install 详细流程

流程步骤说明
Prepare读取 Podfile 和 Podfile.lock,解析项目配置,识别需要安装或更新的依赖项。
Resolve Dependencies解析依赖关系,确定各依赖库的具体版本,确保依赖间兼容,不引入不兼容的库版本。
Download Dependencies下载所需的依赖库(本地缓存或远程服务器),获取必要的源文件。
Validate Targets验证各目标的配置,确保依赖设置与项目平台、编译环境等兼容,不符合的会发出警告。
Integrate将依赖库集成到 Xcode 项目,生成 Pods 项目文件,更新 .xcconfig 和 xcworkspace。
Write Locks将解析到的依赖版本写入 Podfile.lock,记录当前安装版本,确保后续安装一致性。
Post Install执行 post_install 钩子(如定义在 Podfile 中),允许用户在安装后执行自定义脚本。
4. pod install ,pod install --repo-update, pod update的区别
pod install || pod install --repo-update

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

pod update

def run
    verify_podfile_exists!

    installer = installer_for_config
    installer.repo_update = repo_update?(:default => true)
    installer.clean_install = @clean_install
    if @pods.any? || @excluded_pods.any? || @source_pods.any?
      verify_lockfile_exists!
      verify_pods_are_installed!
      verify_excluded_pods_are_installed!

      @pods += @source_pods.select { |pod| config.lockfile.pod_names.include?(pod) }
      @pods = config.lockfile.pod_names.dup if @pods.empty?
      @pods -= @excluded_pods

      installer.update = { :pods => @pods }
    else
      UI.puts 'Update all pods'.yellow
      installer.update = true
    end
    installer.install!
end

pod install: 如果 Podfile.lock 存在,pod install 会根据 Podfile.lock 中的版本来安装依赖,以确保与上次安装时的版本一致,确保依赖的一致性。如果 Podfile.lock 不存在,它会根据 Podfile 文件中的依赖进行安装,并生成 Podfile.lock 文件来记录此次安装的依赖版本。

pod install --repo-update: 在执行 pod install 时,添加 --repo-update 选项会首先强制更新本地的 CocoaPods 规格仓库(spec repo)。更新完成后,它会重新解析依赖关系并安装依赖项。这可能会导致依赖版本发生变化,即使 Podfile.lock 已经存在。

pod update: pod update 会忽略 Podfile.lock 中的现有版本约束,根据 Podfile 中的要求更新指定的依赖项到最新版本,并更新 Podfile.lock 文件以记录新的版本。这样确保了所有依赖项都是最新的,且 Podfile.lock 中的内容也相应更新。

5. 依赖仲裁算法(Molinillo)

pod 'xxxx' : 后方没有指定版本,则表示使用仓库的最新版本。

pod 'xxxx', '2.3' : 使用xxxx仓库的2.3版本。

pod 'xxxx', '>2.3': 则表示使用的版本范围是 2.3 <= 版本 < 3.0。如果后方指定版本是>2.3.1, 那么则表示使用的版本范围是 2.3.1 <= 版本 < 2.4.0。

pod 'xxxx', '>2.3': 使用大于2.3的版本。

pod 'xxxx', '>=2.3': 使用2.3及以上的版本。

pod 'xxxx', '<2.3': 使用小于2.3的版本。

pod 'xxxx', '<=2.3': 使用小于等于2.3的版本

1. 收集根依赖

从用户提供的文件(如 Podfile)中,提取项目的根依赖项。

2. 构建依赖图
  • 递归解析: 对每个根依赖进行递归解析,找出它们依赖的所有其他库,形成一个完整的依赖图。
  • 节点和边: 在依赖图中,库(或模块)作为节点,依赖关系作为边。
image.pngimage.png
3. 生成候选版本
  • 版本筛选: 根据版本约束条件,筛选出每个依赖项的候选版本。

  • 排序: 将候选版本按从新到旧的顺序排序,优先考虑最新的兼容版本。

4. 冲突检测与回溯
  • 遍历依赖图: 从根节点开始,逐步选择兼容的版本进行检测。
  • 检测冲突: 检查当前选择的版本是否与已经选定的其他依赖版本产生冲突。
  • 回溯处理: 如果检测到冲突,回溯到之前的选择,尝试不同的版本组合,直到找到无冲突的解决方案。

假设有以下的 Podfile 配置:

image.png

Podfile
pod 'A', '~> 1.0'
pod 'B', '~> 2.0'

Pod A 的依赖:

Pod A 1.0.0 无依赖

Pod A 1.1.0 依赖于 Pod B 2.0.5

Pod A 2.0.0 依赖于 Pod B 2.1.0

   +-------------------------+
   |     初始化解析器         |
   +-------------------------+
               |
               v
   +-------------------------+
   | 解析 Pod A 的版本         |
   | - 候选版本:1.0.0, 1.1.0  |
   | - 选择 1.1.0             |
   +-------------------------+
               |
               v
   +-------------------------+
   | 解析 Pod B 的版本         |
   | - 候选版本:2.0.0, 2.0.5  |
   | - A 1.1.0 依赖 B 2.0.5   |
   | - 选择 B 2.0.5           |
   +-------------------------+
               |
               v
   +-------------------------+
   |     冲突检测             |
   | - 假设其他SDK指定 B 2.0.0 |
   | - 冲突:B 2.0.5 不符合    |
   +-------------------------+
               |
               v
   +-------------------------+
   |        回溯             |
   | - 重新选择 A 1.0.0       |
   | - A 1.0.0 无依赖关系     |
   +-------------------------+
               |
               v
   +-------------------------+
   |  重新解析 Pod B 的版本    |
   | - 选择 B 2.0.0           |
   +-------------------------+
               |
               v
   +-------------------------+
   |   生成最终解决方案         |
   | - A 1.0.0               |
   | - B 2.0.0               |
   +-------------------------+
5. 解决依赖
  • 成功解析: 如果找到一个无冲突的版本组合,则依赖解析成功。

  • 失败: 如果无法找到无冲突的组合,则解析失败,并报告错误。

6. 版本规范(x.y.z)

主版本号 x: 仅在有重大且不兼容的变更时更新,升级到新主版本号可能需要对现有代码进行修改和测试。

次版本号 y : 用于添加新功能或特性,保持向后兼容性,可以较为安全地升级。

修订版本号 z: 用于错误修复和小幅改进,确保稳定性和安全性,通常可以无风险地升级

3. CocoaPods的AOP的实现

1. CocoaPods提供的hook点
hook简介用途
pre_install在 Pod 安装开始之前修改 Podfile 或执行准备工作
pre_integrate在 Pods 集成到 Xcode 项目之前自定义 Pods 项目设置或执行额外操作
post_install在 Pod 安装完成之后调整 Xcode 项目设置或进行清理
post_integrate在 Pods 集成到 Xcode 项目之后最终调整 Xcode 项目或执行其他集成后的操作
source_provider在处理源代码库时定义或修改 Pod 源代码库的提供方式
2. 如何实现自定义hook

当CocoaPods提供的hook点无法满足我们的诉求的时候,我们可以通过以下代码来实现自定义hook,下述hook可以实现hook到CocoaPods的任意方法

require "cocoapods-lucifer/gem_version"
require 'set'
module Pod
  class Installer
    # hook download
    old_download_source = instance_method(:download_source_of_pod)
    define_method(:download_source_of_pod) do | pod_name |
      if ENV["LUCIFER_OPEN"] == "true"
        start_time = Time.now
        old_download_source.bind(self).(pod_name)
        end_time = Time.now
        resolve_duration = (end_time - start_time).round(2)
        puts "\e[32m #{pod_name} download time: #{resolve_duration} 秒\e[0m"
      else
        old_download_source.bind(self).(pod_name)
      end
    end
  end
end

CocoaPods在货拉拉应用实践

熟悉CocoaPods的源码对于解决工程的问题有着重大的意义,同时CocoaPods的插件开发也会经常被用于iOS CICD中用于采集,分析和解决一些潜在的问题和风险,下面我们就重点讲述一下CocoaPods在货拉拉的一些应用案例,重点介绍的是iOS 二进制化在货拉拉的落地。

1. cocoapods-lucifer依赖分析,版本合规分析,耗时分析(CICD依赖分析工具)

2. iOS SDK二进制化

image.png

区别于网上工程的二进制化,我们以sdk为基本编译单元,将每个sdk编译成独立的二进制文件,使用起来更加的灵活,这样也解决了关于源码及二进制的切换问题,以及关于podspec的管理问题

以AFNetworking(4.0.1)为例 ,我们的源码版本及二进制版本分别如下

源码版本framework版本xcframework版本
版本4.0.14.0.1.1-binary4.0.1.1-xcbinary
使用pod 'AFNetworking', '4.0.1'pod 'AFNetworking', '4.0.1.1-binary'pod 'AFNetworking', '4.0.1.1-xcbinary'

WHY :为什么要采用版本的方式来区分源码版本和二进制版本?

  1. 参考了CocoaPods的版本校验规则,假如我们使用4.0.1-binary则会破坏x.y.z的规范,并且无法通过CocoaPods的规则校验
  2. 使用版本来区分可以帮助我们更加灵活的去切换和管理源码和二进制,不需要再去开发额外的插件去支持,这样可以做到对业务工程无侵入
  3. 使用这种方式也简化了podspec的管理方式,我们可以将对应的二进制的podspec push 到任意的repo上,也解决了网上关于多repo还是单repo的问题

iOS SDK二进制化插件 hll-cocoapods-podspec-binary

基于我们在货拉拉的落地实践,我们开源了我们二进制的部分能力,可以通过sdk的podspec来生成对应的framework,xcframework及对应的podspec,详情请参考:github.com/HuolalaTech…

Usage

$ pod mbuild [NAME] [VERSION]
Options:
    --framework     Used to generate the framework
    --xcframework   Used to generate the xcframework
    --sources       The sources from which to pull dependent pods
Example:
$ pod mbuild AFNetworking 4.0.1

我们不支持的

  1. 我们支持了framework,xcframework的格式,但是并没有支持.a的格式

  2. 二进化我们并不支持subspec的管理,单个sdk我们只会构建出一个framework

  3. 目前我们二进制插件只开放了静态库的打包,因为动态库和静态库混用会存在一些问题,在CocoaPods的源码中也对这一块的依赖做了一些限制

总结

虽然CocoaPods已经进入了维护阶段,但是不可否认在最近十多年的时间对于iOS开发者的贡献,在此我们也向CocoaPods开发及维护者们致敬,并且在未来的时间我们依旧无法离开CocoaPods,掌握其原理可以更好的帮我们在日常的开发工作中应对更多的不确定性,也能够帮我们解决日常开发中的一些问题。