自动规避代码陷阱——自定义Lint规则

Android进阶之路系列:http://blog.csdn.net/column/details/16488.html

源码:https://github.com/chzphoenix/LintRulesForAndroid


一、Lint是什么?


Lint 是一款静态代码分析工具,能检查安卓项目的源文件,从而查找潜在的程序错误以及优化提升的方案。

当你忘记在Toast上调用show()时,Lint 就会提醒你。它也会确保你的ImageView中添加了contentDescription,以支持可用性。类似的例子还有成千上万个。诚然,Lint 能在诸多方面提供帮助,包括:正确性,安全,性能,易用性,可用性,国际化等等。

这是引用网上的一段描述,简单来说lint可以对代码进行检查分析,查找各类潜在问题。


二、Lint的使用

在Android Studio中选择Analyze -> Inspect Code
自动规避代码陷阱——自定义Lint规则
自动规避代码陷阱——自定义Lint规则

然后在弹出窗中选择Whole project,点击确定开始检查
自动规避代码陷阱——自定义Lint规则
自动规避代码陷阱——自定义Lint规则

检查结束就可以在下面看到结果了,如下图:
自动规避代码陷阱——自定义Lint规则
自动规避代码陷阱——自定义Lint规则

关于lint的使用不是本文的重点,这里只是简单介绍一下。


三、为什么要使用自定义Lint规则?

由于项目的架构,有时候项目中会有一些非正式的代码规则,比如不使用系统自带的日志工具Log而使用第三方或二次封装过的工具类。这种情况默认的lint就无法检查了,这时候自定义lint规则就派上用场了。
自定义lint规则可以帮助团队规避一些因架构、业务、历史等原因出现的代码陷阱,避免一些问题频繁重复的产生,同时可以让团队新成员快速被动的了解一些开发规则。
下面开始一步步介绍如何自定义lint规则。


四、新建module

想要使用自定义lint规则,一种做法是将定义规则的代码打成jar包,然后放在“%UserHome%/.android/lint/”目录下。
这种做法有两个缺点:一是对所有的项目都产生作用,无法实现不同项目使用不同规则;二是需要每个人都下载并拷贝到目录下。
另外一种做法将定义的规则打包成aar的形式,依赖到项目中。
这种做法需要创建两个module,一个java-lib,一个android-lib,如下图:
自动规避代码陷阱——自定义Lint规则
自动规避代码陷阱——自定义Lint规则
在lintjar中编写规则代码,而lintaar没有任何代码,它的作用是将lintjar的jar包打包成aar以便引用。


五、在lintjar中定义规则

在lintjar中新建一个类,继承Detector,实现一个规则,如下:
public class LogDetector extends Detector implements Detector.ClassScanner {
    public static final Issue ISSUE = Issue.create("LogUtilsNotUsed",
            "You must use our `LogUtils`",
            "Logging should be avoided in production for security and performance reasons. Therefore, we created a LogUtils that wraps all our calls to Logger and disable them for release flavor.",
            Category.MESSAGES,
            9,
            Severity.ERROR,
            new Implementation(LogDetector.class,
                    Scope.CLASS_FILE_SCOPE));

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

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

    @Override
    public void checkCall(@NonNull ClassContext context,
                          @NonNull ClassNode classNode,
                          @NonNull MethodNode method,
                          @NonNull MethodInsnNode call) {
        String owner = call.owner;
        if (owner.startsWith("android/util/Log")) {
            context.report(ISSUE,
                    method,
                    call,
                    context.getLocation(call),
                    "You must use our `LogUtils`");
        }
    }
}
LogDetector的作用是检查代码中是否使用Log类,建议使用封装过的"LogUtils"类。
其中代码的意义和功能我们稍后再细说,目前只需要知道继承Detector来实现一个规则就可以了。

然后我们还需要另外一个类,继承IssueRegistry,这个类的作用是将定义规则注册上,代码很简单,如下:
public class LintRegistry extends IssueRegistry {
    @Override
    public List<Issue> getIssues() {
        return Arrays.asList(InitCallDetector.ISSUE, LogDetector.ISSUE);
    }
}
这样还没有完成注册,要完成注册我们还需要在gradle中进行配置。 


六、配置lintjar中gradle

在lintjar的gradle中引入lint的两个库
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.tools.lint:lint-api:24.3.1'
    compile 'com.android.tools.lint:lint-checks:24.3.1'
}
然后,注册我们之前定义好的Registry类

/**
 * Lint-Registry是lint的注册类
 */
jar {
    manifest {
        attributes("Lint-Registry""com.bennu.lintjar.LintRegistry")
    }
}

最后定义一个打包方法,这个会在lintaar中使用
//定义lintJarOutput方法,在lintaar中被调用
configurations {
    lintJarOutput
}

dependencies {
    //lintJarOutput方法,打jar包
    lintJarOutput files(jar)
}
这样lintJar这个module就可以了,下面开始配置lintAar这个module。


七、配置LintAar的gradle

LintAar这个module中不需要写任何代码,它的作用是将lintJar生成的jar包再打包成aar即可。
在LintAar的gradle中添加如下:
// 定义lintJarImport方法,在copyLintJar任务中被调用
configurations {
    lintJarImport
}

dependencies {
    // 调用lintjar的lintJarOutput方法,获得jar包
    lintJarImport project(path: ':lintjar', configuration: 'lintJarOutput')
}

