头图

Author: Hong Wei (Hong Cha)

If you are an iOS programmer, or you are interested in package management technology, I recommend you to read this article. You can understand the underlying principles of iOS version arbitration, its potential performance risks, and how to prevent Pod update from deteriorating, so that you have a deeper understanding of CocoaPods. In addition, you can also learn about Pubgrub, a new generation of version arbitration algorithm applied in Flutter, and the differences in management strategies of different technology stacks, so as to have a more comprehensive understanding of the field of package management technology.

Pod Update is 8 times slower! ! !

On Friday night, Shuaishuai asked for help in the iOS group: "My main project ran Pod update, and it was stuck. Has anyone encountered it? It's strange! The CPU is full of Ruby processes, but there are no network requests." Xiaoming saw After taking the screenshot of Shuaishuai, he replied: "I have encountered it. This is normal. CocoaPods is doing dependency processing." Shuai Shuai had no choice but to accept the long wait.

The next morning, I saw the news from the group last night and found it a bit strange, so I opened the terminal and tried to update the Pod environment. The task was stuck for a long time after running.

According to the running log, the Pod update took a total of 872S, of which the "version arbitration" process took 810 seconds. In the next step, I switched to the old version for comparative testing. The "version arbitration" of the old version took 120S, and the other phases took about the same time. That is to say, the new version of the "version arbitration" deteriorated verification, which took 8 times longer than the old version!

Then I used the two methods to compare the various commits, and finally found that one of the commits caused the "version arbitration" to slow down. It increased the dependency of several modules, which would change the relationship between the indirect dependencies of the main project, and thus change the search order of the CocoaPods version arbitration. . Because the log output by CocoaPods does not include the version arbitration process, it is necessary to further analyze the source code logic of Cocapods.

[CP cost] prepare :0.002s

[CP cost] resolve_dependencies :810.734s

[CP cost] download_dependencies :20.774s

...

[CP cost] Total :872.28s

Analyze the underlying logic of version arbitration

Print search path for dependency conflicts

The arbitration function of CocoaPods version is based on Molinillo, and Molinillo source code needs to be analyzed and debugged. Readers who are not familiar with relying on arbitration tools, please check the end of the article " Appendix 2: Responsibilities of relying on arbitration tools" .

First download the source code of Molinillo and CocoaPods to the local path, then modify the dependency statement of the Gemfile file, and modify the version dependency to the local path dependency.

gem 'molinillo', :path=>'/.../Molinillo'
gem 'cocoapods', :path=>'/.../CocoaPods'

The entry code for version arbitration is in the Resolver function of the resolution.rb file. In order to analyze the detailed search path during arbitration, I printed out the package names processed by the arbitration process and the number of unprocessed requirements through the log.

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

After a conflict occurs, Cocopod will call the create_conflict function to handle it. I also print out the package name processed by the arbitration process and the number of unprocessed requirements.

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

Compare the difference before and after deterioration

Next, I performed Pod updates on the new version 7.42.0 and the old version 7.41.0 respectively, and then compared the logs of the arbitration process of the two versions. According to the experimental results, the mainly concentrated on Triver's version arbitration .

When conducting Triver arbitration, Molinillo first chooses the latest version 1.1.18.2, because 1.1.18.2 will cause conflicts, Molinillo will gradually select another version starting from the higher version, and finally traverse to 1.0.14.23 until there is no conflict. The final Triver version arbitration in the 7.41.0 version environment took a total of 7 minutes; the Triver version arbitration in the 7.42.0 version environment took a total of 1 minute.

StartFinishtime consuming
7.41.0[Triver (1.1.18.2)] 23:07:59[Triver (1.0.14.23)] 23:08:561 minute
7.42.0[Triver (1.1.18.2)] 19:03:47[Triver (1.0.14.23)] 19:10:387 minutes

The root cause of exponential deterioration

The reason for the deterioration is that Triver/API caused a dependency conflict. There is a module Triver in the dependency graph, which is indirectly dependent on other modules. Triver has multiple subspecs, one of which is Triver/API. Triver/API relies on the MtopSDK module and declares the minimum version of MtopSDK. The latest version of Triver is 1.1.18.2, which needs to rely on MtopSDK 2.5.1.0 or higher, and the main project Podfile states that MtopSDK is a fixed version of 2.2.2.3, so a dependency conflict has occurred.

