android lint check的学习和自定义以及lint语法
阅读原文时间:2021年04月20日阅读:1

lint介绍

android lint是一个静态代码分析工具,通过lint工具,你可以不用边运行边调试,或者通过单元测试进行代码检查,可以检测代码中不规范、不和要求的问题,解决一些潜在的bug。lint工具可以在命令行上使用,也可以在adt中使用。

比如当想检查在manifest.xml中是否有activity,activity中是否包含了launcher activity。如果没有进行错误的警告。

通过lint的这种手段,可以对代码进行规范的控制,毕竟一个团队每个人的风格不同,但是要注意的当然是代码的质量,所以lint可以进行代码的规范和质量控制。

在Android studio还没出来时,lint和Eclipse并不能很好的结合在一起,只能作为一个独立的工具,通过命令行去执行lint检查。

在android studio出现之后,不再建议单独使用lint命令,而是结合gradle进行操作,命令为* ./gradlew lint *进行执行

lint工具通过一下六个方面去检查代码中的问题correctness, security, performance, usability, accessibility, and internationalization。检查的范围包括java文件,xml文件,class文件。

lint工具在sdk16版本之后就带有了,所以在sdk目录/tools/可以找到lint工具。现在建议与gradle一起使用,使用./gradlew lint进行
参考官方文档介绍

使用lint的方法

关于lint的一些命令,可以参考官网,这里简单介绍一些。

  • lint path(项目目录) ——进行项目的lint检查
  • lint –disable id(MissingTranslation,UnusedIds,Usability:Icons) path ——id是lint issue(问题)的标志,检查项目,不包括指定的issue
  • lint –check id path ——利用指定的issue进行项目检查
  • lint –list ——列出所有的issue
  • lint –show id ——介绍指定的issue
  • lint –help ——查看帮助

1.使用android studio自带的lint工具

点击Analyze的Inspect Code选项,即可开启lint检查,在Inspection窗口中可以看到lint检查的结果,lint查询的错误类型包括:

  • Missing Translation and Unused Translation【缺少翻译或者没有】
  • Layout Peformance problems (all the issues the old layoutopt tool used to find, and more)【布局展示问题】
  • Unused resources【没有使用的资源】
  • Inconsistent array sizes (when arrays are defined in multiple configurations)【不一致的数组大小】
  • Accessibility and internationalization problems (hardcoded strings, missing contentDescription, etc)【可访问性和国际化问题,包括硬链接的字符串,缺少contentDescription,等等】
  • Icon problems (like missing densities, duplicate icons, wrong sizes, etc)【图片问题,丢失密度,重复图片,错误尺寸等】
  • Usability problems (like not specifying an input type on a text field)【使用规范,比如没有在一个文本上指定输入的类型】
  • Manifest errors【Manifest.xml中的错误】
  • and so on

android自带的lint规则的更改可以在Setting的Edit选项下选择Inspections(File > Settings > Project Settings),对已有的lint规则进行自定义选择。
参考官方文档

2.使用lint.xml定义检查规则

可以通过lint.xml来自定义检查规则,这里的自定义是指定义系统原有的操作,所以和第一个步骤的结果是一样的,只是可以更方便的配置。

lint.xml生效的位置是要放在项目的根目录下面,lint.xml的示例如下:

<?xml version="1.0" encoding="UTF-8"?>
<lint>
    <!-- 忽略指定的检查 -->
    <issue id="IconMissingDensityFolder" severity="ignore" />

    <!-- 忽略指定文件的指定检查 -->
    <issue id="ObsoleteLayoutParam">
        <ignore path="res/layout/activation.xml" />
        <ignore path="res/layout-xlarge/activation.xml" />
    </issue>

    <!-- 更改检查问题归属的严重性 -->
    <issue id="HardcodedText" severity="error" />
</lint>

参考官方文档

3.自定义lint检查规则

使用google提供的lint规则,确实能找出项目代码中不规范的地方,同时也帮助纠正了许多编译时错误。除了使用google提供的lint规则,我们当然也希望我们能够通过自定义lint规则来为自己的项目进行一个代码检测,包括代码质量把控,查找关键代码位置,或者检测代码规范,比如是否在用了流的地方关闭了流。那么如何自定义lint规则和如何将写好的规则加入已有的lint规则中,就是接下来呀研究的地方。

如何加入已有的lint规则

lint规则是基于JAVA写的,是在AST抽象语法树上去进行一个解析。所以在写lint规则的时候,要学习一下AST抽象语法树。才知道如何去寻找一个类方法和其参数等。以下有两种方法:

