6. PodSpec 管理策略

本文目錄

PodSpec 文件管理

引子

本文是 Core 的最後一篇,它與另外兩篇文章「Podfile 解析邏輯」和「PodSpec 文件分析」共同支撐起 CocoaPods 世界的骨架。CocoaPods-Core 這個庫之因此被命名爲 Core 就是由於它包含了 Podfile -> Spec Repo -> PodSpec 這條完整的鏈路,將散落各地的依賴庫鏈接起來並基於此骨架不斷地完善功能。從提供各類便利的命令行工具,到依賴庫與主項目的自動集成,再到提供多樣的 Xcode 編譯配置、單元測試、資源管理等等,最終造成了咱們所見的 CocoaPods。html

今天咱們就來聊聊 Spec Repo 這個 PodSpec 的聚合倉庫以及它的演變與問題。前端

Source

做爲 PodSpec 的聚合倉庫,Spec Repo 記錄着全部 pod 所發佈的不一樣版本的 PodSpec 文件。該倉庫對應到 Core 的數據結構爲 Source,即爲今天的主角。git

整個 Source 的結構比較簡單,它基本是圍繞着 Git 來作文章,主要是對 PodSpec 文件進行各類查找更新操做。結構以下:github

# 用於檢查 spec 是否符合當前 Source 要求
require 'cocoapods-core/source/acceptor'
# 記錄本地 source 的集合
require 'cocoapods-core/source/aggregate'
# 用於校驗 source 的錯誤和警告
require 'cocoapods-core/source/health_reporter'
# source 管理器
require 'cocoapods-core/source/manager'
# source 元數據
require 'cocoapods-core/source/metadata'

module Pod
  class Source
    # 倉庫默認的 Git 分支
    DEFAULT_SPECS_BRANCH = 'master'.freeze
    # 記錄倉庫的元數據
    attr_reader :metadata
    # 記錄倉庫的本地地址
    attr_reader :repo
    # repo 倉庫地址 ~/.cocoapods/repos/{repo_name}
    def initialize(repo)
      @repo = Pathname(repo).expand_path
      @versions_by_name = {}
      refresh_metadata
    end
    # 讀取 Git 倉庫中的 remote url 或 .git 目錄
    def url
      @url ||= begin
        remote = repo_git(%w(config --get remote.origin.url))
        if !remote.empty?
          remote
        elsif (repo + '.git').exist?
          "file://#{repo}/.git"
        end
      end
    end

    def type
      git? ? 'git' : 'file system'
    end
    # ...
  end
end
複製代碼

Source 還有兩個子類 CDNSourceTrunkSource,TrunkSouce 是 CocoaPods 的默認倉庫。在版本 1.7.2 以前 Master Repo 的 URL 指向爲 Github 的 Specs 倉庫,這也是形成咱們每次 pod installpod update 慢的緣由之一。它不只保存了近 10 年來 PodSpec 文件同時還包括 Git 記錄,再加上牆的緣由,每次更新都很是痛苦。而在 1.7.2 以後 CocoaPods 的默認 Source 終於改成了 CDN 指向,同時支持按需下載,緩解了 pod 更新和磁盤佔用過大問題。shell

Source 的依賴關係以下:npm

01-Source

回到 Source 來看其如何初始化的,能夠看到其構造函數 #initialize(repo) 將傳入的 repo 地址保存後,直接調用了 #refresh_metadata 來完成元數據的加載:json

def refresh_metadata
  @metadata = Metadata.from_file(metadata_path)
end

def metadata_path
  repo + 'CocoaPods-version.yml'
end
複製代碼

Metadata

Metadata 是保存在 repo 目錄下,名爲 CocoaPods-version.yml 的文件,用於記錄該 Source 所支持的 CocoaPods 的版本以及倉庫的分片規則數組

autoload :Digest, 'digest/md5'
require 'active_support/hash_with_indifferent_access'
require 'active_support/core_ext/hash/indifferent_access'

module Pod
  class Source
    class Metadata
      # 最低可支持的 CocoaPods 版本,對應字段 `min`
      attr_reader :minimum_cocoapods_version
      # 最高可支持的 CocoaPods 版本,對應字段 `max`
      attr_reader :maximum_cocoapods_version
      # 最新 CocoaPods 版本,對應字段 `last`
      attr_reader :latest_cocoapods_version
      # 規定截取的關鍵字段的前綴長度和數量
      attr_reader :prefix_lengths
      # 可兼容的 CocoaPods 最新版本
      attr_reader :last_compatible_versions
      # ...
    end
  end