3 factors that slow down arbitration

Dependency conflicts led to 56 retrospective inspections

The previous 56 versions of Triver will cause the version conflicts of MtopSDK, so Molinillor will have to perform 56 backtrackings to arbitrate successfully.

When Molinillor conducts version arbitration, the new version will be taken first. If the new version does not meet the conditions, the lower version will be selected in descending order until the version constraints can be matched. Molinillor began to match the lower version of Triver in descending order, until the 56th version was found to meet the conditions. The 56th version of Triver is 1.0.14.23, and the minimum version that it relies on MtopSDK is 2.0.1.3. This constraint does not conflict with version 2.2.2.3 declared by Podfile, so the result of Triver's arbitration is 1.0.14.23.

CocoaPods Subspec mechanism leads to cross-level backtracking

Triver/API is described by Subspec, it is a sub-module of Triver, and the version of Triver/API is determined by Triver. When a Triver/API conflict occurs, the dependency graph will first go back to the previous Triver and select another Triver version.

DFS traversal leads to a large number of repeated checks during backtracking

The traversal method used by Molinillor is DFS (Depth First Algorithm). Molinillor will build a dependency graph, and each node of the dependency graph represents a module. It will use DFS to traverse each node of the dependency graph and arbitrate the versions of all modules. When there is a conflict between Triver/API, the dependency graph will first go back to the previous level of Triver, and reselect the version of Triver. But Triver has 8 sub-modules. If the Triver/API sub-module traverses the sequence later, you need to wait for other sub-modules to complete the deep traversal. Some sub-modules, such as Triver/AppContainer, rely on a very long link, and the depth traversal will take longer.

Comparison of retrospective complexity before and after deterioration

The time complexity of Triver/API version arbitration can be expressed as 56 m O(n), where m is the traversal order of Triver/API when the Triver traverses the sub-modules, and n is the number of nodes in the tree that the Triver sub-module depends on.

According to the log of the traversal process, the Triver/API traversal order before the deterioration is , 2nd , and ranks after Triver/ZCache. After the deterioration, the Triver/API traversal ranking is , which ranks after Triver/AppContainer, Triver/ZCache, Triver/TinyShop, Triver/Monitor.

The number of nodes backtracking each time before the deterioration is 6, and the number of nodes backtracking each time after the deterioration is 24. The arbitration time of also increased from 1 minute to 8 minutes .

Optimization

The optimization plan is to declare a fixed version of Triver in the Podfile, and declare that the fixed version of the module does not require version arbitration, thereby avoiding repeated backtracking searches after dependency conflicts that consume a lot of time.

The iOS version of the arbitration algorithm Molinillo

The package manager is an important part of modern programming languages. The core of the package manager is the version arbitration algorithm, that is, how to ensure that the version of each installation package can meet all dependency requirements. The package manager will first get all the packages that the main project directly depends on and transitively depend on, and then find a version combination that satisfies all dependencies.

The arbitration strategies of package managers are very different, but all arbitration strategies have their own advantages and disadvantages. js rarely has almost no dependency conflicts, but there is a famous node_module dependency hell. Android depends on compiling, but there will be all kinds of strange Crash at runtime. iOS is often mocked because of dependency problems but not compiling, but the stability will be better. "Appendix 1: Differences in Arbitration Strategies in Different Language Versions" at the end of the

The iOS package manager is Cocoapods, and the version arbitration function of Cocoapods is implemented by Molinillo. Molinillo is the old generation version arbitration algorithm, and PubGrub is the new generation version arbitration algorithm. The old generation version of the arbitration algorithm has two obvious shortcomings. The first shortcoming is the poor traversal efficiency of version conflicts, and the other shortcoming is the unclear error log of arbitration failure. The case at the beginning of this article is the pit of stepping on the first problem.

The core of the Molinillo algorithm is based on backtracking and forward checking. If you are interested in understanding Molinillo's code design, you can check Molinillo's official introduction , or this source code analysis article .

