Android Lint:自定义Lint调试与开发

Android Lint实现简介

Android SDK

Android SDK中涉及Lint的主要有下面几个包,均包含在Android Gradle插件com.android.tools.build:gradle中。

  1. com.android.tools.lint:lint-api,这个包提供了Lint的API,包括Context、Project、Detector、Issue、IssueRegistry等。

  2. com.android.tools.lint:lint-checks,这个包实现了Android原生Lint规则。在25.2.3版本中,BuiltinIssueRegistry中共包含263条Lint规则。

  3. com.android.tools.lint:lint,这个包用于运行Lint检查,提供:

    • com.android.tools.lint.XxxReporter:检查结果报告,包括纯文本、XML、HTML格式等

    • com.android.tools.lint.LintCliClient:用于在命令行中执行Lint

    • com.android.tools.lint.Main:这个类是命令行版本Lint的Java入口(Command line driver),主要是解析参数、输出结果

  4. com.android.tools.build:gradle-core,这个包提供Gradle插件核心功能,其中与Lint相关的主要有:

    • com.android.build.gradle.internal.LintGradleProject:继承自lint-api中的Project类。Gradle执行Lint检查时使用的Project对象,可获取Manifest、依赖等信息。其中又包含了AppGradleProjectLibraryProject两个内部类。

    • com.android.build.gradle.internal.LintGradleClient:用于在Gradle中执行Lint,继承自LintCliClient

    • com.android.build.gradle.tasks.Lint,Gradle中Lint任务的实现

Lint命令行实现

Lint可执行文件位于<android-home>/tools/lint,是一个Shell脚本,配置相关参数并执行Java调用com.android.tools.lint.Main进行检查。

Android Studio、IDEA中的实现

在Android Studio或装有Android插件的IDEA环境下,Inspections中的Lint检查是通过Android插件实现的,代码实现主要在org.jetbrains.android.inspections.lint包中。

IDEA Android插件中Lint部分的实现
https://github.com/JetBrains/android/blob/master/android/src/org/jetbrains/android/inspections/lint

自定义Lint开发基础

主要API

自定义Lint开发需要调用Lint提供的API,最主要的几个API如下。

  • Issue:表示一个Lint规则。例如调用Toast.makeText()方法后,没有调用Toast.show()方法将其显示。

  • IssueRegistry:用于注册要检查的Issue列表。自定义Lint需要生成一个jar文件,其Manifest指向IssueRegistry类。

  • Detector:用于检测并报告代码中的Issue。每个Issue包含一个Detector。

  • Scope:声明Detector要扫描的代码范围,例如Java源文件、XML资源文件、Gradle文件等。每个Issue可包含多个Scope。

  • Scanner:用于扫描并发现代码中的Issue。每个Detector可以实现一到多个Scanner。自定义Lint开发过程中最主要的工作就是实现Scanner。

简易示例如下。