// 调用lintJarImport得到jar包,拷贝到指定目录
task copyLintJar(type: Copy) {
    from (configurations.lintJarImport) {
        rename {
            String fileName ->
                'lint.jar'
        }
    }
    into 'build/intermediates/lint/'
}

// 当项目执行到prepareLintJar这一步时执行copyLintJar方法(注意:这个时机需要根据项目具体情况改变)
project.afterEvaluate {
    def compileLintTask = project.tasks.find{ it.name == 'prepareLintJar'}
    compileLintTask.dependsOn(copyLintJar)
}
定义一个lintJarImport方法,这个方法会调用lintJar中的lintJarOutput方法得到jar包。
新建一个copyLintJar的任务task,目的是将前面得到的jar包拷贝到指定的目录。
最后在afterEvaluate中判断当执行了‘prepareLintJar’这个task时执行copyLintJar这个任务。
注意:‘prepareLintJar’是基于我自己的环境判断出来的,在不同的gradle版本上可能有所不同,请根据实际情况修改copyLintJar的执行时机。

这样lintjar和lintaar这两个module都完成了,下一步将它们依赖进项目。


八、在项目中引入规则

在项目的gradle中引入lintaar
dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation project(':lintaar')
}
同时添加如下配置
android {
    
    ...
    
    lintOptions {
        textReport true // 输出lint报告
        textOutput 'stdout'
        abortOnError false // 遇到错误不停止
    }
}
然后,我们在代码中随便写个Log代码,如下:
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Log.e("test", "use log.e");
    init();
}
接下来就可以测试自定义的规则是否生效了。


九、执行lint

点开gradle窗口,在项目(如:app)下找到lint的相关task,双击执行即可,如下
自动规避代码陷阱——自定义Lint规则
自动规避代码陷阱——自定义Lint规则

执行时就可以中Message窗口中看到相关信息,如下
自动规避代码陷阱——自定义Lint规则
自动规避代码陷阱——自定义Lint规则

可以看到,我们定义的规则已经使用了,找到了一处使用Log类的代码。


十、如何定义规则

上面我们实现了规则并成功使用了,但是对于规则的定义,即Detector类一笔带过,这个其实才是重点,下面我们以LogDetector为例详细说说如何定义自己的规则。

(1)首先创建一个Issue对象,如下:
public static final Issue ISSUE = Issue.create("LogUtilsNotUsed",
        "You must use our `LogUtils`",
        "Logging should be avoided in production for security and performance reasons. Therefore, we created a LogUtils that wraps all our calls to Logger and disable them for release flavor.",
        Category.MESSAGES,
        9,
        Severity.ERROR,
        new Implementation(LogDetector.class,
                Scope.CLASS_FILE_SCOPE));
Issue的create函数有七个参数:
  • id:问题的id
  • briefDescription:问题的简单描述
  • explanation:问题的解释,即如何解决问题
  • category:问题的类型,具体间Category类
  • priority:问题的重要程度,从1到10,10是最重要
  • severity:问题的严重性,有ERROR、WARNING等
  • implementation:问题的实现,Implementation类型

Implementation类的构造函数中第一个参数是定义问题的类;第二个参数是文件范围,即在什么类型的文件中扫描这个问题。

(2)然后通过重写必要的函数来实现一定的查找规则,代码如下:
@Override
public List<String> getApplicableCallNames() {
    return Arrays.asList("v", "d", "i", "w", "e", "wtf");
}

@Override
public List<String> getApplicableMethodNames() {
    return Arrays.asList("v", "d", "i", "w", "e", "wtf");
}
重写了Detector的两个函数,来查找调用的方法名是“v”、“d”等的那部分代码。
Detector有很多Scanner,每个Scanner又有不少函数,这里就不一个个来说了。具体需要使用那个Scanner的哪些函数,需要大家根据自己的情况,结合Detector源码中每个函数的说明来自己判断。这部分网上的资料不多,后续的文章中,我可能会就几个例子讲解一些函数的使用。
在上面的代码中用来两个函数getApplicableCallNames和getApplicableMethodNames。其中getApplicableCallNames是ClassScanner的函数,而getApplicableMethodNames是JavaScanner的函数,两个函数作用是一样的,这两个函数会返回一个字符串列表,检查时当发现方法调用而且调用的方法名在列表中时,就会触发check。

(3)最后重写check函数实现问题逻辑,代码如下:
@Override
public void checkCall(@NonNull ClassContext context,
                      @NonNull ClassNode classNode,
                      @NonNull MethodNode method,
                      @NonNull MethodInsnNode call) {
    String owner = call.owner;
    if (owner.startsWith("android/util/Log")) {
        context.report(ISSUE,
                method,
                call,
                context.getLocation(call),
                "You must use our `LogUtils`");
    }
}
检查每次函数(已过滤)调用,当调用主体是Log类时,使用ClassContext的report函数上报一个问题。
report函数有5个参数:
  • issue:上面定义的ISSUE
  • method:MethodNode类型
  • instruction:MethodInsnNode类型
  • location:问题的位置
  • message:问题描述

通过上面这个简单的事例,一个问题规则的定义基本上就是通过上面三步来完成。


十一、总结

本篇文章主要是讲解一下如何在项目中完成一个自定义lint的引入,并且通过一个简单的例子讲解如何创建一个简单的规则,并且运行查看结果。