The core logic of Molinillo arbitration is introduced below. If the main project is the root node, all dependencies will add up to form a dependency graph. Each node represents a package, and each package has a different version. Different versions of the same package may declare different dependencies. Molinillo uses a deep traversal method to traverse each package in the dependency graph, and only select one version for each package. Because the dependencies of each version will be different, each choice represents a way to go. (As shown below)

Just like the slow arbitration case analyzed above, during the traversal process, Molinillo will build a version combination (a 1.0, b1.1,...). In the different nodes of the dependency graph, if there are two conflicting dependency constraints (a> 2.2, a = 1.8), a dependency conflict will occur.

According to the figure below, when the child node C has a dependency conflict, Molinillo will backtrack to its parent node B, re-select another version of the parent node B, and then traverse its child nodes again. If the parent node has many child nodes, deep traversal of other child nodes will also bring M times the time, and M is the number of all nodes through which the child node of B is deeply traversed. parent node B may declare new constraint conditions for child node C, thus solving the dependency conflict problem of child node C.

However, sometimes multiple versions of the parent node will cause conflicts between the child nodes. At this time, Molinillo will continue to reselect the version of the parent node. This will bring N times the work, where N is the number of selected parent node versions. most unfortunate case, Molinillo selects all available versions of the parent node B, and subsequent child nodes C will have conflicts. At this point Molinillo will continue to trace back to the parent node A of the parent node B, re-select the new version of the parent node A, and re-traverse its child nodes. At this time, Molinillo may repeatedly enter a dead end, such as choosing B 1.3 before, and now choosing it again.

New generation version of arbitration algorithm Pubgrub

The package management version arbitration is an NP-hard problem. The NP-hard problem means that there may not be an algorithm that can effectively solve it in all situations. The Molinillo algorithm adopted by iOS is introduced above, which is relatively inefficient in dealing with dependency conflicts. Pubgrub appeared to solve the problem of low arbitration efficiency. It is optimized on the basis of the old generation version arbitration, which can greatly improve the processing efficiency of version conflicts. Pubgrub is also known as the new generation version arbitration algorithm.

Pubgrub put forward new ideas to solve the conflict, readers want to know all the details can be read author's articles or document Dart-lang of , Here is what I would interpret Pubgrub core logic.

When encountering a version conflict, Pubgrub will use an algorithm to deduce the root cause of the version conflict, which is represented by Incompatibility (incompatibility). As mentioned above, when the version conflicts, the arbitration tool will always go back to the parent node, and then re-traverse the original path. When traversing again, Pubgrub will use "Incompatibility" to filter out conflicting paths, so as to avoid entering a dead end again. We can understand that Pubgrub will use the conflicting relationship to derive a set of incompatible version constraints, and then use this incompatible constraint for pruning.

The following describes the details of Pubgrub optimization algorithm. Pubgrub abstracts the version dependency between packages into two elements, Term and Incompatibility. Term represents the version constraint of a package, and Incompatibility represents a set of incompatible relationships. After being abstracted into elements, Pubgrub can conveniently derive mathematical formulas, thereby reducing the complex dependencies between packages into simple incompatible combinations.

Term

The basic unit of Pubgrub operation is a Term. Term represents a statement about a package, stating that a given package version may be right or wrong. For example, if we choose foo 1.2.3, then foo ^1.0.0 is a real Term; if we choose foo 2.3.4, then foo ^1.0.0 is a fake Term. Conversely, if foo 1.2.3 is selected, then not foo ^1.0.0 is false, and if foo 2.3.4 is selected or the foo version is not selected at all, not foo ^1.0.0 is true.

In order to express the relationship between a set of Term and a Term, Pubgrub defines three concepts: satisfaction, contradicts, and inconclusive.

  • satisfies: Given a set of Terms S and a Term t, if and only if S is a subset of t, the relationship between S and t can be expressed as S satisfies t, for example {foo >=1.0.0, foo < 2.0.0} satisfies foo ^1.0.0.
  • contradicts: Given a set of Terms S and a Term t, if and only if S and t are completely disjoint, the relationship between S and t can be expressed as S contradicts t, such as foo ^1.5.0 contradicts not foo ^1.0 .0.
  • inconclusive : Given a set of Terms S and a Term t, when S is a true superset of t, the relationship between S and t can be expressed as S inconclusive for t, such as foo ^1.0.0 inconclusive for foo ^1.5. 0.
