7. Molinillo 依賴校驗

Molinillo 依賴校驗

引子

經過「前文」對 CocaPods-Core 的分析,咱們大致瞭解了 Pod 是如何被解析、查詢與管理的。有了這些總體概念以後,咱們就能夠逐步深刻 pod install 的各個細節。今天咱們就來聊聊 Pod 的依賴校驗工具 --- Molinillo。開始前,須要聊聊依賴校驗的背景。node

依賴管理的挑戰

同大多數包管理工具同樣 Pod 會將傳遞依賴的包用扁平化的形式,安裝至 workspace 目錄 (即:Pods/)。git

依賴傳遞pod A 依賴於 pod B,而 pod B 依賴 Alamofiregithub

01-dependency-flat

能夠看到,經依賴解析原有的依賴樹被拍平了,安裝在同層目錄中。算法

然而在大型項目中,遇到的更多狀況可能像下面這樣:npm

02-dependency-conflict

**依賴衝突:pod Apod B 分別依賴不一樣版本的 Alamofire。這就是依賴地獄**的開始。設計模式

依賴地獄:指在操做系統中因爲軟件之間的依賴性不能被知足而引起的問題。數組

隨着項目的迭代,咱們不斷引入依賴並最終造成錯綜複雜的網絡。這使得項目的依賴性解析變得異常困難,甚至出現致命錯誤ruby

那麼,產生的問題有哪些類型 ?微信

問題類型

依賴過多/多重依賴

即項目存在大量依賴關係,或者依賴自己有其自身依賴(依賴傳遞),致使依賴層級過深。像微信或淘寶這樣的超級應用,其中的單一業務模塊均可能存在這些問題,這將使得依賴解析過於複雜,且容易產生依賴衝突和依賴循環。markdown

依賴衝突

即項目中的兩個依賴包沒法共存的狀況。可能兩個依賴庫內部的代碼衝突,也可能其底層依賴互相沖突。上面例子中因 Alamofire 版本不一樣產生的問題就是依賴衝突。

依賴循環

即依賴性關係造成一個閉合環路。以下圖三個 pod 庫之間互相依賴產生循環:

03-dependency-conflict

要判斷依賴關係中是否存在依賴環,則須要通依賴仲裁算法來解決。

依賴關係的解決

對於依賴過多或者多重依賴問題,咱們可經過合理的架構和設計模式來解決。而依賴校驗主要解決的問題爲:

  1. 檢查依賴圖是否存在版本衝突;
  2. 判斷依賴圖是否存在循環依賴;

0x01 版本衝突的解決方案

對於版本衝突可經過修改指定版本爲帶兼容性的版本範圍問題來避免。如上面的問題有兩個解決方案:

  • 經過修改兩個 podAlamofire 版本約束爲 ~> 4.0 來解決。
  • 去除兩個 pod 的版本約束,交由項目中的 Podfile 來指定。

不過這樣會有一個隱患,因爲兩個 Pod 使用的主版本不一樣,可能帶來 API 不兼容,致使 pod install 即便成功了,最終也沒法編譯或運行時報錯。

還有一種解決方案,是基於語言特性來進行依賴性隔離。如 npm 的每一個傳遞依賴包若是衝突均可以有本身的 node_modules 依賴目錄,即一個依賴庫能夠存在多個不一樣版本。

04-dependency-conflict

0x02 循環依賴的解決方案

循環依賴則須要須要進行數學建模生成 DAG 圖,利用拓撲排序的拆點進行處理。經過肯定依賴圖是否爲 DAG 圖,來驗證依賴關係的合理性。

一個 DAG 圖的示例:

04-DAG

DAG 是圖論中常見的一種描述問題的結構,全稱**有向無環圖 (Directed Acyclic Graph) **。想了解更多,可查看瓜瓜的文章 ---「從拓撲排序到 Carthage 依賴校驗算法」。