(1)所以自定义lint规则应该是一个写好的jar包,jar包生效的位置是在~/.android/lint目录,这个是对于Mac和Linux来说的,对于Windows来说就是在C:/Users/Administrator/.android/lint下,放到这个目录下,lint工具会自动加载这个jar包作为lint的自定义检查规则。

(2)放到lint目录下着实是一件比较麻烦的事情,即使可以用脚本来代替,但是仍然不是一个特别方便的方法。也是由于当android项目直接依赖于lint.jar包时不能起作用,而无法进行直接依赖。
而aar很好的解决了这个问题,aar能够将项目中的资源、class文件、jar文件等都包含,所以通过将lint.jar放入lintaar中,再由项目依赖于lintaar,这时候就可以达到自定义lint检查的目的。

下面就是如何使自定义lint生效的代码示例,使用第二个方法(第二个方法就包括了第一个方法):

主要包括了两个Module一个是lintJar,一个是lintAar。还有一个是测试的app项目。使用自定义lint的主要方式是利用lintAar将lintJar生成的jar包大包成aar,方便引用。所以主要是gradle的操作。
在lintJar中,主要是编写lint规则。它的gradle如下:

apply plugin: 'java'
// 依赖于lint的规则的api
dependencies {
    compile 'com.android.tools.lint:lint-api:24.3.1'
    compile 'com.android.tools.lint:lint-checks:24.3.1'

    testCompile 'com.android.tools.lint:lint-tests:24.3.1'
}

/**
 * Lint-Registry是透露给lint工具的注册类的方法,也就是PermissionIssueRegistry是lint工具的入口,同时也通过这个方法进行打jar包
 */
jar {
    manifest {
        attributes("Lint-Registry": "com.mogujie.PermissionIssueRegistry")
    }
}

defaultTasks 'assemble'
// 定义一个方法lintJarOutput
configurations {
    lintJarOutput
}
// 指定定义方法lintJarOutput的作用,此处是获得调用jar方法后的生成的jar包
dependencies {
    lintJarOutput files(jar)
}

在lintAar中,gradle如下:

apply plugin: 'com.android.library'

android {
    // 此处省略,太多了,就是普通的as生成的模板
}

// 依赖所有jar结尾的jar包
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
}
// 定义方法lintJarImport
configurations {
    lintJarImport
}

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

// 将得到的JAR包复制到目录build/intermediates/lint/下
task copyLintJar(type: Copy) {
    from (configurations.lintJarImport) {
        rename {
            String fileName ->
                'lint.jar'
        }
    }
    into 'build/intermediates/lint/'
}
// 当项目build到compileLint这一步时执行copyLintJar方法
project.afterEvaluate {
    def compileLintTask = project.tasks.find{ it.name == 'compileLint'}
    compileLintTask.dependsOn(copyLintJar)
}

在app项目中,为了方便在命令窗口看到输出的信息,还需要在gradle中添加一段代码:

android {
    lintOptions {
        textReport true // 输出lint报告
        textOutput 'stdout'  
        abortOnError false // 遇到错误不停止
    }
}

参考官方文章
参考的文章

如何编写lint规则

编写lint规则实际上是利用了java的语法树进行一个结构的遍历,是利用java语言进行开发的,使用的Google提供的lint检查api。
先设定一个问题:检查流的close方法是否在finally中被调用。
对于这个问题的想法是
1.先检查流的close方法所在的位置。
2.检查close外层的try/catch所在位置
3.检查try/catch外层的try/catch,并与finally的行数进行判断

lint规则时在libjar项目中进行编写,所以gradle需要依赖lint的相关api。
首先先看下需要定义的注册类,也就是暴露给lint的入口,这个入口需要在gradle中进行注册,请看前面的哦。IssueRegistry就是注册类,继承他,并重写getIssues的方法即可。
MyIssueRegistry.java

/**
 * 把所定义的Issue进行导出,用于提供此jar中所有输出的lint规则,这个类是暴露给lint的一个注册类
 */
public class MyIssueRegistry extends IssueRegistry {
    @Override
    public List<Issue> getIssues() {
        System.out.println("***************************************************");
        System.out.println("**************** lint is starting *****************");
        System.out.println("***************************************************");
        return Arrays.asList(
                CloseDetector.ISSUE
        );
    }

}

Detector就是检查的工具类,通过继承Detector实现自己的检查规则
Issue就是定义的问题的描述,通过Issue可将检查结果进行反馈
Detector.JavaScanner是针对于java文件扫描的接口文件,以及一些方法。除此之外海游XmlScanner, GradleScanner, ClassScanner, BinaryResourceScanner, ResourceFolderScanner, OtherFileScanner。
由于刚刚提出的问题是关于close的,则Detector定义为CloseDetector。
CloseDetector.java

