作者:洪尉(洪茶)
如果你是一名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_specsensure end_resolutionend
呈现抵触后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 篇挪动技术实际&干货给你思考!