另外,各類包管理工具的依賴校驗算法也各不相同,有如 Dart 和 SwiftPM 所使用的 PubGrub,做者號稱其爲下一代依賴校驗算法,Yarn 的 Selective dependency resolutions,還有咱們今天聊到的 Molinillo。

Molinillo

Molinillo 做爲通用的依賴解析工具,它不只應用在 CocoaPods 中,在 Bundler 1.9 版本也採用 Molinillo。另外,值得注意的是 Bundler 在 Ruby 2.6 中被做爲了默認的 gem 工具內嵌。能夠說 Ruby 相關的依賴工具都經過 Molinillo 完成依賴解析。

ResolutionState

Molinillo 算法的核心是基於回溯 (Backtracking)向前檢查 (forward checking),整個過程會追蹤棧中的兩個狀態 DependencyStatePossibilityState

module Molinillo
  # 解析狀態
  ResolutionState = Struct.new(
    # [String] 當前需求名稱
    :name,
    # [Array<Object>] 未處理的需求
    :requirements,
    # [DependencyGraph] 依賴關係圖
    :activated,
    # [Object] 當前需求
    :requirement,
    # [Object] 知足當前需求的可能性
    :possibilities,
    # [Integer] 解析深度
    :depth,
    # [Hash] 未解決的衝突,以需求名爲 key
    :conflicts,
    # [Array<UnwindDetails>] 記錄着未處理過的須要用於回溯的信息
    :unused_unwind_options
  )

  class ResolutionState
    def self.empty
      new(nil, [], DependencyGraph.new, nil, nil, 0, {}, [])
    end
  end

  # 記錄一組需求和知足當前需求的可能性
  class DependencyState < ResolutionState
	 # 經過不斷 pop 過濾包含的可能性,找出最符合需求的解
    def pop_possibility_state
      PossibilityState.new(
        name,
        requirements.dup,
        activated,
        requirement,
        [possibilities.pop],
        depth + 1,
        conflicts.dup,
        unused_unwind_options.dup
      ).tap do |state|
        state.activated.tag(state)
      end
    end
  end

  # 僅包含一個知足需求的可能性
  class PossibilityState < ResolutionState
  end
end
複製代碼

光看 state 定義你們可能以爲雲裏霧裏。這裏頗有必要解釋一下:

咱們說的需求 (requirement) 究竟是指什麼呢?你們能夠理解爲在 Podfile 中聲明的 pod。**之因此稱爲需求,是因爲沒法判判定義的 dependency 是否合法。**假設它合法,又是否存在符合需求限制版本的解呢 ?便是否存在對應的 PodSpec 咱們不而知。所以,這些未知狀態稱爲統一被可能性 possibility

Tips: 瞭解這個概念很是重要,這也是筆者在幾乎寫完本文的狀況下,纔想明白這些變量名的意義。💔

Resolution Loop

咱們先經過圖來了解一下 Molinillo 的核心流程 (先忽略異常流):

07-backtrack-state

能夠看到整個流程就是不斷的將 requirement 的 possibility 過濾和處理,一層層剝離轉換爲 DependencyState,如此循環往復。

Molinillo 的入口爲 Resolution::resolve 方法,也是上圖對應的實現,邏輯以下:

# lib/molinillo/resolution.rb

def resolve
  # 1. 初始化 timer 統計耗時初始位置打點
  # 2. 內部會調用 push_initial_state 初始化 DependencyState 壓棧
  # 3. 初始化 DependencyGraph 實例
  start_resolution

  while state
    break if !state.requirement && state.requirements.empty?
    # 輸出一個進度佔位
    indicate_progress
    if state.respond_to?(:pop_possibility_state) # DependencyState
      # 調試日誌入口
      # 若是環境變量 MOLINILLO_DEBUG 是非 nil 就輸出 log
      # 這裏的調試日誌有助於排查 Pod 組件的依賴問題
      debug(depth) { "Creating possibility state for #{requirement} (#{possibilities.count} remaining)" }
      state.pop_possibility_state.tap do |s|
        if s
          states.push(s)
          activated.tag(s)
        end
      end
    end
    # 處理棧頂 Possibility State
    process_topmost_state
  end
  # 遍歷 Dependency Graph 
  resolve_activated_specs
