如果你的 iOS 项目是使用 Development Pods 来做组件化的话,这篇文章或许值得一看。

起源

知乎的 iOS 项目大概在 2016 年 Q4 开始进行组件化的工作,当时的计划就是把主 target 内的文件以 Development Pods 的形式拆分为 Basic,Core,Middleware,Module 四种类别的 Development Pods 供主 target 进行依赖。

这几层结构之间有下面这个下面这个约束关系,Pods 只能依赖比它们层级低的而不能依赖比它们层级高的,而且同层间也尽量避免依赖关系。换名话说,Core 可以依赖 Basic 但是 Basic 不能依赖 Core。这样做的原因很简单,就是避免循环引用的问题。

组件化的目的之一是减少依赖,可以让一个组件独立于主 target 之外独立运行,这样为了这个组件建立单独的工程,独立编译、开发,提升效率。

为某个 Development Pods 创建独立工程

但是当我为一个组件单独创建工程的时候,遇到了一个问题,姑且把它叫作组件 R 吧。作为一个 Pod,R 的依赖关系是由 R.podspec 体现的,它只需要关心它直接依赖的东西就好了,对于它依赖的依赖它是不需要关心的,Cocoapods 已经帮我们处理好了。

1
2
3
4
5
6
7
Pod::Spec.new do |s|
s.name = "R"
# some other fields...
s.dependency 'A'
s.dependency 'C'
s.dependency 'E'
end

比如,上图的 R 依赖了 A、C、E 三个 Pods。

为 R 创建依赖独立工程的时候,需要把 R 作为这个工程的一个依赖,使用路径的形式引入。

1
2
3
4
5
# Podfile
target 'Example' do
pod 'R', :path => '../../R',
end

如果你以为这样写就大功告成了,就大错特错了。执行 pod install 后就会发现 未能找到 A 类似的错误。为什么会出现这样的问题呢?其实我也不知道具体的原因😂,但是一个可能的猜测是
Podfile 中的依赖关系必须要明确,尤其是这些 pods 都是以本地路径关系相互依赖的情况下。而 podspec 中的依赖不需要那么明确,Cocoapods 会帮你处理。

因为我们使用 Development Pods,所以所有的 Pods 必然以本地路径的关系进行依赖。所以在为 R 创建独立工程的时候,需要把 R 依赖的所有 Pods 的路径标记出来给 CocoaPods 看。所以你可能会写出下面的 Podfile

1
2
3
4
5
6
target 'Example' do
pod 'R', :path => '../../R'
pod 'A', :path => '../../A'
pod 'C', :path => '../../C'
pod 'E', :path => '../../E'
end

满欣欢喜地跑去 pod install ,结果又失败了,原来 C 还依赖了 B!于是你又跑去给 podfile 加了一行,

1
2
3
4
5
6
7
target 'Example' do
pod 'R', :path => '../../R'
pod 'A', :path => '../../A'
pod 'B', :path => '../../B'
pod 'C', :path => '../../C'
pod 'E', :path => '../../E'
end

好吧,你又失败了,E 还依赖了 D…… Stop! 如果总是这样一次又一次 pod install 才能知道自己全部依赖的 Pods 有什么是不是太搓了?如果依赖少还好,但是对于依赖很多情况,这样一次次处理就有点 2 了。能不能有什么别的方法能一次性知道某个 Development Pod 的全部依赖呢?

答案是可以的,感兴趣的可以直接移步这里 X140Yu/development-pods-dependency-checker

Development Pods Dependency Checker

效果在 repo 里的 README 就可以看到了,它能找出工程中的所有依赖,并且把每个 Pod 的依赖展开,显示出它的全部依赖,而不是 podspec 中的那一小部分。

它的原理很简单,在 pod install 成功之后, Pods/Local Podspecs 目录中会出现一堆 *.podspec.json 文件,你可以把它们理解为 podspec 的 JSON 版本,这对 JS 处理更加友好。

1
2
3
4
5
6
7
8
9
10
11
// R.podspec.json
{
"name": "R",
"version": "0.0.1",
// ...
"dependencies": {
"A": [],
"C": [],
"E": []
}
}

而作为 localpods 的 A, C, E 也会有类似这样的文件。所以我们要做的事情就很简单了,挨个 parse 各个 JSON 文件,递归处理 dependencies 中的每一项,直到它的 dependencies 是空为止。其实核心的逻辑也就下面这一小段,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function trigger(pods){
let podsRet =[]
pods.forEach(ele =>{
podsRet.push(recursiveFindDependencies(ele, pods))
})
return podsRet
}
function recursiveFindDependencies(ele, pods){
let pod ={}
if (ele.name == undefined){
return
}
pod.name = ele.name
pod.dependencies =[]
ele.dependencies.forEach(dep =>{
if (pod.dependencies.filter(es => es == dep).length == 0){
pod.dependencies.push(dep)
}
let deps = findDependencies(pods, dep).forEach(e =>{
Array.prototype.push.apply(pod.dependencies, recursiveFindDependencies(e, pods))
if (pod.dependencies.filter(es => es == e).length == 0){
pod.dependencies.push(e)
}
})
})
return pod
}

有了这个工具以后,就能够看清一个 pod 到底依赖了什么,首先它更便于为 pod 创建独立运行的工程,其次,也能在开发阶段知道依赖关系,避免同层依赖及低层向更高层的 pod 依赖。

关于这个小工具,还想说两句。它是用 electron 写的,其实这东西完全用不上 electron 的跨平台便利性,因为它只可能在 mac 上运行,但我还是脑一抽用它写了,毕竟 HTML & CSS 比原生开发效率高得多。