Manifest文件(META-INF/MANIFEST.MF

1
2
Manifest-Version: 1.0
Lint-Registry: com.paincker.lint.core.MyIssueRegistry

Java代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyIssueRegistry extends IssueRegistry {

@Override
public synchronized List<Issue> getIssues() {
return Arrays.asList(LogDetector.ISSUE, NewThreadDetector.ISSUE);
}
}

public class LogDetector extends Detector implements Detector.JavaPsiScanner {

public static final Issue ISSUE = Issue.create(
"LogUsage",
"避免调用android.util.Log",
"请勿直接调用android.util.Log,应该使用统一工具类",
Category.SECURITY, 5, Severity.ERROR,
new Implementation(LogDetector.class, Scope.JAVA_FILE_SCOPE));

// Detector的实现...
}

Scanner

Lint中包括多种类型的Scanner如下,其中最常用的是扫描Java源文件和XML文件的Scanner。

  • JavaScanner / JavaPsiScanner / UastScanner:扫描Java源文件
  • XmlScanner:扫描XML文件
  • ClassScanner:扫描class文件
  • BinaryResourceScanner:扫描二进制资源文件
  • ResourceFolderScanner:扫描资源文件夹
  • GradleScanner:扫描Gradle脚本
  • OtherFileScanner:扫描其他类型文件

值得注意的是,扫描Java源文件的Scanner先后经历了三个版本。

  1. 最开始使用的是JavaScanner,Lint通过Lombok库将Java源码解析成AST(抽象语法树),然后由JavaScanner扫描。

  2. 在Android Studio 2.2和lint-api 25.2.0版本中,Lint工具将Lombok AST替换为PSI,同时弃用JavaScanner,推荐使用JavaPsiScanner。

    PSI是JetBrains在IDEA中解析Java源码生成语法树后提供的API。相比之前的Lombok AST,可以支持Java 1.8、类型解析等。使用JavaPsiScanner实现的自定义Lint规则,可以被加载到Android Studio 2.2+版本中,在编写Android代码时实时执行。

  3. 在Android Studio 3.0和lint-api 25.4.0版本中,Lint工具将PSI替换为UAST,同时推荐使用新的UastScanner。

    UAST是JetBrains在IDEA新版本中用于替换PSI的API。UAST更加语言无关,除了支持Java,还可以支持Kotlin。

本文目前仍然基于PsiJavaScanner做介绍。根据UastScanner源码中的注释,可以很容易的从PsiJavaScanner迁移到UastScanner。

PSI介绍

PSI(Program Structure Interface)是IDEA中用于解析代码的一套API,可将文件的内容表示为特定编程语言中的元素的层级结构。

A PSI (Program Structure Interface) file is the root of a structure representing the contents of a file as a hierarchy of elements in a particular programming language.

每种Psi元素对应一个类,均继承自com.intellij.psi.PsiElement。例如PsiMethodCallExpression表示方法调用语句,PsiNewExpression表示对象实例化语句等。

官方文档

IntelliJ Platform SDK DevGuide
http://www.jetbrains.org/intellij/sdk/docs/basics/architectural_overview/psi_files.html

PSI Viewer

可以在IDEA / Android Studio中安装PSI Viewer插件,查看代码解析后的PSI元素及其属性值,例如下图中的new Thread(...)语句,就是一个PsiNewExpression元素。

JavaPsiScanner介绍

JavaPsiScanner中包含6组、12个回调方法,如下。

  1. getApplicablePsiTypes返回了需要检查的Psi元素类型列表时,类型匹配的Psi元素(PsiElement)就会被createPsiVisitor返回的JavaElementVisitor检查。

  2. getApplicableMethodNames返回方法名的列表时,名称匹配的方法调用(PsiMethodCallExpression)就会被visitMethod检查。

  3. getApplicableConstructorTypes返回类名的列表时,类名匹配的构造语句(PsiNewExpression)就会被visitConstructor检查。

  4. getApplicableReferenceNames返回引用名的列表时,名称匹配的引用语句(PsiJavaCodeReferenceElement)就会被visitReference检查。

  5. appliesToResourceRefs返回true时,Java代码中的资源引用(例如R.layout.main)就会被visitResourceReference检查。

  6. applicableSuperClasses返回父类名的列表时,父类名匹配的类声明(PsiClass)就会被checkClass检查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public interface JavaPsiScanner  {

@Nullable
List<Class<? extends PsiElement>> getApplicablePsiTypes();

@Nullable
JavaElementVisitor createPsiVisitor(@NonNull JavaContext context);

@Nullable
List<String> getApplicableMethodNames();

void visitMethod(
@NonNull JavaContext context,
@Nullable JavaElementVisitor visitor,
@NonNull PsiMethodCallExpression call,
@NonNull PsiMethod method);

@Nullable
List<String> getApplicableConstructorTypes();

void visitConstructor(
@NonNull JavaContext context,
@Nullable JavaElementVisitor visitor,
@NonNull PsiNewExpression node,
@NonNull PsiMethod constructor);

@Nullable
List<String> getApplicableReferenceNames();

void visitReference(
@NonNull JavaContext context,
@Nullable JavaElementVisitor visitor,
@NonNull PsiJavaCodeReferenceElement reference,
@NonNull PsiElement referenced);

boolean appliesToResourceRefs();

void visitResourceReference(
@NonNull JavaContext context,
@Nullable JavaElementVisitor visitor,
@NonNull PsiElement node,
@NonNull ResourceType type,
@NonNull String name,
boolean isFramework);

@Nullable
List<String> applicableSuperClasses();

void checkClass(@NonNull JavaContext context, @NonNull PsiClass declaration);
}

自定义Lint开发过程

示例工程可在此下载
https://github.com/jzj1993/AndroidLint

创建Lint.jar

创建基于Gradle的Java工程/模块,编写代码,使用gradle assemble指令打包成jar。具体可参考示例工程。

其中build.gradle文件如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apply plugin: 'java'

repositories {
mavenCentral()
}

dependencies {
compile 'com.android.tools.lint:lint-api:25.3.0'
compile 'com.android.tools.lint:lint-checks:25.3.0'
}

jar {
manifest {
attributes("Lint-Registry": "com.paincker.lint.core.MyIssueRegistry")
}
}

configurations {
lintChecks
}

dependencies {
lintChecks files(jar)
}

Java源码如下。在这个例子里,创建了两条Lint规则:

  • LogDetector:检查是否使用了Android系统的Log工具类,并要求使用统一封装的工具类。
  • NewThreadDetector:检查是否直接创建了新线程,并要求使用AsyncTask或统一工具类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.paincker.lint.core;

import com.android.tools.lint.client.api.IssueRegistry;
import com.android.tools.lint.detector.api.Issue;

import java.util.Arrays;
import java.util.List;

public class MyIssueRegistry extends IssueRegistry {

@Override
public synchronized List<Issue> getIssues() {
System.out.println("==== my lint start ====");
return Arrays.asList(LogDetector.ISSUE, NewThreadDetector.ISSUE);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.paincker.lint.core;

import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.intellij.psi.JavaElementVisitor;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiMethodCallExpression;

import java.util.Arrays;
import java.util.List;

public class LogDetector extends Detector implements Detector.JavaPsiScanner {

public static final Issue ISSUE = Issue.create(
"LogUsage",
"避免调用android.util.Log",
"请勿直接调用android.util.Log,应该使用统一工具类",
Category.SECURITY, 5, Severity.ERROR,
new Implementation(LogDetector.class, Scope.JAVA_FILE_SCOPE));

@Override
public List<String> getApplicableMethodNames() {
return Arrays.asList("v", "d", "i", "w", "e", "wtf");
}

@Override
public void visitMethod(JavaContext context, JavaElementVisitor visitor, PsiMethodCallExpression call, PsiMethod method) {
if (context.getEvaluator().isMemberInClass(method, "android.util.Log")) {
context.report(ISSUE, call, context.getLocation(call.getMethodExpression()), "请勿直接调用android.util.Log,应该使用统一工具类");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.paincker.lint.core;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.tools.lint.detector.api.Category;
import com.android.tools.lint.detector.api.Detector;
import com.android.tools.lint.detector.api.Implementation;
import com.android.tools.lint.detector.api.Issue;
import com.android.tools.lint.detector.api.JavaContext;
import com.android.tools.lint.detector.api.Location;
import com.android.tools.lint.detector.api.Scope;
import com.android.tools.lint.detector.api.Severity;
import com.intellij.psi.JavaElementVisitor;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiNewExpression;

import java.util.Collections;
import java.util.List;

public class NewThreadDetector extends Detector implements Detector.JavaPsiScanner {

public static final Issue ISSUE = Issue.create(
"NewThread",
"避免自己创建Thread",
"请勿直接调用new Thread(),建议使用AsyncTask或统一的线程管理工具类",
Category.PERFORMANCE, 5, Severity.ERROR,
new Implementation(NewThreadDetector.class, Scope.JAVA_FILE_SCOPE));

@Override
public List<String> getApplicableConstructorTypes() {
return Collections.singletonList("java.lang.Thread");
}

@Override
public void visitConstructor(@NonNull JavaContext context, @Nullable JavaElementVisitor visitor,
@NonNull PsiNewExpression node, @NonNull PsiMethod constructor) {
context.report(ISSUE, node, context.getLocation(node), "请勿直接调用new Thread(),建议使用AsyncTask或统一的线程管理工具类");
}
}

验证Lint.jar文件可用

复制上一步生成的lint.jar文件到~/.android/lint/目录下,在Android工程中写一些不符合自定义Lint规则的代码如下。在工程根目录下调用./gradlew lint执行Lint检查,即可看到Lint输出结果。

验证完成后删掉jar文件,防止和后续步骤冲突。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MainActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

Log.d("tag", "msg");

new Thread(new Runnable() {
@Override
public void run() {

}
}).run();
}
}

创建Lint.aar

前面的使用方式,自定义Lint必须保存在电脑中的特定文件夹。实际应用时,往往希望自定义Lint和工程关联,而不是和电脑关联,因此需要创建lint.aar包,并在需要执行自定义Lint检查的工程中依赖这个AAR。

依赖关系:Java模块 --> 包含lint.jarlint.aar模块 --> 实际Android项目

具体步骤如下(完整的工程见示例代码)。

  1. 在Android Studio中创建一个空的Java模块lintjar,和一个空的Android Library模块lintaar

  2. lintjar模块中的配置和前面相同,用于编写实际的Lint规则。

  3. lintaar模块依赖lintjar模块,build.gradle如下,主要是把jar文件改成了lint.jar并打包到AAR里。lintaar模块编译生成的AAR即为需要的lint.aar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
apply plugin: 'com.android.library'

android {
compileSdkVersion 24
buildToolsVersion '25.0.2'

defaultConfig {
minSdkVersion 15
targetSdkVersion 25
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

configurations {
lintLibrary
}

dependencies {
lintLibrary project(path: ":lintjar", configuration: "lintChecks")
}

task copyLintJar(type: Copy) {
from(configurations.lintLibrary) {
rename {
String fileName ->
'lint.jar'
}
}
into 'build/intermediates/lint/'
}

project.afterEvaluate {
def compileLintTask = project.tasks.find { it.name == 'compileLint' }
compileLintTask.dependsOn(copyLintJar)
}

运行自定义Lint

在Android工程中依赖lint.aar,或者直接依赖前面的lintaar工程,在执行Lint任务时,就会同时执行自定义的Lint规则(完整工程见示例代码)。

自定义Lint调试

开发过程中,可能需要对自定义Lint进行调试。在电脑上编译Android工程时,自定义Lint是以jar文件的形式被加载并运行的。实际试验发现,其调试过程和Gradle插件开发的调试过程相似。

  1. 在Android项目中,以源码形式依赖自定义Lint代码(和示例代码一致)。

  2. 提前在自定义Lint代码中打好断点。

  3. 在Android Application模块的build.gradle中关闭Lint的abortOnError选项,以免还没到断点时build就中止了。

    1
    2
    3
    lintOptions {
    abortOnError false
    }
  4. 在Android Studio的运行参数(Run Configurations)中添加一个Remote类型,都取默认值即可。

  5. 打开一个命令行窗口,执行下面命令设置临时环境变量,从而开启Gradle调试。端口号为默认的5005,和前面在Android Studio中新增的Run Configuration端口号一致。

    1
    export GRADLE_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005"
  6. 还是在这个命令行窗口,执行Gradle任务,并设置参数关闭Gradle Deamon。执行后Gradle会等待Android Studio调试器连接。

    1
    ./gradlew clean lintDebug -Dorg.gradle.daemon=false
  7. Android Studio使用刚配置的Remote运行参数,点击调试箭头按钮,连接到Gradle就会开始执行,执行到Lint任务时就会在断点处中断,可以正常调试Java源码。

  8. 命令行执行下面代码,可关闭Gradle调试

    1
    unset GRADLE_OPTS

参考资料与扩展阅读