Terms can also be expressed as union by set conformity: foo ^1.0.0 ∪ foo ^2.0.0 is foo >=1.0.0 <3.0.0. Intersection: foo >=1.0.0 ∩ not foo >=2.0.0 is foo ^1.0.0. Difference: foo ^1.0.0 \ foo ^1.5.0 is foo >=1.0.0 <1.5.0 Remarks: The above uses ISO 31-11 standard symbols for collective operations

Incompatibility

Pubgrub defines a concept "incompatibility", "incompatibility" represents a set of Terms that cannot be fully established.

For example, incompatibility (foo ^1.0.0, bar ^2.0.0) means that foo ^1.0.0 and bar ^2.0.0 are incompatible, so if the solution obtained by version arbitration includes foo 1.1.0 and bar 2.0. 2. Then this solution is invalid.

As mentioned above, the relationship between a set of Terms and a Term includes satisfaction, contradicts, and inconclusive for. Incompatibility represents a set of Terms that cannot be fully established. "Terms" and "incompatibility" have four relationships. Given an incompatibility I, a set of terms S. If S satisfies every item in I, we say S satisfies I. If S contradicts at least one of I, then S contradicts I. If S satisfies all items except only one item in I, and the only item is uncertain, we say S "almost satisfies" I, and the only item we have is "unsatisfied term ".

The source of incompatibility is the dependency declaration of the package. For example, "foo ^1.0.0 depends on bar ^2.0.0" is a set of dependencies, which means incompatibility is {foo ^1.0.0, not bar ^2.0.0}. Another example is that the main project declares the dependency foo <1.3.0, which means that incompatibility is {not foo <1.3.0}. The above incompatibility is called "external incompatibility", and they come from the dependency description of the root project or package.

Pubgrub traverses the dependency graph of the project will encounter a large number of dependencies, and these dependencies will be transformed into a large number of "external incompatibility". If "external incompatibility" exists as discrete individuals, it will not help Pubgrub to improve the efficiency of the arbitration process to choose a version. Conversely, if the discrete "external incompatibility" can be aggregated into an incompatibility combination, Pubgrub can quickly determine which package versions will conflict.

During conflict resolution, Pubgrub will use basic equations and set formulas to derive the two incompatibility that caused the version conflict into a new incompatibility. The aggregated incompatibility is called "derived (derived) incompatibility", and the derived "derived" "incompatibility" will be used as the basis for judging the choice of package version.

During conflict resolution, Pubgrub will backtrack and re-search the state space. Pubgrub can use the relationship between "terms" and "incompatibility" to determine whether there is a problem with the current search path, thereby avoiding repeated searches for the same dead end in the state space.

Conflict Resolution

Pubgrub maintains an array of version combinations to record each package and version selected during the traversal process. After the arbitration is successful, this array is the solution. When traversing, Pubgrub will check whether the current package version combination is incompatible. If there is an incompatibility, it means that continuing to traverse will enter a dead end, giving up continuing to traverse the next level of nodes, and reselecting the current package version until there is no incompatibility. After the traversal is complete, the current package version combination is used as the final solution.

This algorithm can prevent the arbitration tool from repeatedly walking into the same dead end, and greatly improve the search efficiency in case of version conflicts. This is like the road closure feedback function provided by map software. Users can feed back to the map software that a certain section of the road is unreachable through feedback and exchange of information. When its users navigate again, the navigation algorithm will automatically avoid this dead end.

The following introduces Pubgrub's derivation incompatibility algorithm . To understand its derivation process, you need to master the basic knowledge of logic.

It uses a basic equation: if any given "(a or b) and (not a or c)" is true, then it can be deduced that "(b or c)" is also true. Then this logical equation is described by the concept of "incompatibility": if any "incompatibility {t,q} and incompatibility {not t,r}" is given as true, then "incompatibility" can be deduced Nature {q,r} is true".