ensure
  end_resolution
end
複製代碼
  1. 首先 #start_resolution 會初始化 timer 用於統計解析耗時,並調用 #push_initial_state 初始化 DependencyState 入棧,以及 DependencyGraph 初始化。
  2. 獲取棧頂 state 檢查是否存在待解析需求,接着調用 #pop_possibility_state 進行 state 轉換併入棧。
  3. 調用 #process_topmost_state 處理棧頂的 possiblity state,若是當前 state 可被激活,則將該 possiblity 存入 DependencyGraph 對應頂點的 payload 中。不然斷定爲衝突,須要進行狀態回滾。
  4. 循環直到 state 的可能性所有處理結束。
  5. 調用 #resolve_activated_specs ,遍歷 DependencyGraph 以存儲更新需求的可能性,解析結束。

固然,依賴處理並不是這麼簡單,複雜的過濾和回溯邏輯都隱藏在 #process_topmost_state 中。

ResolutionState 的變化

其實從 ResolutionState 的定義可以看出,爲了方便回溯和數據還原,state 是以 Struct 結構定義的。同時在每次 #pop_possibility_state 中,經過 #dup 對 diff 數據進行了複製。

這裏用依賴傳遞的例子來展現解析後狀態棧的變化。假設咱們在 Podfile 中聲明瞭 A,B,C 三個依賴,他們的關係爲:A -> B -> C

target 'Example' do
  pod 'C', :path => '../'
  pod 'B', :path => '../'  
  pod 'A', :path => '../’ end 複製代碼

#resolve_activated_specs 方法設置斷點,在解析結束時打印狀態棧 @states(簡化處理後)以下:

[
   #<struct Molinillo::DependencyState name="C", requirements=[B, A], ...>, 
   #<struct Molinillo::PossibilityState name="C", requirements=[B, A], ...>
   #<struct Molinillo::DependencyState name="B", requirements=[A], ...>
   #<struct Molinillo::PossibilityState name="B", requirements=[A], ...>
   # 省略了 C、C、A、A...
   #<struct Molinillo::DependencyState name="B", requirements=[], ..., 
   #<struct Molinillo::PossibilityState name="B", requirements=[], ..., 
   #<struct Molinillo::DependencyState name="", requirements=[], ..., 
]
複製代碼

能夠看到棧內保存的 states 中 DependencyStatePossibilityState 是成對出現的。不過最後入棧的 DependencyState 是一個空狀態,requirements 也爲空,此時沒法再 pop state 循環結束。

DependencyGraph

其實包括 Molinillo 在內的依賴解析工具都會在運行期間對依賴關係進行建模來構建依賴圖,畢竟這是咱們表達依賴關係的方式。那麼 DependencyGraph (如下簡稱 dg ) 是如何定義:

module Molinillo

  class DependencyGraph
    # 有向邊
    Edge = Struct.new(:origin, :destination, :requirement)
    # @return [{String => Vertex}] 用字典保存頂點, key 爲頂點名稱(即 requirement.name)
    attr_reader :vertices
    # @return [Log] 操做日誌
    attr_reader :log
	 ...
end
複製代碼

另外 Vertex 定義以下:

module Molinillo
  class DependencyGraph
    class Vertex
      attr_accessor :name
      # @return [Object] 頂點的元數據,reqiuremnt 對應的 possiblity
      attr_accessor :payload
      # @return [Array<Object>] 須要依賴該頂點可能性能的需求
      attr_reader :explicit_requirements
      # @return [Boolean] 是否爲根結點
      attr_accessor :root
      # @return [Array<Edge>] 出度 {Edge#origin}
      attr_accessor :outgoing_edges
      # @return [Array<Edge>] 入度 {Edge#destination}
      attr_accessor :incoming_edges
      # @return [Array<Vertex>] 入度的起點
      def predecessors
        incoming_edges.map(&:origin)
      end
      # @return [Array<Vertex>] 出度的終點
      def successors
        outgoing_edges.map(&:destination)
      end
      ...
    end
  end
