增量Lint适配AGP7.2.2

816 阅读3分钟

最近在优化Lint检查耗时,在搜索现有的方案后发现在AGP7.2.2上都失效了。为了方便后来老哥能早点下班故记录下Hook思路。

1. AGP7.2.2适配困难之处

请问大佬们有在新版7.2.2的agp上试过吗,我的项目用的是agp 7.2.2,这个版本里面LINT_CLASS_PASH这个字段被删除了,我看了下gradle的源码,发现现在lintclasspath是从projectService里面的一个叫LintFromMaven的对象里面拿的,现在的问题在于无法获得projectService实例。
我想过拿到LintPlugin的实例以后用反射获取里面的service,但是又无法获取LintPlugin实例

MR_YNAG大佬很好的概括了在AGP7.2.2上的问题。就不做过多的赘述。

2. 思路

根据maybee大佬的增量Lint适配实践[1]可知:调用Lint库实现实现Lint 主要逻辑在 com.android.tools.lint.Main。所以我们实现增量Lint的关键逻辑还是在替换系统 com.android.tools.lint.Main类并在其中自定义实现中实现增量文件识别并添加到LintRequest中。

2.1 现状

我们以AndroidLintTask为例搞清楚com.android.tools.lint.Main构造过程。
AndroidLintTask在execute设置AndroidLintInputscom.android.tools.lint.Main 设置为mainClass WeChatce22ef720e8e793917e83d738ba85054.pngAndroidLintInputs会把Lint任务包装成AndroidLintWorkAction提交到Gradle封装的 WorkerExecutor

AndroidLintInpus->submit.png

AndroidLintWorkAction的 runLint方法中会通过URLClassLoader构造com.android.tools.lint.Main类,执行实际的Lint任务

image.png

image.png

image.png 接下来我们看看URLClassLoader是如何创建的:

@Synchronized
private fun getClassloader(key: String, classpath: FileCollection): ClassLoader {

    val uris = classpath.files.map { it.toURI() }
    return cachedClassloader.executeCallableSynchronously {
        val map = cachedClassloader.get()
        val classloader = map[key]?.get()?.also {
            logger.info("Android Lint: Reusing lint classloader {}", key)
        } ?: createClassLoader(key, uris).also { map[key] = SoftReference(it) }
        toDispose[key] = classloader
        maintainClassloaders(map)
        classloader
    }
}


private fun createClassLoader(key: String, classpath: List<URI>): URLClassLoader {
    logger.info("Android Lint: Creating lint class loader {}", key)
    val classpathUrls = classpath.map { it.toURL() }.toTypedArray()
    return URLClassLoader(classpathUrls, getPlatformClassLoader())
}

跟据上面逻辑可以知道:构建com.android.tools.lint.Main的URLClassLoader会缓存到cachedClassloader 这个变量中,下次创建Class首先通过key判断URLClassLoader在不在缓存中,在的话复用。
接下来我们看看cachedClassloader这个变量

image.png 通过二把刀翻译可知cacheClassLoader实例是整个进程期间都存在的:

translate.png

经过实际测试只要守护进程在多次app:lintRelease也是使用同样的cachedClassloader实例。

2.2 Hook思路

既然cachedClassloader是整个JVM周期都存在的并且是懒加载的,我们只要提前准备好优先加载定制的com.android.tools.lint.Main的URLClassLoader,那么后面增量Lint也就水到渠成了。这个实际可以分解成2个步骤:

  1. 创建支持增量Lint的com.android.tools.lint.Main
  2. 在合适的时机构造URLClassLoader

首先解决第一个步骤:
支持增量Lint的com.android.tools.lint.Main 根据之前大佬们适配的经验可知:只需要在Main的run执行开始调用LintApi之前插入需要Lint的文件即可:

image.png

image.png 这里需要注意File需要的是绝对路径,Git命令返回不一定符合预期。

接着来到重头戏URLClassLoader构造:
我们先看看AndoirdLintWrokAction是如何构造的,然后看看能否依葫芦画瓢?
构造URLClassLoader需要相应的Classpath image.png 而Classpath是LintPlugin中ProjectServicesLintFromMaven提供的

image.png

而LintPlugin我们暂时没有途径获取到,但是在AGP7.2.2中发现AppPlugin的基类中AndroidPluginBaseServices有ProjectServices 变量

image.png 所以我们只需要在自己的Hook Plugin反射获取这个变量即可。