end
複製代碼

這裏以筆者 💻 環境中 Master 倉庫下的 CocoaPods-version.yml 文件內容爲例:瀏覽器

---
min: 1.0.0
last: 1.10.0.beta.1
prefix_lengths:
- 1
- 1
- 1
複製代碼

最低支持版本爲 1.0.0,最新可用版本爲 1.10.0.beta.1,以及最後這個 prefix_lengths[1, 1, 1] 的數組。那麼這個 prefix_lengths 的做用是什麼呢 ?緩存

要回答這個問題,咱們先來看一張 Spec Repo 的目錄結構圖:

02-trunk-folder

再 🤔 另一個問題,爲何 CocoaPods 生成的目錄結構是這樣 ?

其實在 2016 年 CocoaPods Spec 倉庫下的全部文件都在同級目錄,不像如今這樣作了分片。這個是爲了解決當時用戶的吐槽:Github 下載慢,最終解決方案的結果就如你所見:將 Git 倉庫進行了分片

那麼問題來了,爲何分片可以提高 Github 下載速度?

很重要的一點是 CocoaPods 的 Spec Repo 本質上是 Git 倉庫,而 Git 在作變動管理的時候,會記錄目錄的變動,每一個子目錄都會對應一個 Git model。而當目錄中的文件數量過多的時候,Git 要找出對應的變動就變得十分困難。有興趣的同窗能夠查看官方說明

另外再補充一點,在 Linux 中最經典的一句話是:「一切皆文件」,不只普通的文件和目錄,就連塊設備、管道、socket 等,也都是統一交給文件系統管理的。也就是說就算不用 Git 來管理 Specs 倉庫,當目錄下存在數以萬計的文件時,如何高效查找目標文件也是須要考慮的問題。

Tips:關於文件系統層次結構有興趣的同窗能夠查看FHS 標準,以及知乎這篇:傳送門

回到 CocoaPods,如何對 Master 倉庫目錄進行分片就涉及到 Metadata 類中的關鍵方法:

def path_fragment(pod_name, version = nil)
  prefixes = if prefix_lengths.empty?
               []
             else
               hashed = Digest::MD5.hexdigest(pod_name)
               prefix_lengths.map do |length|
                 hashed.slice!(0, length)
               end
             end
  prefixes.concat([pod_name, version]).compact
end
複製代碼

#path_fragment 會依據 pod_name 和 version 來生成 pod 對應的索引目錄:

  1. 首先對 pod_name 進行 MD5 計算獲取摘要;
  2. 遍歷 prefix_lengths 對生成的摘要不斷截取指定的長度做爲文件索引。

AFNetworking 爲例:

$ Digest::MD5.hexdigest('AFNetworking')
"a75d452377f3996bdc4b623a5df25820"
複製代碼

因爲咱們的 prefix_lengths[1, 1, 1] 數組,那麼它將會從左到右依次截取出一個字母,即: a75 ,這三個字母做爲索引目錄,它正好符合咱們 👆 目錄結構圖中 AFNetworking 的所在位置。

Versions

要找到 Podfile 中限定版本號範圍的 PodSpec 文件還須要須要最後一步,獲取當前已發佈的 Versions 列表,並經過比較 Version 得出最終所需的 PodSpec 文件。

在上一步已經過 metadatapod_name 計算出 pod 所在目錄,接着就是找到 pod 目錄下的 Versions 列表:

03-versons-folder

獲取 Versions:

def versions(name)
  return nil unless specs_dir
  raise ArgumentError, 'No name' unless name
  pod_dir = pod_path(name)
  return unless pod_dir.exist?
  @versions_by_name[name] ||= pod_dir.children.map do |v|
    basename = v.basename.to_s
    begin
      Version.new(basename) if v.directory? && basename[0, 1] != '.'
    rescue ArgumentError
    raise Informative, 'An unexpected version directory ...'
    end
  end.compact.sort.reverse
end
複製代碼

該方法重點在於將 pod_dir 下的每一個目錄都轉換成爲了 Version 類型,並在最後進行了 sort 排序。

