Android开发android

Android Studio 工具:Lint 代码扫描工具(含自

2017-12-18  本文已影响0人  凯玲之恋

什么是 Lint

Android Lint 是 SDK Tools 16(ADT 16)开始引入的一个代码扫描工具,通过对代码进行静态分析,可以帮助开发者发现代码质量问题和提出一些改进建议。除了检查 Android 项目源码中潜在的错误,对于代码的正确性、安全性、性能、易用性、便利性和国际化方面也会作出检查。
Android Lint 作为项目的代码检测工具,是因为它具有以下几个特性:

一、开始使用

Android Lint 的工作过程比较简单,一个基础的 Lint 过程由 Lint Tool(检测工具),Source Files(项目源文件) 和 lint.xml(配置文件) 三个部分组成,Lint Tool 读取 Source Files,根据 lint.xml 配置的规则(issue)输出结果(如下图)。


123.png

1.1Android studio使用

Android Studio 中,Android Lint 已经被集成,只需要点击菜单 —— Analyze —— Inspect Code 即可运行 Android Lint,在弹出的对话框中可以设置执行 Lint 的范围,可以选择整个项目,也可以只选择当前的子模块或者其他自定义的范围:


123.png

检查完毕后会弹出 Inspection 的控制台,并在其中列出详细的检查结果:


123.png

如上图所展示的,Android Lint 对检查的结果进行了分类,同一个规则(issue)下的问题会聚合,其中针对 Android 的规则类别会在分类前说明是 Android 相关的,主要是六类:

1.2配置

对于执行 Lint 操作的相关配置,是定义在 gradle 文件的 lintOptions 中,可定义的选项及其默认值

android {
    lintOptions {
        // 设置为 true,则当 Lint 发现错误时停止 Gradle 构建
        abortOnError false
        // 设置为 true,则当有错误时会显示文件的全路径或绝对路径 (默认情况下为true)
        absolutePaths true
        // 仅检查指定的问题(根据 id 指定)
        check 'NewApi', 'InlinedApi'
        // 设置为 true 则检查所有的问题,包括默认不检查问题
        checkAllWarnings true
        // 设置为 true 后,release 构建都会以 Fatal 的设置来运行 Lint。
        // 如果构建时发现了致命(Fatal)的问题,会中止构建(具体由 abortOnError 控制)
        checkReleaseBuilds true
        // 不检查指定的问题(根据问题 id 指定)
        disable 'TypographyFractions','TypographyQuotes'
        // 检查指定的问题(根据 id 指定)
        enable 'RtlHardcoded','RtlCompat', 'RtlEnabled'
        // 在报告中是否返回对应的 Lint 说明
        explainIssues true
        // 写入报告的路径,默认为构建目录下的 lint-results.html
        htmlOutput file("lint-report.html")
        // 设置为 true 则会生成一个 HTML 格式的报告
        htmlReport true
        // 设置为 true 则只报告错误
        ignoreWarnings true
        // 重新指定 Lint 规则配置文件
        lintConfig file("default-lint.xml")
        // 设置为 true 则错误报告中不包括源代码的行号
        noLines true
        // 设置为 true 时 Lint 将不报告分析的进度
        quiet true
        // 覆盖 Lint 规则的严重程度,例如:
        severityOverrides ["MissingTranslation": LintOptions.SEVERITY_WARNING]
        // 设置为 true 则显示一个问题所在的所有地方,而不会截短列表
        showAll true
        // 配置写入输出结果的位置,格式可以是文件或 stdout
        textOutput 'stdout'
        // 设置为 true,则生成纯文本报告(默认为 false)
        textReport false
        // 设置为 true,则会把所有警告视为错误处理
        warningsAsErrors true
        // 写入检查报告的文件(不指定默认为 lint-results.xml)
        xmlOutput file("lint-report.xml")
        // 设置为 true 则会生成一个 XML 报告
        xmlReport false
        // 将指定问题(根据 id 指定)的严重级别(severity)设置为 Fatal
        fatal 'NewApi', 'InlineApi'
        // 将指定问题(根据 id 指定)的严重级别(severity)设置为 Error
        error 'Wakelock', 'TextViewEdits'
        // 将指定问题(根据 id 指定)的严重级别(severity)设置为 Warning
        warning 'ResourceAsColor'
        // 将指定问题(根据 id 指定)的严重级别(severity)设置为 ignore
        ignore 'TypographyQuotes'
    }
}

lint.xml 这个文件则是配置 Lint 需要禁用哪些规则(issue),以及自定义规则的严重程度(severity),lint.xml 文件是通过 issue 标签指定对一个规则的控制,在项目根目录中建立一个 lint.xml 文件后 Android Lint 会自动识别该文件,在执行检查时按照 lint.xml 的内容进行检查。如上面提到的那样,开发者也可以通过 lintOptions 中的 lintConfig 选项来指定配置文件。一个 lint.xml 示例如下:

123.png
issue 标签中使用 id 指定一个规则,severity="ignore" 则表明禁用这个规则。需要注意的是,某些规则可以通过 ignore 标签指定仅对某些属性禁用,例如上面的 Deprecated,表示检查是否有使用不推荐的属性和方法,而在 issue 标签中包裹一个 ignore 标签,在 ignore 标签的 regexp 属性中使用正则表达式指定了 singleLine,则表明对 singleLine 这个属性屏蔽检查。

另外开发者也可以使用 @SuppressLint(issue id) 标注针对某些代码忽略某些 Lint 检查,这个标注既可以加到成员变量之前,也可以加到方法声明和类声明之前,分别针对不同范围进行屏蔽。

二、展开叙述

2.2.1Correctness (不是全部,常见的)

2.2.2Internationalization

2.2.3Performance

2.2.4Security

2.2.6Usability

2.2其他类型

Class structure 类结构
Code maturity issues 代码成熟度问题
Code style issues 代码样式问题
Compiler issues 编译器问题
Control flow issues 控制流量问题
Data flow issues 数据流问题
Declaration redundancy 声明冗余
Error handling 错误处理
General 一般
Imports 进口
J2ME issues J2ME问题
Java 5 Java 5
Java 7 Java 7
Java language level migration aids Java语言级别的迁移辅助
Javadoc issues Javadoc问题
Naming conventions 命名约定
Numeric issues 数字问题
Performance issues 性能问题
Probable bugs 可能的错误
Properties Files 属性文件
Spelling 拼字
Style 样式
Verbose or redundant code constructs 详细或冗余的代码结构
XML XML

2.2.1Class structure

Field can be local字段可以是本地的
Parameter can be local参数可以是本地的
'private' method declared 'final'
'static' method declared 'final''

2.2.2Code maturity issues 代码成熟度问题

Deprecated API usage不推荐使用API
Deprecated member is still used不推荐使用的成员仍在使用

2.2.3Code style issues 代码样式问题

Unnecessary enum modifier不必要的枚举修饰符
Unnecessary interface modifier不必要的界面修饰符
Unnecessary semicolon不必要的分号
private public

2.2.4Compiler issues 编译器问题

Unchecked warning未经检查的警告

2.2.5Control flow issues 控制流问题

Double negation双重否定
Pointless boolean expression无意义的布尔表达式
Redundant 'if' statement冗余“if”语句
Redundant conditional expression冗余的条件表达式
Simplifiable boolean expression简化布尔表达式
Simplifiable conditional expression简化条件表达式
Unnecessary 'return' statement不必要的“return”声明
return;

2.2.6Data flow issues 数据流问题

Boolean method is always inverted布尔方法总是倒置的
Redundant local variable冗余局部变量

2.2.7Declaration redundancy 声明冗余

Access static member via instance reference通过实例引用访问静态成员
this.minsize = this.maxsize;

Actual method parameter is the same constant实际的方法参数是相同的常量
Actual value of parameter ''register'' is always ''true''

Declaration access can be weaker声明访问权限可以再弱
Can be private

Declaration can have final modifier宣言可以有最终的修改

Duplicate throws重复抛出

Empty method空方法

Method can be void方法可以是无效的

Method returns the same value方法返回相同的值
All implementations of this method always return '3'

Redundant throws clause冗余抛出子句
The declared exception 'UnsupportedEncodingException' is never thrown

Unnecessary module dependency不必要的模块依赖
Unused declaration未使用的声明(方法,变量)

2.2.8Error handling 错误处理

Caught exception is immediately rethrown捕获到的异常立即被重新抛出
Empty 'catch' block空'catch'块
'return' inside 'finally' block在'finally'块中'返回'
'throw' inside 'finally' block在“finally”块内“抛出”

2.2.9General

Annotator注解者
Default File Template Usage默认文件模板的用法

2.2.10Imports 导入

Unused import没有用到的导入

2.2.11J2ME issues J2ME问题

'if'语句可以用&&或||代替 表达

2.2.12Java 5 Java 5

'for' loop replaceable with 'foreach''for'循环可替换为'foreach'
'indexOf()' expression is replaceable with 'contains()''indexOf()'表达式可以用'contains()'来替换
'StringBuffer' may be 'StringBuilder''StringBuffer'可能是'StringBuilder'
Unnecessary boxing不必要的装箱
Unnecessary unboxing不必要的拆箱
'while' loop replaceable with 'foreach''while'循环可以替换'foreach'

2.2.13Java 7 Java 7

Explicit type can be replaced with <>显式类型可以用<>来替换
'试试最后'用资源替换'试用'

2.2.14Java language level migration aids Java语言级别的迁移辅助

'if' replaceable with 'switch'

2.2.15Javadoc issues Javadoc问题

Dangling Javadoc comment 摇摇晃晃的Javadoc评论
Declaration has Javadoc problems 宣言有Javadoc问题
Declaration has problems in Javadoc 声明在Javadoc引用中有问题

2.2.16Naming conventions 命名约定

2.2.17Numeric issues 数字问题

数字溢出 Numeric overflow
八进制整数 Octal integer
无意义的算术表达式 Pointless arithmetic expression

2.2.18Performance issues 性能问题

Redundant 'String.toString()' 冗余'String.toString()'
Redundant 'substring(0)' call 冗余'substring(0)'调用
Redundant call to 'String.format()' 冗余调用'String.format()'
String concatenation as argument to 'StringBuffer.append()' call 字符串连接作为“StringBuffer.append()”调用的参数
String concatenation in loop 循环中的字符串连接
'StringBuffer' can be replaced with 'String' 'StringBuffer'可以替换为'String'

2.2.19Probable bugs 可能的错误

Collection added to self Collection添加到自我
Constant conditions & exceptions 不变的条件和例外
Mismatched query and update of collection 不匹配的查询和集合更新
Mismatched query and update of StringBuilder 不匹配的查询和更新的StringBuilder
@NotNull/@Nullable problems @NotNull / @可空问题
Result of method call ignored 方法调用的结果被忽略
Statement with empty body 声明与空的实现
String comparison using '==', instead of 'equals()' 使用'=='进行字符串比较,而不是'equals()'
Suspicious collections method calls 可疑collections方法调用
Suspicious variable/parameter name combination 可疑变量/参数名称组合
Unused assignment 没用的赋值操作

2.2.20Properties Files 属性文件

Unused Property未使用的属性

2.2.21Spelling 拼字

2.2.22Style 样式

Unnecessary semicolon没必要的分号

2.2.23Verbose or redundant code constructs 详细或冗余的代码结构

Redundant array creation创建冗余阵列
Redundant type cast冗余类型转换

2.2.24XML XML

Deprecated API usage in XML 在XML中不推荐使用API
Unbound XML namespace prefix 未绑定的XML名称空间前缀
Unused XML schema declaration 未使用的XML模式声明
XML highlighting XML突出显示
XML tag empty body XML标签为空的正文

三、自定义lint

3.1创建工程

创建自定义 Lint 需要创建一个 Java 项目,项目中需要引入 Android Lint 的包,项目的 build.gradle 如下:

apply plugin: 'java'
 
configurations {
    lintChecks
}
 
dependencies {
    compile "com.android.tools.lint:lint-api:25.1.2"
    compile "com.android.tools.lint:lint-checks:25.1.2"
 
    lintChecks files(jar)
}
 
jar {
    manifest {
        attributes('Lint-Registry': 'com.qmuiteam.qmui.lint.QMUIIssueRegistry')
    }
}

其中 lint-api 是 Android Lint 的官方接口,基于这些接口可以获取源代码信息,从而进行分析,lint-checks 是官方已有的检查规则。Lint-Registry 表示给自定义规则注册,以及打包为 jar.

3.2 Detector

Detector 是自定义规则的核心,它的作用是扫描代码,从而获取代码中的各种信息,然后基于这些信息进行提醒和报告,在本场景中,我们需要扫描 Java 代码,找到 getDrawable 方法的调用,然后分析其中传入的 Drawable 是否为 Vector Drawable,如果是则需要进行报告,完整代码如下:

/**
 * 检测是否在 getDrawable 方法中传入了 Vector Drawable,在 4.0 及以下版本的系统中会导致 Crash
 */
 
public class QMUIJavaVectorDrawableDetector extends Detector implements Detector.JavaScanner {
 
    public static final Issue ISSUE_JAVA_VECTOR_DRAWABLE =
            Issue.create("QMUIGetVectorDrawableWithWrongFunction",
                    "Should use the corresponding method to get vector drawable.",
                    "Using the normal method to get the vector drawable will cause a crash on Android versions below 4.0",
                    Category.ICONS, 2, Severity.ERROR,
                    new Implementation(QMUIJavaVectorDrawableDetector.class, Scope.JAVA_FILE_SCOPE));
 
    @Override
    public List<String> getApplicableMethodNames() {
        return Collections.singletonList("getDrawable");
    }
 
    @Override
    public void visitMethod(@NonNull JavaContext context, AstVisitor visitor, @NonNull MethodInvocation node) {
 
        StrictListAccessor<Expression, MethodInvocation> args = node.astArguments();
        if (args.isEmpty()) {
            return;
        }
 
        Project project = context.getProject();
        List<File> resourceFolder = project.getResourceFolders();
        if (resourceFolder.isEmpty()) {
            return;
        }
 
        String resourcePath = resourceFolder.get(0).getAbsolutePath();
        for (Expression expression : args) {
            String input = expression.toString();
            if (input != null && input.contains("R.drawable")) {
                // 找出 drawable 相关的参数
 
                // 获取 drawable 名字
                String drawableName = input.replace("R.drawable.", "");
                try {
                    // 若 drawable 为 Vector Drawable,则文件后缀为 xml,根据 resource 路径,drawable 名字,文件后缀拼接出完整路径
                    FileInputStream fileInputStream = new FileInputStream(resourcePath + "/drawable/" + drawableName + ".xml");
                    BufferedReader reader = new BufferedReader(new InputStreamReader(fileInputStream));
                    String line = reader.readLine();
                    if (line.contains("vector")) {
                        // 若文件存在,并且包含首行包含 vector,则为 Vector Drawable,抛出警告
                        context.report(ISSUE_JAVA_VECTOR_DRAWABLE, node, context.getLocation(node), expression.toString() + " 为 Vector Drawable,请使用 getVectorDrawable 方法获取,避免 4.0 及以下版本的系统产生 Crash");
                    }
                    fileInputStream.close();
                } catch (Exception ignored) {
                }
            }
        }
    }
}

QMUIJavaVectorDrawableDetector 继承于 Detector,并实现了 Detector.JavaScanner 接口,实现什么接口取决于自定义 Lint 需要扫描什么内容,以及希望从扫描的内容中获取何种信息。Android Lint 提供了大量不同范围的 Detector:

不同的接口定义了各种方法,实现自定义 Lint 实际上就是实现 Detector 中的各种方法,在上面的例子中,getApplicableMethodNames 的返回值指定了需要被检查的方法,visitMethod 则可以接收检查到的方法对应的信息,这个方法包含三个参数,其作用分别是:

值得注意的是,在例子中我们并没有直接实例 Drawable,然后通过 Drawable 的方法判断是否为 Vector Drawable,而是通过较为繁琐的步骤检查文件内容,这是因为 Android Lint 的项目是一个纯 Java 项目,不能使用 android.graphics 等包,因而开发时会比较繁琐。

3.3 Issue

在检查出问题需要进行报告时,context.report 方法中传入了一个 ISSUE_JAVA_VECTOR_DRAWABLE,这里的"issue"是声明一个规则,因此自定义一个 Lint 规则就需要定义一个 issue。issue 由类方法 Issue.create 创建,参数如下:

3.4 Category

Category 用于给 Issue 分类,系统已经提供了几个常用的分类,系统 Issue(即 Android Lint 自带的检查规则)也是使用这个 Category:

public class QMUICategory {
    public static final Category UI_SPECIFICATION = Category.create("UI Specification", 105);
}

使用如下:

public static final Issue ISSUE_JAVA_VECTOR_DRAWABLE =
       Issue.create("QMUIGetVectorDrawableWithWrongFunction",
               "Should use the corresponding method to get vector drawable.",
               "Using the normal method to get the vector drawable will cause a crash on Android versions below 4.0",
               QMUICategory.UI_SPECIFICATION, 2, Severity.ERROR,
               new Implementation(QMUIJavaVectorDrawableDetector.class, Scope.JAVA_FILE_SCOPE));

3.5 Registry

创建自定义 Lint 的最后一步是 “Lint-Registry”,如前面所述,build.gradle 中需要声明 Regisry 类,打包成 jar:

jar {
    manifest {
        attributes('Lint-Registry': 'com.qmuiteam.qmui.lint.QMUIIssueRegistry')
    }
}

而 registry 类中则是注册创建好的 Issue,以 QMUIIssueRegistry 为例:

public final class QMUIIssueRegistry extends IssueRegistry {
   @Override public List<Issue> getIssues() {
       return Arrays.asList(
               QMUIFWordDetector.ISSUE_F_WORD,
               QMUIJavaVectorDrawableDetector.ISSUE_JAVA_VECTOR_DRAWABLE,
               QMUIXmlVectorDrawableDetector.ISSUE_XML_VECTOR_DRAWABLE,
               QMUIImageSizeDetector.ISSUE_IMAGE_SIZE,
               QMUIImageScaleDetector.ISSUE_IMAGE_SCALE
       );
   }
}

QMUIIssueRegistry 继承与 IssueRegistry,IssueRegistry 中注册了 Android Lint 自带的 Issue,而自定义的 Issue 则可以通过 getIssues 系列方法传入。

到这一步,这个用于自定义 Lint 的 Java 项目编写完毕了。

3.6 接入项目

Google 官方的方案是把 jar 文件放到 ~/.android/lint/,如果本地没有 lint 目录可以自行创建,这个使用方式较为简单,但也使得 Android Lint 作用于本地所有的项目,不大灵活。
在主项目中新建一个 Module,打包为 aar,把 jar 文件放到该 aar 中,这样各个项目可以以 aar 的方式自行引入自定义 Lint,比较灵活,项目之间不会造成干扰。
Module 的 build.gradle 内容如下(以 QMUI Lint 为例):

apply plugin: 'com.android.library'
 
configurations {
    lintChecks
}
 
dependencies {
    lintChecks project(path: ':qmuilintrule', configuration: 'lintChecks')
}
 
task copyLintJar(type: Copy) {
    from(configurations.lintChecks) {
        rename { 'lint.jar' }
    }
    into 'build/intermediates/lint/'
}
 
project.afterEvaluate {
    def compileLintTask = project.tasks.find { it.name == 'compileLint' }
    compileLintTask.dependsOn(copyLintJar)
}

其中 qmuilintrule 是自定义 Lint 规则的 Module,这样这个需要进行 aar 打包的 Module 即可获取到 jar 文件,并放到 build/intermediates/lint/ 这个路径中。把 aar 发布到 Bintray 后,需要用到自定义 Lint 的地方只需要引入 aar 即可,例如:

compile 'com.qmuiteam:qmuilint:1.0.0'

另外需要注意,在编写自定义规则的 Lint 代码时,编写后重新构建 gradle,新代码也不一定生效,需要重启 Android Studio 才能确保新代码已经生效。

Android 性能优化:使用 Lint 优化代码、去除多余资源
Android Lint 实践 —— 简介及常见问题分析

上一篇 下一篇

猜你喜欢

热点阅读