end
複製代碼

熟悉圖論的同窗都瞭解,圖的保存經常使用的方式是鄰接表鄰接矩陣。Molinillo 則經過 map + list,vertext 字典與邊集數組來保存。若是僅用邊集數組來查詢頂點自己效率並不高,好在頂點直接用了字典保存了。Molinillo 經過棧來維護解析狀態,不斷將解析結果 possibility 存入 dg 的 payload 中,同時記錄了各個頂點的依賴關係,即 dg 的出度和入度。

06-Vertex

  • 在有向圖中對於一個頂點來講,若是一條邊的終點是這個頂點,這條邊就是這個頂點的入度;
  • 在有向圖中對於一個頂點來講,若是一條邊的起點是這個頂點,這條邊就是這個頂點的出度。

當成功解析的一刻,dg 圖也構建完畢。

Operation Log

當解析過程出現衝突時,狀態棧要回溯直接 pop 一下就完事了,而 dg 咋辦 ? 它可無法 pop。

好在 Molinillo 設計了 Operation Log 機制,經過 Log 記錄 dg 執行過的操做。這些操做類型包括:AddEdgeNoCircular、AddVertex、DeleteEdge、DetachVertexNamed、SetPayload、Tag

Log 結構以下:

# frozen_string_literal: true

module Molinillo
  class DependencyGraph
    class Log
      def initialize
        @current_action = @first_action = nil
      end

      def pop!(graph)
        return unless action = @current_action
        unless @current_action = action.previous
          @first_action = nil
        end
        action.down(graph)
        action
      end

      # 回撤到指定的操做節點
      def rewind_to(graph, tag)
        loop do
          action = pop!(graph)
          raise "No tag #{tag.inspect} found" unless action
          break if action.class.action_name == :tag && action.tag == tag
        end
      end

      private

      # 插入操做節點
      def push_action(graph, action)

        action.previous = @current_action
        @current_action.next = action if @current_action
        @current_action = action
        @first_action ||= action
        action.up(graph)
      end
      ...
    end
  end
end
複製代碼

標準的鏈表結構,Log 提供了當前指針 @current_action 和表頭指針 @first_action 便於鏈表的遍歷。接着看看 Action:

# frozen_string_literal: true

module Molinillo
  class DependencyGraph

    class Action
      # @return [Symbol] action 名稱
      def self.action_name
        raise 'Abstract'
      end

      # 對圖執行正向操做
      def up(graph)
        raise 'Abstract'
      end

      # 撤銷對圖的操做
      def down(graph)
        raise 'Abstract'
      end
       
      # @return [Action,Nil] 前序節點
      attr_accessor :previous
      # @return [Action,Nil] 後序節點
      attr_accessor :next
    end
  end
end
複製代碼

Action 自己是個抽象類,Log 經過 Action 子類的 #up#down 來完成對 dg 的操做和撤銷。所提供的 Action 中除了 Tag 特殊一點,其他均是對 dg 的頂點和邊的 CURD 操做。這裏以 AddVertex 爲例:

# frozen_string_literal: true

require_relative 'action'
module Molinillo
  class DependencyGraph
	 # @!visibility private
    class AddVertex < Action # :nodoc:
      def self.action_name
        :add_vertex
      end
      
      # 操做添加頂點
      def up(graph)
        if existing = graph.vertices[name]
          @existing_payload = existing.payload
          @existing_root = existing.root
        end
        vertex = existing || Vertex.new(name, payload)
        graph.vertices[vertex.name] = vertex
        vertex.payload ||= payload
        vertex.root ||= root
        vertex
      end

      # 刪除頂點
      def down(graph)
        if defined?(@existing_payload)
          vertex = graph.vertices[name]
          vertex.payload = @existing_payload
          vertex.root = @existing_root
        else
          graph.vertices.delete(name)
        end
      end

      # @return [String] 頂點名稱 (或者說依賴名稱)
      attr_reader :name
      # @return [Object] 頂點元數據
      attr_reader :payload
      # @return [Boolean] 是否爲根
      attr_reader :root
		...
    end
  end