In the version arbitration scenario, we can understand t, q, and r as the version constraints of a certain package. In actual scenarios, there are often differences in package constraints, such as "Package A> 1.0" and "Package A> 2.0". We can call different constraints of the same package t1 and t2.

Then the following equation can be obtained: Given that any "incompatibility {t1,q} and incompatibility {t2,r}" is true, then it can be deduced that "incompatibility {q,r,t1 ∪ t2} is real". If we add a condition "t1 is not a superset of t2", the conclusion can be simplified to "incompatibility {q,r} is true".

for example:

The figure above is an example of a version conflict. The root project declares the version constraint of module M. In the transitive dependency chain, module C also declares the version constraint of "module M". Let's introduce how Pubgrub's algorithm avoids entering a dead end twice.

  • According to the above figure, the dependency condition 1: The root project depends on the module M=2.0. "Dependency 1" can be transformed into Incompatibility 1 {not "Module M=2.0", root}
  • According to the above figure, the dependency condition 2: "Module C is less than or equal to 3.2 versions are dependent on" Module M<1.5". "Dependency 2" can be converted into incompatibility {not "Module M<1.5", Module C<=3.2 }, and then deduced as incompatibility 2{module M>=1.5, module C<=3.2}
  • According to the above figure, the dependency condition 3: "Module B 1.3" depends on "Module C<3.2", which can be converted into incompatibility {not "Module C<3.2", Module B=1.3}, and then deduced as incompatibility 3. {Module C>3.2, Module B=1.3}

According to the basic equation, incompatibility 1 and incompatibility 2 can be deduced as "incompatibility {not" module M=2.0" ∪ module M>=1.5,root,module C<=3.2}", and then simplified to get Incompatibility 4{root,module C<=3.2}

Known incompatibility 4 and incompatibility 3, according to the basic equation can be derived incompatibility {module C>3.2 ∪ module C>3.2, root, module B=1.3}, simplified to get => incompatibility 5{ root, module B=1.3}

With the incompatibility 5{root,module B=1.3}, Pubgrub will not select the 1.3 version of module B when re-searching the path, so as to avoid going into a dead end for the second time.

Best Practices for iOS Package Management

1. Main project Podfile management middleware and tripartite library

Triver is a middleware of Alibaba Group. If the middleware and Sanku declare specific versions in Podfile, it can reduce the pressure of Molinillo's arbitration and keep the speed of version arbitration stable.

2. The internal module only declares dependencies and does not declare version constraints

The functions of large-scale projects are complex, and shell projects rely on a large number of internal and external SDKs. The alibaba iOS project has a total of 140 internal modules and more than 300 group or third-party modules. The team maintains internal modules, and there will be more dependencies between modules. If too many module dependencies limit the version range, it is easy to cause version conflicts. The best practice is that module dependencies are not allowed to declare a fixed version, only a version greater than a certain version is allowed.

3. The main project Podfile declares the fixed version of all modules

Many projects are used to declaring the module>xxx version so that the latest version will be downloaded every time. It is difficult for us to guarantee that the management of the third party library module is very strict, and every time it is a compatibility upgrade, it is easy to upgrade frequently like this and the engineering environment will be stable. The consequences are also very serious, ranging from engineering compilation, to online problems.

In addition, in order to improve the compilation speed, iOS modules are usually made into static libraries. When the shell project is built, the code of the module does not need to be compiled, only the static library needs to be linked. The binary format of OC is Mach-O. The Mach-O file only records the symbols of the classes, not the symbols of the functions. If module A calls function X of module B, and function X is deleted, the main project construction will not report an error, but it will crash during runtime. Therefore, if a module declares a vague version limit, the version will be automatically upgraded. If an incompatible version is upgraded, it will bring uncertain risks.

Summarize