#versions 方法主要在 pod search 命令中被調用,後續會介紹。

來摟一眼 Version 類:

class Version < Pod::Vendor::Gem::Version
  METADATA_PATTERN = '(\+[0-9a-zA-Z\-\.]+)'
  VERSION_PATTERN = "[0-9]+(\\.[0-9a-zA-Z\\-]+)*#{METADATA_PATTERN}?"
  # ...
end
複製代碼

該 Version 繼承於 Gem::Version 並對其進行了擴展,實現了語義化版本號的標準,sort 排序也是基於語義化的版原本比較的,這裏咱們稍微展開一下。

Semantic Versioning

語義化版本號(Semantic Versioning 簡稱:SemVer)絕對是依賴管理工具繞不開的坎。語義化的版本就是讓版本號更具語義化,能夠傳達出關於軟件自己的一些重要信息而不僅是簡單的一串數字。 咱們每次對 Pod 依賴進行更新,最後最重要的一步就是更新正確的版本號,一旦發佈出去,再要更改就比較麻煩了。

SemVer 是由 Tom Preston-Werner 發起的一個關於軟件版本號的命名規範,該做者爲 Gravatars 創辦者同時也是 GitHub 聯合創始人。

那什麼是語義化版本號有什麼特別呢 ?咱們以 AFNetworking 的 release tag 示例:

3.0.0
3.0.0-beta.1
3.0.0-beta.2
3.0.0-beta.3
3.0.1
複製代碼

這些 tags 並不是隨意遞增的,它們背後正是遵循了語義化版本的標準。

基本規則

  • 軟件的版本一般由三位組成,如:X.Y.Z。
  • 版本是嚴格遞增的,
  • 在發佈重要版本時,能夠發佈 alpha, rc 等先行版本,
  • alpha 和 rc 等修飾版本的關鍵字後面能夠帶上次數和 meta 信息,

版本格式:

主版本號.次版本號.修訂號

版本號遞增規則以下:

| Code status | Stage                  | Example version |
| ------------------ | ---------------------- | --------------- |
| 新品首發 |1.0.0 開始           | 1.0.0 |
| 向後兼容的 BugFix | 增長補丁號 Z             | 1.0.1 |
| 向後兼容的 Feature | 增長次版本號 Y           | 1.1.0 |
| 向後不兼容的改動 | 增長主版本號 X           | 2.0.0 |
| 重要版本的預覽版 | 補丁號後添加 alpha, rc   | 2.1.0-rc.0 |
複製代碼

關於 CocoaPods 的 Version 使用描述,傳送門

CDNSource

CocoaPods 在 1.7.2 版本正式將 Master 倉庫託管到 Netlify 的 CDN 上,當時關於如何支持這一特性的文章和說明鋪天蓋地,這裏仍是推薦你們看官方說明。另外,當時感覺是彷佛國內的部分 iOS 同窗都炸了,各類標題黨:什麼最完美的升級等等。

因此這裏明確一下,對於 CocoaPods 的 Master 倉庫支持了 CDN 的行爲,僅解決了兩個問題:

  1. 利用 CDN 節點的全球化部署解決內容分發慢,提升 Specs 資源的下載速度。
  2. 經過 Specs 按需下載擺脫了原有 Git Repo 模式下本地倉庫的磁盤佔用過大,操做卡的問題。

然而,僅僅對 PodSpec 增長了 CDN 根本沒能解決 GFW 致使的 Github 源碼校驗、更新、下載慢的問題。 只能說路漫漫其修遠兮。

PS:做爲 iOS 工程師,就常常被前端同窗 😒 。你看這 CocoaPods 也太垃圾了吧!!!一旦刪掉 Pods 目錄從新 install 就卡半天,緩存基本不生效,哪像 npm 多快 balabala ...

先來看 CDNSource 結構:

require 'cocoapods-core/source'
# ...
module Pod
  class CDNSource < Source
    def initialize(repo)
      # 標記是否正在同步文件
      @check_existing_files_for_update = false
      # 記錄時間用於對比下載文件的新舊程度,以確認是否須要更新保存所下的資源
      @startup_time = Time.new
      # 緩存查詢過的 PodSpec 資源
      @version_arrays_by_fragment_by_name = {}
      super(repo)
    end

    def url
      @url ||= File.read(repo.join('.url')).chomp.chomp('/') + '/'
    end

    def type
      'CDN'
    end
    # ...
  end
