iOS Pod Update 指数级变慢?看 Flutter 新一代仲裁算法 Pubgrub 如何解

原创
2021/12/02 20:11
阅读数 2.6K

作者:洪尉(洪茶)

如果你是一名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版本没有冲突。

参考材料

关注【阿里巴巴移动技术】微信公众号,每周 3 篇移动技术实践&干货给你思考!

展开阅读全文
打赏
0
1 收藏
分享
加载中
更多评论
打赏
1 评论
1 收藏
0
分享
返回顶部
顶部