This article introduces the issue of Cocopods version arbitration. When developers update the cocopods environment, if there is a version conflict, the speed of Cocopods version arbitration will be very slow. When the version constraint declared by a package conflicts with other nodes, Cocopods traces back to the package of the parent node, and then DFS searches for all available versions of the parent node until the version conflict conflict of the child node is bypassed. If the available version of the parent node does not meet the conditions, it is necessary to continue backtracking to the parent node of the parent node, and so on until all the possibilities of the dependency graph are searched. The dependency graph of a large project is extremely complex. There are four to five hundred packages, and each package has dozens of versions. The differences between each version are very large. When encountering complex scenes, the backtracking search will be very slow.

Based on this, this article also introduces the new generation version of the arbitration algorithm Pubgrub, which appeared to solve the problem of low efficiency of the previous generation version of the arbitration algorithm. Pubgrub has been applied to the package management of Dart and SwiftPM. Pubgrub authors have designed a new algorithm, which can effectively avoid relying on the retrieval process to repeatedly enter the dead end, thereby greatly improving the efficiency of version arbitration. iOS developers will frequently update cocopods. If this process is slow, it will seriously damage the team’s development experience and development efficiency.

Finally, this article introduces a package management strategy, using this strategy can reduce the work of Cocopods version arbitration, so as to avoid falling into the dead end of version conflicts. This strategy has three steps. The first step is to prohibit the declaration of dependent package version constraints in the team’s private SDK; the second step is to declare all dependent package version constraints in the Podfile of the main project; the third step is to declare only the fixed version of the package. The package interval version is not declared.

Appendix 1: Differences in Arbitration Strategies in Different Language Versions

The iOS package management tool is Cocopods. Cocopods adopts strict mode and does not allow any form of version conflict. Cocopods immediately reports an error when discovering a dependency conflict and stops downloading the module, waiting for the developer to resolve the conflict before continuing. This strategy can avoid runtime risks. But it increases the cost of project management. If the version declaration of the project is confused, it is easy to report errors during compilation.

Android's package management tool is Maven, and Maven has a higher tolerance for dependency conflicts. If there is a dependency conflict in a Maven project, it will select the module version according to the minimum path. This strategy can avoid compile-time errors, and development does not need to spend time dealing with dependency conflicts. But it increases the risk of stability at runtime, there may be symbols that do not exist at runtime, and finally report NoSuchMethodError.

The figure below is Mave's minimum path principle strategy:

The commonly used package management tool for the front-end is npm, and front-end development never encounters the problem of package conflicts. Npm uses language features to achieve dependency package isolation, which is a strategy of redundancy in exchange for stability. When there is a transitive dependency conflict in the npm project, each node will keep the version it depends on. This strategy can avoid conflict errors that rely on arbitration and has high runtime stability. But it will lead to dependency hell and the package size will swell.

The following figure shows npm's dependency redundancy strategy:

Appendix 2: Responsibilities of relying on arbitration tools

The dependency arbitration algorithm of various technology stack package management tools is different. Cocopod uses Molinillo for dependency arbitration, and Dart and SwiftPM use PubGrub. To understand the specific reasons for the slowness of relying on arbitration, it is necessary to analyze the source code of Molinillo. Before that, let’s briefly review the responsibilities of relying on arbitration tools. The dependency arbitration tool has two main responsibilities, one is to judge the dependency cycle, and the other is to find a combination of module versions that do not conflict.

Judgment dependent cycle

The package management tool cannot handle projects with circular dependencies, so it needs to determine whether there are circular dependencies in the project. The package management tool will mathematically model the project dependencies. After the modeling, a dependency graph will be formed, and then it will be judged whether the dependency graph is DAG.

Found no conflicting dependency combination

To give a simple example, App declares that module M is version 2.6, and then indirectly depends on module B through module A. Because module B did not declare a specific version, Cocoapods chose module B2.0 version, but the 2.0 version of module B depends on module M version 3.2 or higher. The 2.6 version declared by this signed App conflicts, so module B3.2 version cannot be selected. Cocoapods will re-select other versions of module B, and finally find that there is no conflict in module B1.0 version.

reference material

, 3 mobile technology practices & dry goods for you to think about every week!


阿里巴巴终端技术
336 声望1.3k 粉丝

阿里巴巴移动&终端技术官方账号。