Android Module依赖关系的可视化实现

avatar
研发 @字节跳动

作者:大力智能技术团队-客户端 五香鱼

​背景

大力教育下的Android工程代码,从出生开始都是组件化的结构,所以多module是必然的事情。伴随着模块的新增/删除/移动,都会导致之前梳理好的模块依赖图变得老旧,需要额外的人力去维护模块依赖结构。特别是对于刚融入的新同学,参考这老版本的依赖图,会有一定程度上的信息滞后。

那么是不是可以写一个这样的自动化工具,代替人力来动态维护module之间的依赖关系?本文就基于lint方案,自动实现模块之间的依赖关系视图。

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<String, ElementNode>()
    // 根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<String>? {
        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的projectNameprojectDepedencyList,最后把每个module对应的project进行汇总,梳理出整个树状依赖结构。既然整个依赖数据结构已经有了,下一步是不是要可视化输出了?

Markdown

为什么要选择Markdown?工具不重要,核心是要能出图。基于以下原因考虑:

  1. 熟悉,不需要额外学习成本。
  2. 简单,流程图语法参考:mermaid-js.github.io/mermaid/#/?…
  3. 满足我的需求,当下只需要流程图能力。
  4. 无缝链接编程语言,无需额外依赖。

结果图:

通过一个demo来展示当前依赖效果。

虽然只有几个模块,但由于一些重复依赖的线条,导致该图不那么的清晰简洁。所以做一些精简:

  1. app->account-api->common
  2. app->common

改善的原则:「保留多结点依赖,删除直接依赖」。也就是保留【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版本,再做兼容调试。

价值

  1. 在新人的角度,可以快速获取最新依赖图,加速对项目结构的理解。
  2. 在非新人的角度,定期输出依赖图,分析其中不合理的依赖并及时改进。