end
複製代碼

Source 類是基於 Github Repo 來同步更新 PodSpec,而 CDNSource 則是基於 CDN 服務所返回的 Response,所以將 Source 類的大部分方法重寫了一個遍,具體會在 SourceManager 一節來展開。

最後看一下 TrunkSource 類:

module Pod
  class TrunkSource < CDNSource
    # 新版落盤後倉庫名稱
    TRUNK_REPO_NAME = 'trunk'.freeze

    TRUNK_REPO_URL = 'https://cdn.cocoapods.org/'.freeze

    def url
      @url ||= TRUNK_REPO_URL
      super
    end
  end
end
複製代碼

核心就是重寫了返回的 url,因爲舊版 Spec 倉庫名稱爲 master 爲了加以區分,CDN 倉庫則更名爲 trunk

Source Manager

Manager 做爲 source 的管理類,其主要任務爲 source 的添加和獲取,而對 PodSpec 文件的更新和查找行爲則交由 source 各自實現。不過因爲一個 pod 庫可能對應多個不一樣的 source,這裏又產生出 Aggregate 類來統一 PodSpec 的查詢。

它們的關係以下:

04-manager

Manager 實現:

module Pod
  class Source
    class Manager
      attr_reader :repos_dir

      def initialize(repos_dir)
        @repos_dir = Pathname(repos_dir).expand_path
      end

      def source_repos
        return [] unless repos_dir.exist?
        repos_dir.children.select(&:directory?).sort_by { |d| d.basename.to_s.downcase }
      end

      def aggregate
        aggregate_with_repos(source_repos)
      end

      def aggregate_with_repos(repos)
        sources = repos.map { |path| source_from_path(path) }
        @aggregates_by_repos ||= {}
        @aggregates_by_repos[repos] ||= Source::Aggregate.new(sources)
      end

      def all
        aggregate.sources
      end
      # ...
    end
  end
end
複製代碼

Manager 類的初始化僅須要傳入當前 repos 目錄,即 ~/.cocoapods/repos,而 Aggregate 的生成則保存 repos_dir 了目錄下的 Source,用於後續處理。

先看 Source 的生成,在 #source_from_path 中:

def source_from_path(path)
  @sources_by_path ||= Hash.new do |hash, key|
    hash[key] = case
                when key.basename.to_s == Pod::TrunkSource::TRUNK_REPO_NAME
                  TrunkSource.new(key)
                when (key + '.url').exist?
                  CDNSource.new(key)
                else
                  Source.new(key)
                end
  end
  @sources_by_path[path]
end
複製代碼

repos_dir 下的目錄名稱來區分類型,而 CDNSource 則須要確保其目錄下存在名爲 .url 的文件。同時會對生成的 source 進行緩存。

最後看 Aggregate 結構,核心就兩個 search 方法:

module Pod
  class Source
    class Aggregate
      attr_reader :sources

      def initialize(sources)
        raise "Cannot initialize an aggregate with a nil source: (#{sources})" if sources.include?(nil)
        @sources = sources
      end
      # 查詢依賴對應的 specs
      def search(dependency) ... end
       
      # 查詢某個 pod 以發佈的 specs
      def search_by_name(query, full_text_search = false) ... end
        
      # ...
  end
end
複製代碼

Source 源起

本節咱們來談談 source 是如何添加到 repo_dir 目錄下的。

由前面的介紹可知,每一個 source 中自帶 url,在 Source 類中 url 讀取自 Git 倉庫的 remote.origin.url 或本地 .git 目錄,而在 CDNSource 中 url 則是讀取自當前目錄下的 .url 文件所保存的 URL 地址。

那 CDNSource 的 .url 文件是在何時被寫入的呢 ?

這須要從 Podfile 提及。不少老項目的 Podfile 開頭部分大都會有一行或多行 source 命令:

source 'https://github.com/CocoaPods/Specs.git'
source 'https://github.com/artsy/Specs.git'
複製代碼

用於指定項目中 PodSpec 的查找源,這些指定源最終會保存在 ~/.cocoapods/repos 目錄下的倉庫。

當敲下 pod install 命令後,在 #resolve_dependencies 階段的依賴分析中將同時完成 sources 的初始化。

01-pod-install

# lib/cocoapods/installer/analyzer.rb