end
複製代碼

Action 子類均聲明爲 private 的,經過 Log 提供的對應方法來執行。

def tag(graph, tag)
  push_action(graph, Tag.new(tag))
end

def add_vertex(graph, name, payload, root)
  push_action(graph, AddVertex.new(name, payload, root))
end

def detach_vertex_named(graph, name)
  push_action(graph, DetachVertexNamed.new(name))
end

def add_edge_no_circular(graph, origin, destination, requirement)
  push_action(graph, AddEdgeNoCircular.new(origin, destination, requirement))
end

def delete_edge(graph, origin_name, destination_name, requirement)
  push_action(graph, DeleteEdge.new(origin_name, destination_name, requirement))
end

def set_payload(graph, name, payload)
  push_action(graph, SetPayload.new(name, payload))
end
複製代碼

最後 log 聲明的這些方法會由 dg 直接調用,如 #addVertext:

module Molinillo
  class DependencyGraph
    def add_vertex(name, payload, root = false)
      log.add_vertex(self, name, payload, root)
    end
    ...
  end
end
複製代碼

Unwind For Conflict

有了 op log 以後咱們還須要同樣重要的東西:哨兵節點。由 Tag 類來承載:

# frozen_string_literal: true
module Molinillo
  class DependencyGraph
    # @!visibility private 
    class Tag < Action

      def up(graph)
      end
       
      def down(graph)
      end

      attr_reader :tag

      def initialize(tag)
        @tag = tag
      end
    end
  end
end
複製代碼

做爲哨兵節點 Tag 的 #up#down 操做老是成對出現的。在 Molinillo 中有兩處須要進行狀態回溯,分別爲可能性校驗和衝突狀態回撤。

0x1 可能性校驗

#possibility_satisfies_requirements? 方法用於衝突產生的先後,用於判斷該可能性可否同時知足多個需求:

def possibility_satisfies_requirements?(possibility, requirements)
  name = name_for(possibility)

  activated.tag(:swap)
  activated.set_payload(name, possibility) if activated.vertex_named(name)
  satisfied = requirements.all? { |r| requirement_satisfied_by?(r, activated, possibility) }
  activated.rewind_to(:swap)

  satisfied
end
複製代碼

爲了直觀的說明參數,咱們舉個例子。Case 1假設 Podfile 中存在 pod A 和 B,且 A、B 分別依賴了 Alamofire 3.0 和 4.0,那麼對應的參數爲:

possibility: #<Pod::Specification name="Alamofire" version="4.0.0">
requirements: [
	<Pod::Dependency name=Alamofire requirements=~> 3.0 source=nil external_source=nil>, 		<Pod::Dependency name=Alamofire requirements=~> 4.0 source=nil external_source=nil>
]
複製代碼

如今來看方法實現:

  1. 首先 activated 就是 Podfile 解析生成的 dg 對象,這裏將 symbol :swap 做爲標識用於稍後的回撤;
  2. 調用 #set_payload 將頂點 Alamofire 的 payload 修改成 possibility 版本;
  3. 遍歷 requirements 並調用代理的 #requirement_satisfied_by 以校驗 possiblity 在 dg 中存在的可能性;
  4. 調用 #rewind_to 將頂點的修改回撤至 :swap 前的狀態,最後返回檢驗結果。

Tips: 此處的代理是指 CocoaPods,它作爲 Molinillo 的 client 實現了不少代理方法,後續會聊到。

做爲候選項 possibility 固然不止一個,代理提供的查詢方法 #search_for(dependency) 會返回全部符合 requiremnt 名稱的依賴。在 CocoaPods 中,就是經過 Pod::Source 查詢得到全部版本的 Pod::Specification,具體能夠看上一篇文章:PodSpec 管理策略

0x2 衝突狀態回撤

