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可执行文件位于 /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

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

Java代码

public class MyIssueRegistry extends IssueRegistry {

    @Override
    public synchronized List 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 检查。

public interface JavaPsiScanner  {

    @Nullable
    List<Class> getApplicablePsiTypes();

    @Nullable
    JavaElementVisitor createPsiVisitor(@NonNull JavaContext context);

    @Nullable
    List getApplicableMethodNames();

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

    @Nullable
    List getApplicableConstructorTypes();

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

    @Nullable
    List 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 applicableSuperClasses();

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

自定义Lint开发过程

示例工程可在此下载

https://github.com/jzj1993/AndroidLint

创建Lint.jar

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

其中build.gradle文件如下。

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或统一工具类。
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 getIssues() {
        System.out.println("==== my lint start ====");
        return Arrays.asList(LogDetector.ISSUE, NewThreadDetector.ISSUE);
    }
}
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 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,应该使用统一工具类");
        }
    }
}
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 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文件,防止和后续步骤冲突。

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

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就中止了。

lintOptions {
    abortOnError false
}

4、在Android Studio的运行参数(Run Configurations)中添加一个Remote类型,都取默认值即可。

5、打开一个命令行窗口,执行命令 export GRADLE_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005" 设置临时环境变量,从而开启Gradle调试。端口号为默认的5005,和前面在Android Studio中新增的Run Configuration端口号一致。

6、还是在这个命令行窗口,执行Gradle任务 ./gradlew clean lintDebug -Dorg.gradle.daemon=false ,设置参数关闭Gradle Deamon。执行后Gradle会等待Android Studio调试器连接。

7、Android Studio使用刚配置的Remote运行参数,点击调试箭头按钮,连接到Gradle就会开始执行,执行到Lint任务时就会在断点处中断,可以正常调试Java源码。

8、命令行执行 unset GRADLE_OPTS ,可关闭Gradle调试

参考资料与扩展阅读

来自为知笔记(Wiz)

稿源:Paincker (源链) | 关于 | 阅读提示

本站遵循[CC BY-NC-SA 4.0]。如您有版权、意见投诉等问题,请通过eMail联系我们处理。
酷辣虫 » 移动开发 » Android Lint:自定义Lint调试与开发

喜欢 (0)or分享给?

专业 x 专注 x 聚合 x 分享 CC BY-NC-SA 4.0

使用声明 | 英豪名录