def sources
  @sources ||= begin
    # 省略獲取 podfile、plugins、dependencies 的 source url ...
    sources = ...
     
    result = sources.uniq.map do |source_url|
      sources_manager.find_or_create_source_with_url(source_url)
    end
    unless plugin_sources.empty?
      result.insert(0, *plugin_sources)
      plugin_sources.each do |source|
        sources_manager.add_source(source)
      end
    end
    result
  end
end
複製代碼

獲取 sources url 以後會經過 sources_manager 來完成 source 更新,邏輯在 CocoaPods 項目的 Manager 擴展中:

# lib/cocoapods/sources_manager.rb

module Pod
  class Source
    class Manager

      def find_or_create_source_with_url(url)
        source_with_url(url) || create_source_with_url(url)
      end

      def create_source_with_url(url)
        name = name_for_url(url)
        is_cdn = cdn_url?(url)
		  # ...
        begin
          if is_cdn
            Command::Repo::AddCDN.parse([name, url]).run
          else
            Command::Repo::Add.parse([name, url]).run
          end
        rescue Informative => e
          raise Informative, # ...
        ensure
          UI.title_level = previous_title_level
        end
        source = source_with_url(url)
        raise "Unable to create a source with URL #{url}" unless source
        source
      end
      # ...
    end
  end
end
複製代碼

查找會先調用 #source_with_url 進行緩存查詢,如未命中則會先下載 Source 倉庫,結束後重刷 aggreate 以更新 source。

# lib/cocoapods-core/source/manager.rb

def source_with_url(url)
  url = canonic_url(url)
  url = 'https://github.com/cocoapods/specs' if url =~ %r{github.com[:/]+cocoapods/specs}
  all.find do |source|
    source.url && canonic_url(source.url) == url
  end
end

def canonic_url(url)
  url.downcase.gsub(/\.git$/, '').gsub(%r{\/$}, '')
end
複製代碼

另外,倉庫的下載的則會經過 #cdn_url? 方法區分,最後的下載則 📦 在兩個命令類中,歸納以下:

  • Command::Repo::AddCDN:即 pod repo add-cdn 命令,僅有的操做是將 url 寫入 .url 文件中。
  • Command::Repo::Add:即 pod repo add 命令,對於普通類型的 Source 倉庫下載本質就是 git clone 操做。

簡化後源的添加流程以下:

05-source-add

PodSpec 查詢

一樣在 #resolve_dependencies 的依賴仲裁階段,當 Molinillo 依賴仲裁開始前,會觸發緩存查詢 #find_cached_set 並最終調用到 Aggregate 的 #search。完整調用棧放在 gist 上。

咱們來看看 #search 入口:

# lib/cocoapods-core/source/aggregate.rb

def search(dependency)
  found_sources = sources.select { |s| s.search(dependency) }
  unless found_sources.empty?
    Specification::Set.new(dependency.root_name, found_sources)
  end
end
複製代碼

Aggregate 先遍歷當前 sources 並進行 dependency 查找。因爲 Git 倉庫保存了完整的 PodSpecs,只要能在分片目錄下查詢到對應文件便可,最終結果會塞入 Specification::Set 返回。

Specification::Set 記錄了當前 pod 關聯的 Source,一個 pod 可能存在與多個不一樣的 Spec 倉庫 中。

CDN 倉庫查詢

CDNSource 重寫了 #search 實現:

# lib/cocoapods-core/cdn_source.rb

def search(query)
  unless specs_dir
    raise Informative, "Unable to find a source named: `#{name}`"
  end
  if query.is_a?(Dependency)
    query = query.root_name
  end

  fragment = pod_shard_fragment(query)
  ensure_versions_file_loaded(fragment)
  version_arrays_by_name = @version_arrays_by_fragment_by_name[fragment] || {}
   
  found = version_arrays_by_name[query].nil? ? nil : query

  if found
    set = set(query)
    set if set.specification_name == query
  end
end
複製代碼

邏輯兩步走:

  1. 經過 #ensure_versions_file_loaded 檢查 all_pods_versions 文件,若是不存在會進行下載操做。
  2. 若是當前 source 包含查詢的 pod,會建立 Specification::Set 做爲查詢結果,並在 #specification_name 方法內完成 PodSpec 的檢查和下載。

0x01 all_pods_versions 文件下載