依賴解析過程出現衝突屬於正常狀況,此時經過回撤也許能夠避免部分衝突,找出其它可行解。Molinillo 經過定義 Conflict 來記錄當前的衝突的必要信息:

Conflict = Struct.new(
  :requirement,
  :requirements,
  :existing,
  :possibility_set,
  :locked_requirement,
  :requirement_trees,
  :activated_by_name,
  :underlying_error
)
複製代碼

重點關注 underlying_error,它記錄了所攔截的指定類型錯誤,並用於狀態回撤時的一些判斷依據 (後面會解釋)。這裏咱們先看一下定義的錯誤類型:

# frozen_string_literal: true

module Molinillo

  class ResolverError < StandardError; end
   
  # 錯誤信息:"Unable to find a specification for `#{dependency}`"
  class NoSuchDependencyError < ResolverError ... end
        
  # 錯誤信息:"There is a circular dependency between ..."
  class CircularDependencyError < ResolverError ... end
        
  # 當出現版本衝突時拋出
  # 錯誤信息:"Unable to satisfy the following requirements:\n\n ..."
  class VersionConflict < ResolverError ... end
end
複製代碼

除了主動攔截錯誤以外,possiblity 不存在時也會主動生成衝突,同時進入狀態回撤處理。發生衝突後調用 #create_conflict#unwind_for_conflict 兩個方法分別用於生成 Conflict 對象和狀態回撤。

def process_topmost_state
  if possibility
    attempt_to_activate
  else
    create_conflict
    unwind_for_conflict
  end
rescue CircularDependencyError => underlying_error
  create_conflict(underlying_error)
  unwind_for_conflict
end

def attempt_to_activate
  debug(depth) { 'Attempting to activate ' + possibility.to_s }
  existing_vertex = activated.vertex_named(name)
  if existing_vertex.payload
    debug(depth) { "Found existing spec (#{existing_vertex.payload})" }
    attempt_to_filter_existing_spec(existing_vertex)
  else
    latest = possibility.latest_version
    possibility.possibilities.select! do |possibility|
      requirement_satisfied_by?(requirement, activated, possibility)
    end
    if possibility.latest_version.nil?
      # ensure there's a possibility for better error messages
      possibility.possibilities << latest if latest
      create_conflict
      unwind_for_conflict
    else
      activate_new_spec
    end
  end
end

def attempt_to_filter_existing_spec(vertex)
  filtered_set = filtered_possibility_set(vertex)
  if !filtered_set.possibilities.empty?
    activated.set_payload(name, filtered_set)
    new_requirements = requirements.dup
    push_state_for_requirements(new_requirements, false)
  else
    create_conflict
    debug(depth) { "Unsatisfied by existing spec (#{vertex.payload})" }
    unwind_for_conflict
  end
end
複製代碼

能夠看到這 3 個方法中處理了 4 處衝突的狀況。其中 #process_topmost_state 方法攔截了 CircularDependencyError 並將其記錄在 Conflict 的 underlying_error 中,其他的都是由於 possibility 可行解不存在而主動拋出衝突。

咱們簡化成下面的狀態圖:

08-process-topmost-state

能夠理解 possiblity 狀態機,經過不斷檢查可能性,一旦出錯主動生成異常。爲何要這麼作 ? 由於狀態回溯的成本是很高的,一旦發生意味着咱們以前檢查工做可能就白費了。這也是 Molinillo 前向查詢的充電,經過提前暴露問題,提早回溯。

unwind_for_conflict

瞭解了衝突時如何產生以後,接下來該 #unwind_for_conflict 登場了:

def unwind_for_conflict
  details_for_unwind = build_details_for_unwind
  unwind_options = unused_unwind_options
  debug(depth) { "Unwinding for conflict: #{requirement} to #{details_for_unwind.state_index / 2}" }
  conflicts.tap do |c|
    sliced_states = states.slice!((details_for_unwind.state_index + 1)..-1)
    raise_error_unless_state(c)
    activated.rewind_to(sliced_states.first || :initial_state) if sliced_states
    state.conflicts = c
    state.unused_unwind_options = unwind_options
    filter_possibilities_after_unwind(details_for_unwind)
    index = states.size - 1
    @parents_of.each { |_, a| a.reject! { |i| i >= index } }
    state.unused_unwind_options.reject! { |uw| uw.state_index >= index }
  end
