1、说明
最近一段时间,项目迁移到AndroidX后,突然发现之前编写的lint规则没有生效。查看lint报告提示lintError,后面说明就反馈找不到自定义规则对应的ID。
2、问题
经过gradle调试(gradle调试方法自行百度)发现目前使用的lint版本(26.2.1)不没有适应新的Lint规则导入(META-INF/services/com.android.tools.lint.client.api.IssueRegistry)。但是AndroidX中很多包都使用这种方式添加lint规则以保证该包被正确使用。
JarFileIssueRegistry代码如下:
private fun findRegistries(
client: LintClient,
jarFiles: Collection<File>
): Map<String, File> {
val registryClassToJarFile = HashMap<String, File>()
for (jarFile in jarFiles) { //1、遍历lint规则的jar包
JarFile(jarFile).use { file ->
val manifest = file.manifest
val attrs = manifest.mainAttributes
var attribute: Any? = attrs[Attributes.Name(MF_LINT_REGISTRY)]
var isLegacy = false
if (attribute == null) {
attribute = attrs[Attributes.Name(MF_LINT_REGISTRY_OLD)]
if (attribute != null) {
isLegacy = true
}
}
//2、AndroidX包中带的规则包并不包含上面两种属性,因此执行else代码
if (attribute is String) {
val className = attribute
if (!isLegacy || registryClassToJarFile[className] == null) {
registryClassToJarFile[className] = jarFile
}
} else {
val services = file.getJarEntry(SERVICE_KEY)
if (services != null) {
...
//3、发现META-INF/services/com.android.tools.lint.client.api.IssueRegistry定义的规则之后立即返回
return registryClassToJarFile
}
client.log(
Severity.ERROR, null,
"Custom lint rule jar %1\$s does not contain a valid " +
"registry manifest key (%2\$s).\n" +
"Either the custom jar is invalid, or it uses an outdated " +
"API not supported this lint client",
jarFile.path, MF_LINT_REGISTRY
)
}
}
}
return registryClassToJarFile
}
由上述代码中的三处注释可以看出,lint规则jar包并没有全部遍历完成就直接退出了,我们添加的自定义规则并没获取到。
3、解决
讲到这里可能就有人觉得这个问题更新一下版本不就能很好解决了。Lint-27.1.0版本确实是没有这样的问题的,因为它去掉了return registryClassToJarFile这段代码,遍历了所有的jar包。但是我们项目中对lint流程做了定制,让它能够实现增量检查(如需实现Lint增量检查请看自定义lint增量检查)。更新版本需要重新适配,还需要更新gradle插件版本,导致别的插件都需要进行相应修改。这样的改动实在太大,风险很高,让人无法接受。那么我们这么解决这个问题呢?
最开始解决这个问题的思路是将自定义的规则放在其他规则的前面,让它的规则提前添加registryClassToJarFile中,但是发现jarFiles是一个Set集合,没有固定点的顺序。而且这样的方案还有一个弊端,那就是部分AndroidX包的规则并不会执行,也就是即使我们使用AndroidX有错误,Lint也检查不出来问题。
前面方案的失败让我打算将整个lint包(包括lint-gradle,lint-gradle-api)都直接到导入到项目中,然后直接修改代码来解决问题。但是突然想到之前解决kotlin项目编译报错的时候,利用ClassLoader将Lint执行的流程置于独立的环境(具体参考LintRunner.java)。使用这个ClassLoader可以将JarFileIssueRegistry这个类替换成我们自己的类JarFileIssueRegistry.kt(删除了return registryClassToJarFile代码)。具体如何实现呢?那就是初始化ClassLoader时将我们的jar包放在最前面,ClassLoader加载JarFileIssueRegistry这个类的时候优先加载我们编写的类。
private static ClassLoader getLintClassLoader(Gradle gradle, Set<File> lintClassPath, String customJarName) {
DelegatingClassLoader l = loader;
if (l == null) {
List<URL> urls = computeUrlsFallback(lintClassPath, customJarName);
l = new DelegatingClassLoader(urls.toArray(new URL[0]));
loader = l;
}
......
}
private static List<URL> computeUrlsFallback(Set<File> lintClassPath, String customJarName) {
List<URL> urls = new ArrayList<>();
Iterator<File> iterator = lintClassPath.iterator();
//优先加载包含修改的JarFileIssueRegistry.kt的jar包
while (iterator.hasNext()) {
File file = iterator.next();
if (file.getName().startsWith(customJarName)) {
try {
urls.add(file.toURI().toURL());
} catch (MalformedURLException e) {
e.printStackTrace();
}
iterator.remove();
}
}
lintClassPath.forEach(file -> {
String name = file.getName();
if (name.startsWith("uast-") ||
name.startsWith("intellij-core-") ||
name.startsWith("kotlin-compiler-") ||
name.startsWith("asm-") ||
name.startsWith("kxml2-") ||
name.startsWith("trove4j-") ||
name.startsWith("groovy-all-") |
// All the lint jars, except lint-gradle-api jar (self)
name.startsWith("lint-") &&
// Do *not* load this class in a new class loader; we need to
// share the same class as the one already loaded by the Gradle plugin
!name.startsWith("lint-gradle-api-")
)
try {
urls.add(file.toURI().toURL());
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
});
return urls;
}
至此自定义规则不生效的问题就解决了,这样既能确认自定义规则正常执行,也能保证AndroidX中的规则包能够正常添加。同时如果后续需要升级版本,只需要进行少量文件移植,无需进行大量比对修改。