大力教育下的Android工程代码,从出生开始都是组件化的结构,所以多module是必然的事情。伴随着模块的新增/删除/移动,都会导致之前梳理好的模块依赖图变得老旧,需要额外的人力去维护模块依赖结构。特别是对于刚融入的新同学,参考这老版本的依赖图,会有一定程度上的信息滞后。
那么是不是可以写一个这样的自动化工具,代替人力来动态维护module之间的依赖关系?本文就基于lint方案,自动实现模块之间的依赖关系视图。
大家对Lint的认知,基本上都觉得它只是一个静态代码分析工具,通过规则规范团队代码。但我想为Lint证明:「只有你想不到,没有我做不到」的能力。Lint在运行期间一个重要环节,就是依赖于gradle环境:
./gradlew lintDebug
既然运行在了gradle环境中,必然可以拿到不同模块之间的依赖关系,该方案去做模块可视化,看去可行。
Detector
:文件检测类,可以扫描gradle/java/kt/res等等文件。
Issue
:每个Detector关注的焦点问题。
这里不多介绍Lint开发环境的搭建,可以直接参考官方demo:github.com/googlesampl… 。 具体效果可以参考ToastDetector:提醒Toast增加show()方法。
Detector中有两个和project相关的方法:
beforeCheckRootProject(context: Context)
,遍历根module之前的回调,即项目中最顶层app module
beforeCheckEachProject(context: Context)
,遍历每个子module之前的回调。
该两个方法中都有Context对应的上下文,在构造函数中惊奇的发现Project这个属性。
open class Context(
main: Project?,
project: Project?,
val file: File,
private var contents: CharSequence? = null
)
是不是感觉和gradle中的project很像?通过debug发现,该project确实包含了build.gradle中的depedency信息,那么基于Lint方案的思路就肯定可以落地了。窃窃自喜:「只要你想要,没有我给不了」。
class DependencyProjectDetector : Detector(), Detector.UastScanner {
// 各个module之间的依赖树
private val treeMap = HashMap()
// 根module结点
private var rootNode = ElementNode()
companion object {
const val TAG = "DependencyProjectDetector"
private val IMPLEMENTATION = Implementation(
DependencyProjectDetector::class.java,
Scope.JAVA_FILE_SCOPE
)
val ISSUE: Issue = Issue
.create(
id = "DependencyProjectDetector",
briefDescription = "app dependency relationship",
explanation = """
app dependency relationship
""".trimIndent(),
category = Category.CORRECTNESS,
priority = 9,
severity = Severity.FATAL,
androidSpecific = true,
implementation = IMPLEMENTATION
)
}
//这里可以随意,主要是为了借助遍历method的能力。
override fun getApplicableMethodNames(): List? {
return listOf("a")
}
override fun beforeCheckRootProject(context: Context) {
rootNode = collectNode(context.project.name)
super.beforeCheckRootProject(context)
}
//分析当前模块依赖情况,并收集依赖信息。
private fun analysisCurrentProjectDependency(project: Project) {
val projectName = project.name
val currentNode = collectNode(projectName)
val artifact = project.currentVariant.mainArtifact
val list = project.directLibraries.filter { !it.isExternalLibrary }
list.forEach {
val childNode = collectNode(it.name)
if (!currentNode.dependencyNode.map { it.moduleName }.contains(childNode.moduleName)) {
currentNode.dependencyNode.add(childNode)
}
}
}
private fun collectNode(moduleName: String): ElementNode {
val currentNode = if (treeMap.containsKey(moduleName)) {
treeMap[moduleName]!!
} else {
ElementNode().apply {
this.moduleName = moduleName
treeMap[moduleName] = this
}
}
return currentNode
}
//输出markdown格式的文本信息
private fun generateMarkDownResult() {
for (element: ElementNode in treeMap.values) {
removeUnnecessaryDependency(element)
}
println("```mermaid")
val head = "graph TD"
println(head)
for (element: ElementNode in treeMap.values) {
val currentName = element.moduleName
for (childElement: ElementNode in element.dependencyNode) {
println("${currentName}[${currentName}]-->${childElement.moduleName}[${childElement.moduleName}]")
}
}
println("```")
}
/**
* 优化:移除不需要的依赖,原则:currentNode的孩子队列,最短依赖,有可以被非最短依赖替代的,则删除最短依赖。
*/
private fun removeUnnecessaryDependency(currentNode: ElementNode) {
val dependencyList = currentNode.dependencyNode
//为了避免remove失败,创建了一个temp list
val tempDependencyList = ArrayList(dependencyList)
val iterator = tempDependencyList.iterator()
while (iterator.hasNext()) {
val targetElement = iterator.next()
val leftTargetElements = tempDependencyList.filter { it != targetElement }
for (otherItem: ElementNode in leftTargetElements) {
if (containNode(otherItem, targetElement)) {
dependencyList.remove(targetElement)
continue
}
}
}
}
/**
* rootNode下面是否包含targetNode结点
*/
private fun containNode(rootNode: ElementNode, targetNode: ElementNode): Boolean {
if (rootNode == targetNode) {
return true
}
val rootChildren = rootNode.dependencyNode
for (child: ElementNode in rootChildren) {
if (rootNode == child) {
return true
} else {
var result = containNode(child, targetNode)
if (result) {
return result
}
}
}
return false
}
override fun beforeCheckEachProject(context: Context) {
if (context.project.isExternalLibrary) {
return
}
analysisCurrentProjectDependency(context.project)
super.beforeCheckEachProject(context)
}
override fun afterCheckRootProject(context: Context) {
generateMarkDownResult()
super.afterCheckRootProject(context)
}
}
核心原理: 通过遍历project,收集当前project的projectName
和projectDepedencyList
,最后把每个module对应的project进行汇总,梳理出整个树状依赖结构。既然整个依赖数据结构已经有了,下一步是不是要可视化输出了?
为什么要选择Markdown?工具不重要,核心是要能出图。基于以下原因考虑:
通过一个demo来展示当前依赖效果。
虽然只有几个模块,但由于一些重复依赖的线条,导致该图不那么的清晰简洁。所以做一些精简:
改善的原则:「保留多结点依赖,删除直接依赖」。也就是保留【1】,删除【2】,具体实现参考上述代码中的removeUnnecessaryDependency
。
怎么样,模块依赖是不是比较清晰?
Lint默认运行是不会跨module的,比如./gradlew lint
,会对每个module进行独立的分析和输出,这就导致每一份分析结构都是针对当前module的,所以无法收集相互之间的依赖。解决方法:
lintOptions {
checkDependencies true
}
果不其然,api文档中也没有checkDependencies
属性,google.github.io/android-gra… 那只能你自己慢慢去debug吧。
lint在gradle插件的不同版本3.+和4.+上,会有很大的不同,所以你想要集成该能力,建议自己根据当前工程的gradle版本,再做兼容调试。