end
複製代碼

衝突回溯就涉及到前面說過的兩個狀態須要處理,分別是狀態棧 @states 和 dg 內容的回溯。 @state 自己是數組實現的,其元素是各個狀態的 state, 要回溯到指定的 state 則要利用 state_index,它保存在 UnwindDetails 中:

UnwindDetails = Struct.new(
  :state_index,
  :state_requirement,
  :requirement_tree,
  :conflicting_requirements,
  :requirement_trees,
  :requirements_unwound_to_instead
)

class UnwindDetails
  include Comparable
  ...
end
複製代碼

這裏解釋一下 requirement_trees,這裏是指以當前需求做爲依賴的需求。以上面的 Case 1 爲例,當前衝突的 requirement 就是 Alamofire,對應 requirement_trees 就是依賴了 Alamofire 的 Pod A 和 B:

[
  [
    <Pod::Dependency name=A requirements=nil source=nil external_source=nil>,
    <Pod::Dependency name=Alamofire requirements=~> 3.0 ...>
  ],[
    <Pod::Dependency name=B ...>,
    <Pod::Dependency name=Alamofire requirements=~> 4.0 ...>
  ]
]
複製代碼

#build_details_for_unwind 主要用於生成 UnwindDetails,大體流程以下:

def build_details_for_unwind
  current_conflict = conflicts[name]
  binding_requirements = binding_requirements_for_conflict(current_conflict)
  unwind_details = unwind_options_for_requirements(binding_requirements)

  last_detail_for_current_unwind = unwind_details.sort.last
  current_detail = last_detail_for_current_unwind

  # filter & update details options
  ...
  current_detail
end
複製代碼
  1. 以 conflict.requirement 爲參數,執行 #binding_requirements_for_conflict 以查找出存在衝突的需求 binding_requirements。查詢是經過代理的 #search_for(dependency) 方法;
  2. 經過 #unwind_options_for_requirements 遍歷查詢到的 binding_requirements 獲取 requirement 對應的 state 以及該 state 在棧中的 index,用於生成 unwind_details;
  3. 對 unwind_details 排序,取 last 做爲 current_detail 並進行其餘相關的修改。

關於如何獲取 state_index 和 unwind_details:

def unwind_options_for_requirements(binding_requirements)
  unwind_details = []

  trees = []
  binding_requirements.reverse_each do |r|
    partial_tree = [r]
    trees << partial_tree
    unwind_details << UnwindDetails.new(-1, nil, partial_tree, binding_requirements, trees, [])
    # 1.1 獲取 requirement 對應的 state
    requirement_state = find_state_for(r)
    # 1.2 確認 possibility 存在
    if conflict_fixing_possibilities?(requirement_state, binding_requirements)
      # 1.3 生成 detail 存入 unwind_details
      unwind_details << UnwindDetails.new(
        states.index(requirement_state),
        r,
        partial_tree,
        binding_requirements,
        trees,
        []
      )
    end
    
    # 2. 沿着 requirement 依賴樹的父節點獲取其 state
    parent_r = parent_of(r)
    next if parent_r.nil?
    partial_tree.unshift(parent_r)
    requirement_state = find_state_for(parent_r)
    # 重複 1.2, 1.3 步驟 ...
 	
    # 6. 沿着依賴樹,重複上述操做
    grandparent_r = parent_of(parent_r)
    until grandparent_r.nil?
      partial_tree.unshift(grandparent_r)
      requirement_state = find_state_for(grandparent_r)
      # 重複 1.二、1.3 步驟 ...
      parent_r = grandparent_r
      grandparent_r = parent_of(parent_r)
    end
  end

  unwind_details
end
複製代碼

