您的浏览器不支持CSS3,建议使用Firfox、Chrome等浏览器,以取得最佳显示效果

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

Android 4,015℃ 0 12个月前 (12-30)

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

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

Java代码

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检查。

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文件如下。

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<Issue> 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<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,应该使用统一工具类");
        }
    }
}
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文件,防止和后续步骤冲突。

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代码中打好断点。注意不是Android代码,而是编译期间Gradle加载执行的代码。

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 -Dorg.gradle.debug=true --no-daemon,设置参数关闭Gradle Deamon。执行后Gradle会等待Android Studio调试器连接。

补充:如果失败了,可以执行./gradlew -stop确保关闭已有Gradle Deamon,再尝试一下。新版本的gradle调试可能有一些差异,上面提到的命令把已知的多个版本参数都加上了,也可以执行./gradlew --help查看帮助,以及参考Gradle官方文档:https://docs.gradle.org/current/userguide/troubleshooting.html

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

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

参考资料与扩展阅读

来自为知笔记(Wiz)

最后,欢迎扫码关注微信公众号。

0

暂无评论

评论前:需填写以下信息,或 登录

用户登录

忘记密码?