依據前面提到的分片規則會將 pod 名稱 MD5 分割後拼成 URL。

AFNetworking 爲例,經 #pod_shard_fragment 分割後獲取的 fragment 爲 [a, 7, 5],則拼接後的 URL 爲 cdn.cocoapods.org/all_pods_ve…

AFNetworking/0.10.0/0.10.1/.../4.0.1
AppseeAnalytics/2.4.7/2.4.8/2.4.8.0/...
DynamsoftBarcodeReader/7.1.0/...
...
複製代碼

所包含的這些 pod 都是分片後獲得的相同的地址,所以會保存在同一份 all_pods_versions 中。

def ensure_versions_file_loaded(fragment)
  return if !@version_arrays_by_fragment_by_name[fragment].nil? && !@check_existing_files_for_update

  index_file_name = index_file_name_for_fragment(fragment)
  download_file(index_file_name)
  versions_raw = local_file(index_file_name, &:to_a).map(&:chomp)
  @version_arrays_by_fragment_by_name[fragment] = versions_raw.reduce({}) do |hash, row|
    row = row.split('/')
    pod = row.shift
    versions = row

    hash[pod] = versions
    hash
  end
end

def index_file_name_for_fragment(fragment)
  fragment_joined = fragment.join('_')
  fragment_joined = '_' + fragment_joined unless fragment.empty?
  "all_pods_versions#{fragment_joined}.txt"
end
複製代碼

另外每一份 pods_version 都會對應生成一個文件用於保存 ETag,具體會在下一節會介紹。

0x02 PodSpec 文件下載

#specification_name 將從 all_pods_versions 索引文件中找出該 pod 所發佈的版本號,依次檢查下載對應版本的 PodSpec.json 文件。

module Pod
  class Specification
    class Set
      attr_reader :name
      attr_reader :sources
      
      def specification_name
        versions_by_source.each do |source, versions|
          next unless version = versions.first
          return source.specification(name, version).name
        end
        nil
      end

      def versions_by_source
        @versions_by_source ||= sources.each_with_object({}) do |source, result|
          result[source] = source.versions(name)
        end
      end
      # ...
    end
  end
end
複製代碼

繞了一圈後回到 Source 的 #versions 方法,因爲 CDN Source 不會全量下載 pod 的 PodSpec 文件,在 #version 的檢查過程會進行下載操做。

08-cdn-search

Pod Search 查詢命令

CocoaPods 還提供了命令行工具 cocoapods-search 用於已發佈的 PodSpec 查找:

$ pod search `QUERY`
複製代碼

它提供了 Web 查詢和本地查詢。本地查詢則不一樣於 #search,它須要調用 Aggregate 的 #search_by_name ,其實現同 #search 相似,最終也會走到 Source 的 #versions 方法。

07-pod-search

注意,Gti 倉庫的 #search_by_name 查詢仍舊爲文件查找,不會調用其 #versions 方法。

Repo 更新

pod install 執行過程若是帶上了 --repo-update 命令則在 #resolve_dependencies 階段會觸發 #update_repositories 更新 Spec 倉庫:

# lib/cocoapods/installer/analyzer.rb

def update_repositories
  sources.each do |source|
    if source.updateable?
      sources_manager.update(source.name, true)
    else
      UI.message "Skipping ..."
    end
  end
  @specs_updated = true
end
複製代碼

不過 #update 的實現邏輯在 CocoaPods 項目的 Manager 擴展中:

# lib/cocoapods/sources_managers.rb

def update(source_name = nil, show_output = false)
  if source_name
    sources = [updateable_source_named(source_name)]
  else
    sources = updateable_sources
  end

  changed_spec_paths = {}

  # Do not perform an update if the repos dir has not been setup yet.
  return unless repos_dir.exist?

  File.open("#{repos_dir}/Spec_Lock", File::CREAT) do |f|
    f.flock(File::LOCK_EX)
    sources.each do |source|
      UI.section "Updating spec repo `#{source.name}`" do
        changed_source_paths = source.update(show_output)
        changed_spec_paths[source] = changed_source_paths if changed_source_paths.count > 0
        source.verify_compatibility!
      end
    end
  end
  update_search_index_if_needed_in_background(changed_spec_paths)
