作者:洪尉(洪茶)
如果你是一名 iOS 程序员,或者你对包治理技术感兴趣,举荐你浏览本文。你能够理解到 iOS 版本仲裁的底层原理、它的潜在性能危险、以及如何预防 Pod update 的性能好转,从而对 CocoaPods 有更深刻的了解。此外,你还能理解到利用在 Flutter 的新一代的版本仲裁算法 Pubgrub,以及不同技术栈依赖管理策略的差别,从而对包治理技术畛域有更全面的了解。
Pod Update 慢了 8 倍!!!
周五早晨,帅帅在 iOS 群求助:“我主工程跑 Pod update,始终卡着不动,有人遇到过吗?好奇怪!CPU 被 Ruby 过程占满,但没有任何网络申请。”小明看了帅帅的截图,回了一句:“我遇到过,这是失常的,CocoaPods 在做解决依赖”。帅帅只好无奈地承受了漫长的期待。
第二天早上,我看到昨晚群里的音讯,感觉有点奇怪,于是关上终端尝试更新 Pod 环境,工作运行后始终卡了很久。
依据运行日志,Pod 更新总共耗时 872S,其中“版本仲裁”过程耗时 810 秒。下一步,我切到旧版本进行比照测试,旧版本 ” 版本仲裁 ” 耗时 120S,其余阶段工夫差不多,也就是说新版本“版本仲裁”好转验证,相比旧版本耗时涨了 8 倍!
接着我应用二办法比照各个 commit,最初发现其中一个 commit 导致了“版本仲裁”变慢,它减少了几个模块的依赖,这会扭转主工程间接依赖的关系,从而扭转 CocoaPods 版本仲裁的搜寻程序。因为 CocoaPods 输入的日志不蕴含版本仲裁的过程,须要进一部剖析 Cocapods 的源码逻辑。
[CP cost] prepare:0.002s
[CP cost] resolve_dependencies:810.734s
[CP cost] download_dependencies:20.774s
…
[CP cost] Total:872.28s
剖析版本仲裁的底层逻辑
打印依赖抵触的搜寻门路
CocoaPods 版本仲裁性能基于 Molinillo 实现,须要剖析和调试 Molinillo 源码。不理解依赖仲裁工具的读者请先查看文末《附录 2: 依赖仲裁工具的职责》。
首现下载 Molinillo 和 CocoaPods 源码到本地门路,而后批改 Gemfile 文件的依赖申明,将版本依赖批改为本地门路依赖。
gem 'molinillo', :path=>'/.../Molinillo'
gem 'cocoapods', :path=>'/.../CocoaPods'
版本仲裁的入口代码在 resolution.rb 文件的 Resolver 函数,为了剖析仲裁时具体搜寻门路,我将仲裁过程解决的包名和未解决的需要数量都通过日志打印进去。
def resolve
# 初始化依赖图和依赖栈
start_resolution
while state
break if !state.requirement && state.requirements.empty?
indicate_progress
# 打印以后未解决的需要的数量
puts "BT:requirements.length" + state.requirements.length
# 打印以后需要的模块名
puts "BT:requirement" + state.requirement
if state.respond_to?(:pop_possibility_state) # DependencyState
state.pop_possibility_state.tap do |s|
if s
states.push(s)
activated.tag(s)
end
end
end
# 解决栈顶的模块申明
process_topmost_state
end
# 遍历依赖图
resolve_activated_specs
ensure
end_resolution
end
呈现抵触后 Cocopod 会调用 create_conflict 函数解决,我同样将仲裁过程解决的包名和未解决的需要数量都打印进去。
def create_conflict(underlying_error = nil)
vertex = activated.vertex_named(name)
locked_requirement = locked_requirement_named(name)
requirements = {}
unless vertex.explicit_requirements.empty?
requirements[name_for_explicit_dependency_source] = vertex.explicit_requirements
end
requirements[name_for_locking_dependency_source] = [locked_requirement] if locked_requirement
vertex.incoming_edges.each do |edge|
(requirements[edge.origin.payload.latest_version] ||= []).unshift(edge.requirement)
end
activated_by_name = {}
activated.each {|v| activated_by_name[v.name] = v.payload.latest_version if v.payload }
# 打印以后抵触的模块名
puts "BT:conflict_name" + name
# 打印以后未解决的抵触
puts "BT:conflict-requirements"
puts requirements
conflicts[name] = Conflict.new(
requirement,
requirements,
vertex.payload && vertex.payload.latest_version,
possibility,
locked_requirement,
requirement_trees,
activated_by_name,
underlying_error
)
end
比照好转前后的差别
接下来,我别离在新版本 7.42.0 和旧版本 7.41.0 执行 Pod 更新,而后比照两个版本仲裁过程的日志。依据试验后果,耗时次要集中在 Triver 的版本仲裁。
进行 Triver 仲裁时,Molinillo 先抉择最新版本 1.1.18.2,因为 1.1.18.2 会引发抵触,Molinillo 会从高版本开始逐渐向下抉择另一个版本,最终始终遍历到 1.0.14.23 才没有抵触。最终 7.41.0 版本环境的 Triver 版本仲裁总共用时 7 分钟;7.42.0 版本环境的 Triver 版本仲裁总共用时 1 分钟。
开始 | 完结 | 耗时 | |
---|---|---|---|
7.41.0 | [Triver (1.1.18.2)] 23:07:59 | [Triver (1.0.14.23)] 23:08:56 | 1 分钟 |
7.42.0 | [Triver (1.1.18.2)] 19:03:47 | [Triver (1.0.14.23)] 19:10:38 | 7 分钟 |
指数级好转的根本原因
好转的起因是 Triver/API 引发了依赖抵触。依赖图中有一个模块 Triver,它是被其余模块间接依赖的。Triver 有多个 subspec,其中有一个 subspec 是 Triver/API。Triver/API 依赖了 MtopSDK 模块,并申明了 MtopSDK 的最小版本。Triver 最新的版本是 1.1.18.2,它须要依赖 MtopSDK 2.5.1.0 以上的版本,而主工程 Podfile 申明 MtopSDK 为 2.2.2.3 的固定版本,因而呈现了依赖抵触。
仲裁变慢的 3 个因素
依赖抵触导致 56 次回溯查看
Triver 后面 56 个版本都会导致 MtopSDK 的版本抵触,那么 Molinillor 要进行 56 次回溯能力仲裁胜利。
Molinillor 进行版本仲裁时,会优先取新版本。如果新版本不满足条件,会按程序递加抉择低版本,直到版本束缚能匹配为止。Molinillor 开始递加匹配 Triver 的低版本,始终找到第 56 个版本才符合条件。Triver 的第 56 个版本是 1.0.14.23,它依赖 MtopSDK 的最小版本是 2.0.1.3,这个束缚和 Podfile 申明的 2.2.2.3 版本不抵触,因而 Triver 的仲裁的后果是 1.0.14.23。
CocoaPods Subspec 机制导致跨层级回溯
Triver/API 是 Subspec 形容的,它是 Triver 的一个子模块,Triver/API 的版本由 Triver 决定。当 Triver/API 产生抵触时,依赖图会先回溯到上一层 Triver,从新抉择另一个 Triver 的版本。
DFS 遍历导致回溯时大量反复查看
Molinillor 应用的遍历形式是 DFS(深度优先算法)。Molinillor 会构建一个依赖图,依赖图的每个节点代表一个模块。它会用 DFS 遍历依赖图的每个节点,对所有模块进行版本仲裁。当 Triver/API 产生抵触时,依赖图会先回溯到上一层 Triver,从新抉择 Triver 的版本。但 Triver 有 8 个子模块,如果 Triver/API 子模块遍历排序靠后,就须要期待其它子模块实现深度遍历。有些子模块比方 Triver/AppContainer,它依赖链路很长,深度遍历耗时会更久。
好转前后回溯复杂度比照
Triver/API 版本仲裁的工夫复杂度能够示意为 56mO(n),m 是 Triver 遍历子模块时 Triver/API 的遍历排序,n 是 Triver 子模块依赖树的节点数。
依据遍历过程的日志,好转前 Triver/API 遍历排序是 第 2 ,排在 Triver/ZCache 之后。好转后 Triver/API 遍历排序是 第 5 ,排在 Triver/AppContainer、Triver/ZCache、Triver/TinyShop、Triver/Monitor 之后。
好转前每次回溯的节点数是 6 个,好转后每次回溯的节点数量 24 个,Triver 的仲裁工夫也 从 1 分钟涨到 8 分钟。
优化办法
优化计划是在 Podfile 申明 Triver 固定版本,申明固定版本的模块不须要进行版本仲裁,从而防止依赖抵触后重复回溯搜寻消耗大量工夫。
iOS 版本仲裁算法 Molinillo
包管理器是古代编程语言一个重要的组成部分。包管理器的外围就是版本仲裁算法,即怎么确保每个安装包的版本能够满足所有的依赖需要。包管理器会先获取主工程间接依赖和传递依赖的所有包,而后找到所有依赖都满足的版本组合。
包管理器的仲裁策略差别很大,不过通仲裁策略都有各自的优缺点。js 很少简直没有依赖抵触,但有有驰名的 node_module 依赖天堂,Android 依赖编译不过,但运行时会各种莫名奇怪的 Crash,iOS 常常被讥笑因为依赖问题编译不过,但稳定性会更好。具体能够查看文末 《附录 1:不同语言版本仲裁策略的差别》
iOS 的包管理器是 Cocoapods,Cocoapods 的版本仲裁性能是 Molinillo 实现的,Molinillo 是老一代的版本仲裁算法,PubGrub 则是新一代的版本仲裁算法。老一代的版本仲裁算法有两个显著毛病,第一个毛病是版本抵触遍历效率差,另一个毛病是仲裁失败的谬误日志不清晰。本文结尾的案例就是踩到第一个问题的坑。
Molinillo 算法的外围是基于回溯 (Backtracking) 和 向前查看 (forward checking),如果有趣味理解 Molinillo 的代码设计能够查看 Molinillo 官网介绍,或者这篇源码解析文章。
上面介绍 Molinillo 仲裁的外围逻辑。如果以主工程作为根节点,所有依赖加起来会造成一个依赖图。每个结点都代表一个包,每个包有不同的版本,同一个包的不同版本申明的依赖可能不一样。Molinillo 应用深度遍历法遍历依赖图的每个包,每个包只抉择一个版本。因为每个版本的依赖会有差别,所以每次抉择都代表走了一条路。(如下图所示)
正如上文所剖析的仲裁变慢案例,遍历过程中,Molinillo 会构建一个 版本组合(a 1.0,b1.1,…..)。在依赖图的不同结点里,如果呈现了两个相悖的依赖束缚(a > 2.2,a = 1.8),就会产生依赖抵触。
依据下图所示,当子节点 C 呈现依赖抵触时,Molinillo 会回溯到它的父节点 B,从新抉择父结点 B 的另一个版本,而后从新遍历它的子节点。如果父结点有许多子节点,深度遍历其余子节点也带来 M 倍耗时,M 是深度遍历 B 子节点通过的所有节点数量。 父节点 B 的新版本可能申明了子节点 C 新的约束条件,这样就解决了子节点 C 的依赖抵触问题。
然而,有时候会呈现父结点结点多个版本都会导致子节点抵触,此时 Molinillo 会一直重选父结点的版本。这会带来 N 倍工作,N 是抉择的父节点版本数量。 最可怜的状况下,Molinillo 抉择父节点 B 的所有可用版本,后续子节点 C 都会有抵触。此时 Molinillo 会持续回溯到父节点 B 的父父节点 A,从新抉择父父节点 A 的新版本,再从新遍历它的子节点。此时 Molinillo 可能会反复进入死胡同,比方之前选过 B 1.3,当初又从新抉择一次。
新一代版本仲裁算法 Pubgrub
包治理版本仲裁是一个 NP-hard 问题,NP-hard 问题示意可能没有算法能够在所有状况下无效解决它。上文介绍了 iOS 采纳的 Molinillo 算法,它在依赖抵触是解决效率会比拟低。Pubgrub 的呈现就是为了解决仲裁效率低的问题,它在老一代版本仲裁的根底上进行优化,能够大幅晋升版本抵触时的解决效率,Pubgrub 也被称为新一代版本仲裁算法。
Pubgrub 提出了全新的抵触解决思路,想要理解所有细节的读者能够浏览作者的文章或者 Dart-lang 的文档,上面是我会解读 Pubgrub 外围的逻辑。
遇到版本抵触时,Pubgrub 会应用算法推导出版本抵触的根本原因,它用 Incompatibility(不兼容)来示意。上文有介绍过,版本抵触时仲裁工具会始终回溯父结点,而后从新遍历原走过的门路。从新遍历时,Pubgrub 会利用“Incompatibility”过滤掉会存在抵触的门路,从而防止再次进入死胡同。咱们能够了解为 Pubgrub 会利用抵触的关系,推导出一组不兼容的版本束缚,而后就利用这个不兼容束缚进行剪枝。
上面介绍 Pubgrub 优化的算法细节。Pubgrub 将包之间的版本依赖关系形象为 Term 和 Incompatibility 两个因素,Term 示意一个包的版本束缚,Incompatibility 示意一组不兼容的关系。形象为因素当前,Pubgrub 就能够不便地进行数学公式推导,从而把包之间简单的依赖关系演绎为简略的不兼容组合。
Term
Pubgrub 运行的根本单元是一个 Term,Term 代表一个对于包的申明,申明给定的包版本可能是对的或错的。例如,如果咱们抉择 foo 1.2.3,那么 foo ^1.0.0 就是真的 Term;如果咱们抉择 foo 2.3.4,那么 foo ^1.0.0 就是假的 Term。相同的,如果抉择了 foo 1.2.3,那么 not foo ^1.0.0 则为假,如果抉择了 foo 2.3.4 或者基本没有抉择 foo 版本,那 not foo ^1.0.0 则为真。
为了示意一组 Term 和一个 Term 的关系,Pubgrub 定义了 satisfies(满足)、contradicts(矛盾)、inconclusive(不确定是否满足)三个概念。
- satisfies: 给定一组 Terms S 和一个 Term t,当且仅当 S 是 t 的子集时,S 和 t 的关系能够示意为 S satisfies t,例如 {foo >=1.0.0, foo <2.0.0} satisfies foo ^1.0.0。
- contradicts: 给定一组 Terms S 和一个 Term t,当且仅当 S 和 t 齐全不相交时,S 和 t 的关系能够示意为 S contradicts t,例如 foo ^1.5.0 contradicts not foo ^1.0.0。
- inconclusive:给定一组 Terms S 和一个 Term t,当 S 是 t 的真超集时,S 和 t 的关系能够示意为 S inconclusive for t,例如 foo ^1.0.0 inconclusive for foo ^1.5.0。
Terms 也能够通过汇合合乎来示意并集:foo ^1.0.0 ∪ foo ^2.0.0 is foo >=1.0.0 <3.0.0. 交加:foo >=1.0.0 ∩ not foo >=2.0.0 is foo ^1.0.0. 差集:foo ^1.0.0 \ foo ^1.5.0 is foo >=1.0.0 <1.5.0 备注:以上采纳 ISO 31-11 规范符号进行汇合操作
Incompatibility
Pubgrub 定义了一个概念“incompatibility”,“incompatibility”示意一组不能齐全成立的 Terms。
例如, incompatibility {foo ^1.0.0, bar ^2.0.0} 示意 foo ^1.0.0 和 bar ^2.0.0 不兼容, 所以如果版本仲裁失去的解决方案里蕴含了 foo 1.1.0 和 bar 2.0.2,那这个解决方案是有效的。
上文介绍了,一组 Terms 和一个 Term 的关系有 satisfies、contradicts、inconclusive for。incompatibility 示意一组不能齐全成立的 Terms。“terms”和“incompatibility”有 4 个种关系。给定一个 incompatibility I,一组 terms S。如果 S 满足 I 中的每一项,咱们说 S satisfies I。如果 S 至多与 I 中的一项矛盾,那么 S 与 I contradicts。如果 S 满足除 I 项中除了仅有一项之外的所有项,并且对于仅有的这一项是不确定的,咱们说 S“almost satisfies”I,咱们仅有的这一项为“unsatisfied term”。
incompatibility 的起源是包的依赖申明。例如“foo ^1.0.0 依赖于 bar ^2.0.0”是一组依赖关系,它示意为 incompatibility 就是 {foo ^1.0.0, not bar ^2.0.0}。又例如主工程申明了依赖 foo <1.3.0,它示意为 incompatibility 就是 {not foo <1.3.0}。以上的 incompatibility 被称为“external incompatibility”,它们来自于 root 工程或包的依赖形容。
Pubgrub 遍历工程的依赖图会遇到海量的依赖关系,这些依赖关系会转化为大量的“external incompatibility”。如果“external incompatibility”以离散的个体存在,并不能帮组 Pubgrub 进步仲裁过程抉择版本的效率。反之,如果能够将离散的“external incompatibility”聚合成一个 incompatibility 组合,Pubgrub 就能够疾速判断哪些包的版本会产生抵触。
抵触解决期间,Pubgrub 会利用根底等式和汇合公式,将导致版本抵触的两个 incompatibility 推导为一个新的 incompatibility,聚合进去的 incompatibility 被称为“derived(派生的)incompatibility”,推导进去的“derived incompatibility”会做为包版本抉择的判断根据。
解决抵触期间,Pubgrub 会进行回溯并从新搜寻状态空间,Pubgrub 能够利用“terms”和“incompatibility”的关系,判断以后搜寻门路是否有问题,从而防止反复地搜寻状态空间里同一个死胡同。
Conflict Resolution
Pubgrub 会保护一个版本组合数组,记录遍历过程抉择的每个包和版本,仲裁胜利后这个数组就是解决方案。遍历时,Pubgrub 会校验以后包版本组合是否有不兼容,如果存在不兼容,阐明持续遍历会进入死胡同,放弃持续遍历下一级节点,从新抉择以后包的版本,直到没有不兼容为止。遍历实现后,以后包版本组合作为最终的解决方案。
这个算法能够防止仲裁工具反复走进同一个死胡同,大幅提高版本抵触时搜寻的效率。这就像地图软件提供的封路反馈性能,用户通过反馈互通信息,向地图软件反馈某段路走不通。当其用户再导航时,导航算法会主动避开这条死胡同。
上面介绍 Pubgrub 推导不兼容性的算法,要了解它的推导过程须要把握逻辑学的基础知识。
它应用一个根底等式:如果给定任何“(a or b) and (not a or c)”为真,那么能够推导出“(b or c)”也为真。而后将这个逻辑等式应用“不兼容性”概念来形容:如果给定任何“不兼容性{t,q} and 不兼容性{not t,r}”为真,那么能够推导出“不兼容性{q,r} 为真”。
在版本仲裁场景中,咱们能够将 t、q、r 了解为是某个包的版本束缚。理论场景中,包的束缚常常有差别,比方“包 A > 1.0”和“包 A > 2.0”,咱们能够将同一个包不同的束缚称为 t1、t2。
于是能够失去上面等式:给定任何“不兼容性{t1,q} and 不兼容性{t2,r}”为真,那么能够推导出“不兼容性{q,r,t1 ∪ t2} 为真”。如果加一个条件 “t1 不是 t2 的超集“,那就能够将论断简化为“不兼容性{q,r} 为真”。
举个例子:
上图是一个版本抵触的例子。root 工程申明了模块 M 的版本束缚,传递依赖链中,模块 C 也申明的“模块 M”的版本束缚。上面介绍一下 Pubgrub 的算法是怎么防止二次进入死胡同。
- 依据上图失去依赖条件 1:root 工程 依赖 模块 M =2.0。“依赖条件 1”能够转化为 不兼容性 1 {not“模块 M =2.0”, root}
- 依据上图失去依赖条件 2:“模块 C 小于等于 3.2 的版本都依赖”模块 M <1.5”。“依赖条件 2”能够转化为 不兼容性{not“模块 M <1.5”, 模块 C <=3.2},再推导为 不兼容性 2{模块 M >=1.5 , 模块 C <=3.2}
- 依据上图失去依赖条件 3:“模块 B 1.3”依赖于”模块 C <3.2“,能够转化为 不兼容性{not“模块 C <3.2”, 模块 B =1.3},再推导为 不兼容性 3{模块 C >3.2, 模块 B =1.3}
依据根底等式,能够将 不兼容性 1 和不兼容性 2 推导为“不兼容性{not”模块 M =2.0“∪ 模块 M >=1.5,root, 模块 C <=3.2}”,再简化失去 不兼容性 4{root, 模块 C <=3.2}
已知 不兼容性 4 和不兼容性 3,依据根底等式能够推导出不兼容性{模块 C >3.2 ∪ 模块 C >3.2,root, 模块 B =1.3}, 简化失去 => 不兼容性 5{root, 模块 B =1.3}
有了 不兼容性 5{root, 模块 B =1.3},Pubgrub 从新搜寻门路时就不会抉择模块 B 的 1.3 版本,从防止第二次走进死胡同。
iOS 包治理最佳实际
1、主工程 Podfile 治理中间件和三方库
Triver 是阿里团体的一个中间件,如果中间件和三库在 Podfile 申明具体版本,就能够加重 Molinillo 的仲裁的压力,使得版本仲裁速度保持稳定。
2、外部模块只申明依赖不申明版本束缚
大型项目的性能简单,壳工程会依赖大量外部和内部的 SDK。alibaba iOS 工程总共有 140 的外部模块,300 多个团体或第三方的模块。团队保护外部模块,模块之间相依赖会比拟多。如果模块的依赖过多限度版本范畴,很容易造成版本抵触。最佳实际是模块依赖不容许申明固定版本,只容许申明大于某个版本。
3、主工程 Podfile 申明所有模块的固定版本
很多我的项目习惯申明 module>xxx 版本,这样每次都会下载最新的版本。咱们很难保障三方库模块治理十分严格,每次都是兼容性降级,像这样频繁降级容易工程环境会稳固。结果也很重大,轻则工程编译不过,重则呈现线上问题。
除此之外,为了晋升编译速度,iOS 的模块通常会做成动态库,壳工程构建时不须要编译模块的代码,只须要链接动态库。OC 的二进制格局是 Mach-O,Mach- O 文件只记录类的符号,不记录函数的符号。如果模块 A 调用了模块 B 的函数 X,函数 X 被删掉后,主工程工程构建不会报错,但运行时会 crash。因而,如果一个模块申明了含糊的版本限定,版本会被主动降级,如果降级了不兼容的版本,会带来不确定的危险。
总结
本文介绍了 Cocopods 版本仲裁的问题,当开发者更新 cocopods 环境时,如果呈现版本抵触,Cocopods 版本仲裁的速度会很慢。当某个包申明的版本束缚和其余节点抵触,Cocopods 回溯到上父节点的包,而后 DFS 搜寻父节点所有可用版本,直到绕开子节点的版本抵触抵触为止。如果父节点的可用版本都不符合条件,还须要持续回溯到父节点的父节点,顺次类推直到搜寻完依赖图的所有可能性。大型工程的依赖图异样简单,包的数量有四五百个,每个包有几十个版本,每个版本的差别又很大,遇到简单的场景时回溯搜寻会很慢。
基于此,本文还介绍了新一代的版本仲裁算法 Pubgrub,Pubgrub 的呈现就是为了解决上一代版本仲裁算法效率低的问题。Pubgrub 目前曾经利用到 Dart 和 SwiftPM 的包治理中,Pubgrub 作者设计了全新的算法,能够无效防止依赖检索过程反复进入死胡同,进而大幅度晋升版本仲裁的效率。iOS 开发者会常常更新 cocopods,如果这个过程很慢,会重大侵害团队的开发体验和开发效率。
最初,本文介绍了一种包管理策略,应用这种策略能够加重 Cocopods 版本仲裁的工作,从而防止陷入版本抵触的死胡同里。这个策略有三步,第一步是禁止在团队公有 SDK 申明依赖包的版本束缚;第二步是主工程的 Podfile 文件申明所有的依赖包版本束缚;第三步是 Podfile 只申明包的固定版本,不申明包区间版本。
附录 1:不同语言版本仲裁策略的差别
iOS 的包管理工具是 Cocopods,Cocopods 采纳严格模式,不容许任何模式的版本抵触。Cocopods 发现依赖抵触立马报错并进行下载模块,期待开发者解决抵触后能力从新持续。这种策略能够躲避运行时的危险。但它却减少了工程治理的老本,如果工程的版本申明凌乱,编译时很容易报错。
Android 的包管理工具是 Maven,Maven 对依赖抵触有更高的容忍度。Maven 工程如果呈现依赖抵触,它会依据最小门路的形式抉择模块版本. 这种策略能够防止编译时的谬误,开发不须要花工夫解决依赖抵触。但它减少了运行时的稳定性危险,运行时可能会有执行到不存在的符号,最初报 NoSuchMethodError 谬误。
下图是 Mave 的最小门路准则策略:
前端罕用的包管理工具是 npm,前端开发从来不会遇到包抵触的问题。npm 利用语言个性实现依赖包隔离,这是一种冗余换取稳固的策略。当 npm 工程里呈现传递依赖抵触时,各个节点会保留本人依赖的版本。这种策略能够防止依赖仲裁的抵触谬误,运行时稳定性也高。但它会导致依赖天堂,包大小也会收缩。
下图是 npm 的依赖冗余策略:
附录 2:依赖仲裁工具的职责
各技术栈包管理工具的依赖仲裁算法不一样,Cocopod 应用 Molinillo 进行依赖仲裁,Dart 和 SwiftPM 用应用的是 PubGrub。要理解依赖仲裁变慢的具体起因,须要剖析 Molinillo 的源码。在此之前,先简略回顾一下依赖仲裁工具的职责。依赖仲裁工具次要有两个职责,一个是判断依赖循环,另一个是找到没有抵触的模块版本组合。
判断依赖循环
包管理工具无奈解决带有循环依赖的工程,所以它须要判断工程中是否存在循环会依赖。包管理工具会对工程依赖做数学建模,建模后会造成一个依赖图,而后判断这个依赖图是否 DAG。
找到没有抵触的依赖组合
举个简略的例子例子,App 申明模块 M 是 2.6 版本,而后又通过模块 A 间接依赖了模块 B。因为模块 B 没有申明具体版本,Cocoapods 抉择了模块 B2.0 版本,但模块 B 的 2.0 版本依赖 3.2 以上的模块 M 版本,这个签名 App 申明的 2.6 版本抵触了,因而不能抉择模块 B3.2 版本。Cocoapods 会从新抉择模块 B 其余版本,最初发现模块 B1.0 版本没有抵触。
参考资料
- Pubgrub 官网文档:https://github.com/dart-lang/…
- Molinillo 官网文档:https://github.com/CocoaPods/…
- Molinillo 依赖校验源码解析:https://looseyi.github.io/pos…
- 罕用汇合符号:https://www.shuxuele.com/sets…
关注【阿里巴巴挪动技术】微信公众号,每周 3 篇挪动技术实际 & 干货给你思考!