至此创建URLClassLoader所需的要素都已经备齐。因为AndroidLintTask的URLClassLoader是在任务实际执行的时候才创建,所以我们只需要在此之前为其构造好ClassLoader,构造好的ClassLoader优先加载我们定制的 com.android.tools.lint.Main即可实现Lint增量文件。

上述实现思路用来来说就是:

import com.android.build.gradle.internal.lint.AndroidLintWorkAction
import com.android.build.gradle.internal.lint.LintFromMaven
import com.android.build.gradle.internal.plugins.AppPlugin
import com.android.build.gradle.internal.services.ProjectServices
import com.android.utils.JvmWideVariable
import com.google.common.reflect.TypeToken
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import java.io.File
import java.lang.ref.SoftReference
import java.net.URI
import java.net.URLClassLoader

open class HookLintPlugin : Plugin<Project> {
    private lateinit var project: Project

    private val cachedClassloader = JvmWideVariable(
        AndroidLintWorkAction::class.java,
        "cachedClassloader",
        object : TypeToken<MutableMap<String, SoftReference<URLClassLoader>>>() {}
    ) { HashMap() }

    override fun apply(project: Project) {
        this.project = project
        println("IncrementLintPlugin ======== started")
        project.afterEvaluate {
            startHook(it)
        }
    }

    private fun startHook(project: Project) {
        val classpath = constructClasspath()
        println("first classpath: ${classpath.firstOrNull()}")
        getClassloader(project, PROPERTY_KEY,classpath)
    }

    private fun getClassloader(project: Project,key: String, classpath: List<File>): ClassLoader {
        val uris = classpath.map { it.toURI() }
        return cachedClassloader.executeCallableSynchronously {
            val map = cachedClassloader.get()
            val classloader = map[key]?.get()?.also {
                project.logger.info("Hook Android Lint: Reusing lint classloader {}", key)
            } ?: createClassLoader(project,key, uris).also { map[key] = SoftReference(it) }
            classloader
        }
    }

    private fun createClassLoader(project: Project,key: String, classpath: List<URI>): URLClassLoader {
        project.logger.info("Android Lint: Creating lint class loader {}", key)
        val classpathUrls = classpath.map { it.toURL() }.toTypedArray()
        return URLClassLoader(classpathUrls, getPlatformClassLoader())
    }

    private fun getPlatformClassLoader(): ClassLoader {
        // AGP is currently compiled against java 8 APIs, so do this by reflection (b/160392650)
        return ClassLoader::class.java.getMethod("getPlatformClassLoader").invoke(null) as ClassLoader
    }

    private fun constructClasspath(): List<File> {
        val appPlugin = project.plugins.findPlugin(AppPlugin::class.java)
        val files = getLintMavenByReflection(appPlugin)?.files ?: emptyList()
        val dependenciesFile = createConfig(project).files
        val hookFile = dependenciesFile.firstOrNull {
            it.absolutePath.contains(GROUP_NAME)
        }
        if(hookFile == null){
            project.logger.info("not found $GROUP_NAME absolutePath")
        }
        val linkSet = LinkedHashSet<File>().apply {
            if (hookFile != null) {
                add(hookFile)
            }
            addAll(files)
        }
        return linkSet.toList()
    }

    @Suppress("PrivateApi")
    private fun getLintMavenByReflection(appPlugin: AppPlugin?): LintFromMaven? {
        if (appPlugin == null) {
            return null
        }
        return try {
            val fields =
                com.android.build.gradle.internal.plugins.BasePlugin::class.java.getDeclaredField("projectServices")
            fields.isAccessible = true

            val projectServices = fields.get(appPlugin) as? ProjectServices
            projectServices?.lintFromMaven
        } catch (e: Exception) {
            e.printStackTrace()
            null
        }
    }

    private fun createConfig(project: Project): Configuration {
        val config = project.configurations.detachedConfiguration(
            project.dependencies.create(
                mapOf(
                    "group" to GROUP_NAME,
                    "name" to "lint",
                    "version" to VERSION_NAME,
                )
            )
        )
        config.isTransitive = true
        config.isCanBeResolved = true
        return config
    }

    companion object {
        private const val GROUP_NAME = "你的包名"
        private const val VERSION_NAME = "你的版本"
        private const val PROPERTY_KEY = "30.2.2"
    }
}

3. 结果

image.png

jym记得一键三连哦

参考文章

  1. # AGP7.0增量Lint适配