確認 state_index 後,棧回溯反而比較簡單了,直接 #slice! 便可:

sliced_states = states.slice!((details_for_unwind.state_index + 1)..-1)
複製代碼

dg 回撤仍是 activated.rewind_to(sliced_states.first || :initial_state) if sliced_states。回撤結束後,流程從新回到 Resolution Loop。

SpecificationProvider

最後一節簡單聊聊 SpecificationProvider。爲了更好的接入不一樣平臺,同時保證 Molinillo 的通用性和靈活性,做者將依賴描述文件查詢等邏輯抽象成了代理。

SpecificationProvider 做爲單獨的 Module 聲明瞭接入端必須實現的 API:

module Molinillo
   module SpecificationProvider

    def search_for(dependency)
      []
    end

    def dependencies_for(specification)
      []
    end
    ...
  end
end
複製代碼

而 Provider 就是在 Molinillo 初始化的時候注入的:

require_relative 'dependency_graph'

module Molinillo

  class Resolver
    require_relative 'resolution'

    attr_reader :specification_provider
    attr_reader :resolver_ui

    def initialize(specification_provider, resolver_ui)
      @specification_provider = specification_provider
      @resolver_ui = resolver_ui
    end

    def resolve(requested, base = DependencyGraph.new)
      Resolution.new(specification_provider,
                     resolver_ui,
                     requested,
                     base).
        resolve
    end
  end
end
複製代碼

而在 CocoaPods 中的初始化方法則是:

# /lib/CocoaPods/resolver.rb
def resolve
  dependencies = @podfile_dependency_cache.target_definition_list.flat_map do |target|
    @podfile_dependency_cache.target_definition_dependencies(target).each do |dep|
      next unless target.platform
      @platforms_by_dependency[dep].push(target.platform)
    end
  end.uniq
  @platforms_by_dependency.each_value(&:uniq!)
  @activated = Molinillo::Resolver.new(self, self).resolve(dependencies, locked_dependencies)
  resolver_specs_by_target
rescue Molinillo::ResolverError => e
  handle_resolver_error(e)
end
複製代碼

該方法則處於 pod install 中的 resolve dependencies 階段:

01-pod-install

NoSuchDependencyError

另外,爲了更好的處理產生的異常,同時保證核心邏輯對 provider 的無感知,Molinillo 將代理方法作了一層隔離,而且對異常作了統一攔截:

module Molinillo
  module Delegates

    module SpecificationProvider

      def search_for(dependency)
        with_no_such_dependency_error_handling do
          specification_provider.search_for(dependency)
        end
      end

      def dependencies_for(specification)
        with_no_such_dependency_error_handling do
          specification_provider.dependencies_for(specification)
        end
      end

      ...

      private

      def with_no_such_dependency_error_handling
        yield
      rescue NoSuchDependencyError => error
        if state
          ...
        end
        raise
      end
    end
  end
end
複製代碼

總結

本篇文章從依賴解析的狀態維護、狀態存儲、狀態回溯三個維度來解構 Molinillo 的核心邏輯,它們分別對應了 ResolutionState、DependencyGraph、UnwindDetail 這三種數據結構。一開始寫這篇內容時,頭腦中對於這些概念是未知的,由於一開始就直接看了做者對 Molinillo 的架構闡述更是徹底找不到思緒,好在我有 VSCode !!!最終依據不一樣 Case 下的數據呈現,一點點的進行源碼調試,大體摸清的 Molinillo 的狀態是如何變化轉移的。最後一點,英文和數據結構仍是很重要的,possiblity 你理解了嗎 ?

知識點問題梳理

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

  1. 說說 Resolution 棧中的 state 是如何轉移的 ?
  2. DependencyGraph 的數據經過什麼方式進行回撤的 ?
  3. #process_topmost_state 處理了幾種 conflict 狀況 ?
  4. UnwindDetail 的 state_index 是如何獲取的 ?
  5. 做者如何利用 SpecificationProvider 來解偶的 ?
相關文章
相關標籤/搜索