/**
 * 定义代码检查规则
 * 这个是针对try和catch中的finally是否包涵close方法进行一个判断
 * 由于要对java代码进行扫描,因此继承的是javascanner的接口
 */
public class CloseDetector extends Detector implements Detector.JavaScanner {
    public static Issue ISSUE = Issue.create(
            "CloseMethod",
            "close方法应该在finally中调用",
            "close方法应该在finally中调用,防止导致内存泄漏",
            // 这个主要是用于对问题的分类,不同的问题就可以集中在一起显示。
            Category.CORRECTNESS,
            // 优先级
            6,
            // 定义查找问题的严重级别
            Severity.ERROR,
            // 提供处理该问题的Detector和该Detector所关心的资源范围。当系统生成了抽象语法树(Abstract syntax tree,简称AST),或者遍历xml资源时,就会调用对应Issue的处理器Detector。
            new Implementation(CloseDetector.class,
                    Scope.JAVA_FILE_SCOPE)
    );
    // 限定关心的方法的调用类
    public static final String[] sSupportSuperType = new String[]{
            "java.io.InputStream", "java.io.OutputStream", "android.database.Cursor"
    };

    /**
     * 只关心名是close的方法
     *
     * @return
     */
    @Override
    public List<String> getApplicableMethodNames() {
        return Collections.singletonList("close");
    }

    /**
     * 该方法调用时,会传入代表close方法被调用的节点MethodInvocation,以及所在java文件的上下文JavaContext,
     * 还有AstVisitor。由于我们没有重写createJavaVisitor方法,所以不用管AstVisitor。
     * MethodInvocation封装了close被调用处的代码,而结合JavaContext对象,即可寻找对应的上下文,来帮助我们判断条件。
     *
     * @param context
     * @param visitor
     * @param node
     */
    @Override
    public void visitMethod(JavaContext context, AstVisitor visitor, MethodInvocation node) {
        // 判断类型,看下所监测的资源是否是我们定义的相关资源
        // 通过JavaContext的resolve的方法,传入node节点,由于所有的AST树上的节点都继承自NODE,所以可以通过node去找到class
        JavaParser.ResolvedMethod method = (JavaParser.ResolvedMethod) context.resolve(node);
        JavaParser.ResolvedClass clzz = method.getContainingClass();
        boolean isSubClass = false;
        for (int i = 0; i < sSupportSuperType.length; i++) {
            if (clzz.isSubclassOf(sSupportSuperType[i], false)) {
                isSubClass = true;
                break;
            }
        }
        if (!isSubClass) super.visitMethod(context, visitor, node);
        /**
         * 查找try和block的信息
         * 在AST中,close 代码节点应该是try的一个子孙节点(try是语法上的block),所以从close代码节点向上追溯,
         * 可以找到对应的try,而Node对象本来就有getParent方法,所以可以递归调用该方法来找到Try节点(这也是一个节点),
         * 或者调用JavaContext的查找限定parent类型的方法:
         */
        Try fTryBlock = context.getParentOfType(node, Try.class);
        int fLineNum = context.getLocation(fTryBlock).getStart().getLine();
        System.out.println("    fLineNum=" + fLineNum);

        /**
         * 如果close在try模块中,接着就要对try进行向上查找,看try是否被包裹在try中,同时,是否处于finally中,try节点
         * 有一个astFinally的方法,可以得到finally的节点,只要判断节点的位置,既可以实现判断close是否在finally中
         */
        Try sTryBlock = context.getParentOfType(fTryBlock, Try.class);
        Block finaBlock = sTryBlock.astFinally();
        int sLineNum = context.getLocation(finaBlock).getStart().getLine();
        System.out.println("    sLineNum=" + sLineNum);
        /**
         * 若我们确定了close是在try 块中,且try块不在finally里,那么就需要触发Issue,这样在html报
         * 告中就可以找到对应的信息了
         * 一个莫名的bug,不能再这里写成
         * if (fLineNum < sLineNum){
         *     context.report(ISSUE, node, context.getLocation(node), "请在finally中调用close");
         * }
         * 否则再直接运行Analyze下Inspect选项后,在inspection窗口中没有办法看到Error from custom lint Check
         * 的错误信息
         */
        if (fLineNum > sLineNum) {
            return;
        } else {
            context.report(ISSUE, node, context.getLocation(node), "please use close method in finally");
        }
    }


}

通过短短的示例学习一下lint规则的编写。由于网上关于lint规则的编写还是比较少的,所以要想了解lint规则,还需要多研究下google的源代码,从源代码中进行学习。

官方源码

注意

在使用android studio中的lint检查的工具的时候,有时候需要退出,然后重新启动之后,我门加入的lint.aar才会在系统中生效。
同时也可以在命令窗口上实时调试,请看文中所述。

项目demo下载


手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器