end
複製代碼
  1. 獲取指定名稱的 source,對 aggregate 返回的所有 sources 進行 filter,如未指定則 sources 全量。

  2. 挨個調用 source.update(show_output),注意 Git 和 CDN 倉庫的更新方式的不一樣。

Git 倉庫更新

Git 倉庫更新本質就是 Git 操做,即 git pullgit checkout 命令:

def update(show_output)
  return [] if unchanged_github_repo?
  prev_commit_hash = git_commit_hash
  update_git_repo(show_output)
  @versions_by_name.clear
  refresh_metadata
  if version = metadata.last_compatible_version(Version.new(CORE_VERSION))
    tag = "v#{version}"
    CoreUI.warn "Using the ..."
    repo_git(['checkout', tag])
  end
  diff_until_commit_hash(prev_commit_hash)
end
複製代碼

#update_git_repo 就是 git fetch + git reset --hard [HEAD] 的結合體,更新後會進行 cocoapods 版本兼容檢查,最終輸出 diff 信息。

CDN 倉庫更新

Git 倉庫是能夠經過 Commit 信息來進行增量更新,那以靜態資源方式緩存的 CDN 倉庫是如何更新數據的呢 ?

像瀏覽器或本地緩存本質是利用 ETag 來進行 Cache-Control,關於 CDN 緩存能夠看這篇:傳送門

而 ETag 就是一串字符,內容一般是數據的哈希值,由服務器返回。首次請求後會在本地緩存起來,並在後續的請求中攜帶上 ETag 來肯定緩存是否須要更新。若是 ETag 值相同,說明資源未更改,服務器會返回 304(Not Modified)響應碼。

Core 的實現也是如此,它會將各請求所對應的 ETag 以文件形式存儲:

06-source-cdn

⚠️ 注意,在這個階段 CDNSource 僅僅是更新當前目錄下的索引文件,即 all_pods_versions_x_x_x.txt

def update(_show_output)
  @check_existing_files_for_update = true
  begin
    preheat_existing_files
  ensure
    @check_existing_files_for_update = false
  end
  []
end

def preheat_existing_files
  files_to_update = files_definitely_to_update + deprecated_local_podspecs - ['deprecated_podspecs.txt']

  concurrent_requests_catching_errors do
    loaders = files_to_update.map do |file|
      download_file_async(file)
    end
    Promises.zip_futures_on(HYDRA_EXECUTOR, *loaders).wait!
  end
end
複製代碼

Pod Repo 更新命令

CocoaPods 對於 sources 倉庫的更新也提供了命令行工具:

$ pod repo update `[NAME]`
複製代碼

其實現以下:

# lib/cocoapods/command/repo/update.rb

module Pod
  class Command
    class Repo < Command
      class Update < Repo
        def run
          show_output = !config.silent?
          config.sources_manager.update(@name, show_output)
          exclude_repos_dir_from_backup
        end
        # ...
      end
    end
  end
end
複製代碼

在命令初始化時會保存指定的 Source 倉庫名稱 @name,接着經過 Mixin 的 config 來獲取 sources_manager 觸發更新。

最後用一張圖來收尾 CocoaPods Workflow:

09-cocoapods-flow

總結

最後一篇 Core 的分析文章,重點介紹了它是如何管理 PodSpec 倉庫以及 PodSpec 文件的更新和查找,總結以下:

  1. 瞭解 Source Manager 的各類數據結構以及它們之間的相互關係,各個類之間竟然都作到了權責分明。
  2. 經過對 Metadata 的分析瞭解了 Source 倉庫的演變過程,並剖析了存在的問題。
  3. 掌握瞭如何利用 CDN 來改造原有的 Git 倉庫,優化 PodSpec 下載速度。
  4. 發現原來 CLI 工具不只僅能夠提供給用戶使用,內部調用也不是不能夠。

知識點問題梳理

這裏羅列了五個問題用來考察你是否已經掌握了這篇文章,若是沒有建議你加入收藏再次閱讀:

  1. PodSpecs 的聚合類有哪些,能夠經過哪些手段來區分他們的類型 ?
  2. 說說你對 Aggregate 類的理解,以及它的主要做用 ?
  3. Source 類是如何更新 PodSpec
  4. Core 是如何對倉庫進行分片的,它的分片方式是否支持配置 ?
  5. CDN 倉庫是如何來更新 PodSpec 文件 ?
相關文章
相關標籤/搜索