最近在优化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设置AndroidLintInputs把com.android.tools.lint.Main 设置为mainClass
在
AndroidLintInputs会把Lint任务包装成AndroidLintWorkAction提交到Gradle封装的 WorkerExecutor 中
在AndroidLintWorkAction的 runLint方法中会通过URLClassLoader构造com.android.tools.lint.Main类,执行实际的Lint任务
接下来我们看看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这个变量
通过二把刀翻译可知cacheClassLoader实例是整个进程期间都存在的:
经过实际测试只要守护进程在多次app:lintRelease也是使用同样的cachedClassloader实例。
2.2 Hook思路
既然cachedClassloader是整个JVM周期都存在的并且是懒加载的,我们只要提前准备好优先加载定制的com.android.tools.lint.Main的URLClassLoader,那么后面增量Lint也就水到渠成了。这个实际可以分解成2个步骤:
- 创建支持增量Lint的
com.android.tools.lint.Main - 在合适的时机构造URLClassLoader
首先解决第一个步骤:
支持增量Lint的com.android.tools.lint.Main 根据之前大佬们适配的经验可知:只需要在Main的run执行开始调用LintApi之前插入需要Lint的文件即可:
这里需要注意File需要的是绝对路径,Git命令返回不一定符合预期。
接着来到重头戏URLClassLoader构造:
我们先看看AndoirdLintWrokAction是如何构造的,然后看看能否依葫芦画瓢?
构造URLClassLoader需要相应的Classpath
而Classpath是LintPlugin中
ProjectServices的LintFromMaven提供的
而LintPlugin我们暂时没有途径获取到,但是在AGP7.2.2中发现AppPlugin的基类中AndroidPluginBaseServices有ProjectServices 变量
所以我们只需要在自己的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. 结果
jym记得一键三连哦