Android Studio 工具:Lint 代码扫描工具(含自定义lint)

什么是 Lint

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

  • 已经被集成到 Android Studio,使用方便。
  • 能在编写代码时实时反馈出潜在的问题。
  • 可以自定义规则。Android Lint 本身包含大量已经封装好的接口,能提供丰富的代码信息,开发者可以基于这些信息进行自定义规则的编写。
    先来一张神图
    lint后具体信息.png

一、开始使用

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 相关的,主要是六类:

  • Accessibility 无障碍,例如 ImageView 缺少contentDescription 描述,String 编码字符串等问题。
  • Correctness 正确性
  • Internationalization 国际化,如字符缺少翻译等问题。
  • Performance 性能,例如在 onMeasure、onDraw 中执行 new,内存泄露,产生了冗余的资源,xml 结构冗余等。
  • Security 安全性,例如没有使用 HTTPS 连接 Gradle,AndroidManifest 中的权限问题等。
  • Usability 易用性,例如缺少某些倍数的切图,重复图标等。
    其他的结果条目则是针对 Java 语法的问题,另外每一个问题都有区分严重程度(severity),从高到底依次是:
    Fatal
    Error
    Warning
    Information
    Ignore
    其中 Fatal 和 Error 都是指错误,但是 Fatal 类型的错误会直接中断 ADT 导出 APK,更为严重。
    在结果列表中点击一个条目,可以看到详细的源文件名和位置,以及命中的错误规则(issue)、解决方案或者屏蔽提示
    除了直接在菜单中运行 Lint 外,大部分问题代码在编写时 Android Studio 就会给出提醒:

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 (不是全部,常见的)

  • Appcompat Custom Widgets
    Appcompat自定义小部件一般会让你继承自 android.support.v7.widget.AppCompat...
    不要直接扩展android.widget类,而应该扩展android.support.v7.widget.AppCompat中的一个委托类。

  • Attribute unused on older versions
    旧版本未使用的属性 针对具有minSdkVersion
    这不是一个错误; 应用程序将简单地忽略该属性。
    可以选择在layout-vNN文件夹中创建一个布局的副本 将在API NN或更高版本上使用,您可以利用更新的属性。

  • Class is not registered in the manifest
    类未在清单中注册
    Activities, services and content providers should be registered in the AndroidManifest.xml file

  • Combining Ellipsize and Maxlines
    Ellipsize和Maxlines相结合
    结合ellipsize和maxLines = 1可能导致某些设备崩溃。 早期版本的lint建议用maxLines = 1替换singleLine = true,但在使用ellipsize时不应该这样做

  • Extraneous text in resource files
    资源文件中的无关文本

  • Hardcoded reference to /sdcard
    硬编码参考/ SD卡
    代码不应该直接引用/ sdcard路径; 而是使用:Environment.getExternalStorageDirectory().getPath()
    不要直接引用/ data / data /路径; 它可以在多用户场景中有所不同。

  • Implied default locale in case conversion
    在转换的情况下默认的默认语言环境

  • Implied locale in date format
    隐含的日期格式的区域设置
    调用者都应该使用getDateInstance(),getDateTimeInstance()或getTimeInstance()来获得适合用户语言环境的SimpleDateFormat的现成实例。

  • Likely cut & paste mistakes
    可能剪切和粘贴错误
    剪切和粘贴调用findViewById但忘记更新R.id字段的情况。 有可能你的代码只是(冗余)重复查找字段

  • Mismatched Styleable/Custom View Name
    不匹配的样式/自定义视图名称
    自定义视图的惯例是使用名称与自定义视图类名称相匹配的声明样式。

  • Missing Permissions
    缺少权限

  • Nested scrolling widgets
    嵌套的滚动小部件
    A scrolling widget such as a ScrollView should not contain any nested scrolling widgets since this has various usability issues

  • Obsolete Gradle Dependency 已过时的Gradle依赖关系

  • Target SDK attribute is not targeting latest version 目标SDK属性未定位到最新版本

  • Using 'px' dimension 使用“px”维度

  • Using android.media.ExifInterface 使用android.media.ExifInterface
    旧版的有一些漏洞,使用支持库中的

  • Using dp instead of sp for text sizes 使用dp代替文本大小的sp

  • Using Private APIs 使用私有API

  • Using private resources 使用私人资源

2.2.2Internationalization

  • Hardcoded text
    硬编码文本
    直接在布局文件中对文本属性进行硬编码是有缺陷的
    should use @string resource

  • Overlapping items in RelativeLayout
    在RelativeLayout中重叠项目
    如果相对布局的文本或按钮项左右对齐,则由于本地化的文本扩展,它们可以相互重叠,除非它们具有toEndOf / toStartOf之类的相互约束。

  • Padding and margin symmetry
    填充和边缘对称
    如果您在布局的左侧指定填充或边距,则应该也可以在右侧指定填充(反之亦然),以便从右到左布局对称。

  • TextView Internationalization
    TextView国际化
    永远不要调用Number#toString()来格式化数字; 它不会正确处理分数分隔符和区域特定的数字
    使用具有适当格式规范(%d或%f)的String#格式
    不要传递字符串(例如“Hello”)来显示文本。 硬编码文本无法正确翻译成其他语言,考虑使用Android资源字符串
    不要通过连接文本块来构建消息。 这样的消息不能被正确翻译。

  • Using left/right instead of start/end attributes
    使用左/右而不是开始/结束属性

2.2.3Performance

  • Handler reference leaks
    handler导致的泄漏
    由于该Handler被声明为内部类,所以可以防止外部类被垃圾收集。 如果处理程序对主线程以外的线程使用Looper或MessageQueue,则不存在问题。 如果处理程序正在使用主线程的Looper或MessageQueue,则需要修复Handler声明,
    解决:将Handler声明为静态类; 在外部类中,实例化WeakReference到外部类,并在实例化Handler时将此对象传递给Handler; 使用WeakReference对象来引用外部类的所有成员。
  • HashMap can be replaced with SparseArray
    HashMap可以用SparseArray替换
    对于键类型为integer的映射,使用Android SparseArray API通常效率更高。

  • Inefficient layout weight
    低效的布局权重
    当LinearLayout中只有一个控件定义了一个权重时,为它指定一个0dp的宽度/高度会更有效率,因为它将吸收所有的剩余空间。 如果声明的宽度/高度为0dp,则不必首先测量其自己的大小。

  • Layout has too many views
    布局有太多的意见
    在单个布局中使用太多的视图对性能不利。 考虑使用复合绘图或其他技巧来减少此布局中的视图数量。 最大视图数量默认为80,但可以使用环境变量ANDROID_LINT_MAX_VIEW_COUNT进行配置。

  • Layout hierarchy is too deep
    布局层次太深
    嵌套太多的布局对性能不利。 考虑使用更平坦的布局(比如RelativeLayout或GridLayout)。默认的最大深度是10,但可以使用环境变量ANDROID_LINT_MAX_DEPTH进行配置。

  • Memory allocations within drawing code
    内存分配在绘图代码
    应该避免在绘图或布局操作中分配对象。 这些被频繁地调用,所以平滑的UI可以被对象分配造成的垃圾收集暂停中断。 通常处理的方式是预先分配所需的对象,并为每个绘图操作重新使用它们。 有些方法代表您分配内存(如Bitmap.create),并且应该以相同的方式处理这些内存。

  • Missing @Keep for Animated Properties
    属性动画缺少@Keep
    当你使用属性动画师时,属性可以通过反射来访问。 这些方法应该使用@Keep注释,以确保在发布构建期间,这些方法不会被视为未被使用和删除,或者被视为内部的,并被重新命名为更短。 这个检查还会标记出其他可能遇到的反射问题,比如缺少属性,错误的参数类型等等。

  • Missing baselineAligned attribute
    缺少baselineAligned属性
    当使用LinearLayout在嵌套布局之间按比例分配空间时,应关闭基线对齐属性以使布局计算速度更快。

  • Node can be replaced by a TextView with compound drawables
    节点可以用复合可绘制的TextView替换
    包含ImageView和TextView的LinearLayout可以更有效地处理为复合可绘制(单个TextView,使用drawableTop,drawableLeft,drawableRight和/或drawableBottom属性在文本旁边绘制一个或多个图像)。 如果这两个小部件彼此之间有空白,则可以用drawablePadding属性替换。

  • Obsolete layout params
    过时的布局参数

  • Obsolete SDK_INT Version Check
    已过时的SDK_INT版本检查
    此检查标志版本检查不是必需的,因为minSdkVersion(或周围已知的API级别)已经至少与检查的版本一样高。
    它还会在-vNN文件夹中查找资源,如版本限定符小于或等于minSdkVersion的values-v14,其中内容应合并到最佳文件夹中。

  • Static Field Leaks
    静态常量--持有fragment及activity的引用
    非静态内部类对其外部类具有隐式引用。
    如果该外部类是例如fragment或activity,如果长时间运行的处理程序/加载程序/任务将持有对该activity的引用,长时间没有被回收掉。

  • Useless parent layout
    无用的父母布局
    具有没有兄弟的孩子的布局不是滚动视图或根布局,并且没有背景,可以被移除并且其子节点直接移动到父节点以获得更平坦和更高效的布局分层结构。

  • View Holder Candidates
    查看持有人候选人
    Should use View Holder pattern

2.2.4Security

  • Cipher.getInstance with ECB
    Cipher.getInstance与ECB
    不应使用ECB作为cipher mode或不设置cipher mode来调用Cipher#getInstance,因为android上的默认模式是ECB,这是不安全的。(加解密)

  • Content provider does not require permission
    内容提供者不需要权限
    内容提供程序默认导出,系统上的任何应用程序都可能使用它们来读取和写入数据。 如果内容提供者提供对敏感数据的访问,则应该通过在清单中指定export = false来保护它,或者通过可以授予其他应用程序的权限来保护它。

  • Exported service does not require permission
    导出的服务不需要权限
    导出的服务(设置了exported = true或者包含intent-filter并且不指定exported = false的服务)应该定义一个实体为了启动服务或绑定到服务而必须拥有的权限。 没有这个,任何应用程序都可以使用此服务。

  • Hardware Id Usage
    硬件ID使用情况
    不建议使用这些设备标识符,除了高价值欺诈预防和高级电话使用情况。
    getLine1Number获取手机号,getDeviceId设备IMEI,getMacAddressMAC地址

  • Incorrect constant
    不正确的常量

  • Insecure TLS/SSL trust manager
    不安全的TLS / SSL信任管理器

  • Missing @JavascriptInterface on methods
    缺少@JavascriptInterface方法

  • openFileOutput() or similar call passing MODE_WORLD_READABLE
    openFileOutput()或类似的调用传递MODE_WORLD_READABLE

  • openFileOutput() or similar call passing MODE_WORLD_WRITEABLE
    openFileOutput()或类似的调用传递MODE_WORLD_WRITEABLE
    在某些情况下,应用程序可以编写世界可写文件,但应仔细检查这些文件以确保它们不包含私人数据,并且如果文件被恶意应用程序修改,则不会欺骗或破坏应用程序。

  • Receiver does not require permission
    接收者不需要许可

  • Using setJavaScriptEnabled 使用setJavaScriptEnabled
    如果您不确定您的应用程序确实需要JavaScript支持,那么您的代码不应该调用setJavaScriptEnabled。

2.2.6Usability

  • Button should be borderless
    按钮应该是无边界的
    两个 Buttons 放在一个布局里会被判断为按钮栏,需要添加样式取消它的边框
    在 Buttons 上添加属性 style="?android:attr/buttonBarButtonStyle" 。系统提示也可以在按钮的父布局上添加 style="? android:attr/buttonBarStyle" 属性
  • Ellipsis string can be replaced with ellipsis character
    省略号字符串可以用省略号字符替换
    Replace "..." with ellipsis character (…, …) ?

  • Hyphen can be replaced with dash
    连字符可以用短划线代替
    Replace "-" with an "en dash" character (–, –) ?

  • Missing View constructors for XML inflation
    缺少XML通货膨胀的视图构造函数

  • Text size is too small
    文字太小
    避免使用小于12sp的尺寸。小于12sp的字体会太小导致用户看不清

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:

  • Detector.BinaryResourceScanner 针对二进制资源,例如 res/raw 等目录下的各种 Bitmap
  • Detector.ClassScanner 相对于 Detector.JavaScanner,更针对于类进行扫描,可以获取类的各种信息
  • Detector.GradleScanner 针对 Gradle 进行扫描
  • Detector.JavaScanner 针对 Java 代码进行扫描
  • Detector.ResourceFolderScanner 针对资源目录进行扫描,只会扫描目录本身
  • Detector.XmlScanner 针对 xml 文件进行扫描
  • Detector.OtherFileScanner 用于除上面6种情况外的其他文件

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

  • context 这里的 context 是一个 JavaContext,主要的功能是获取主项目的信息,以及进行报告(包括获取需要被报告的代码的位置等)。
  • visitor visitor 是一个 ASTVisitor,即 AST(抽象语法树)的访问者类,Android Lint 把扫描到的代码抽象成 AST,方便开发者以节点 - 属性的形式获取信息,visitor 则可以方便地获取当前节点的相关节点。
  • node 这是一个 MethodInvocation 实例,MethodInvocation 是 Android Lint 里的 AST 子类,在上面的例子中,node 表示的是被扫描到的方法,所以我们可以通过节点 - 属性的形式获取被扫描的方法的参数等各种信息。
    在例子中我们获取方法的参数,通过遍历参数拿到 Drawable 参数,分解出 Drawable 的文件名,然后通过 context 获取主项目的资源路径,配合 Drawable 的文件名拼接文件的实际路径,确定文件存在后检查文件内容开头是否包含 “vector” 这个字符串,如果是则表示开发者在普通的 getDrawable 方法中传入了 Vector Drawable,最后调用 context 的 report 方法进行报告。

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

3.3 Issue

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

  • id:标记 issue 的唯一值,语义上要能简短描述问题,使用 Java 注解和 XML 属性屏蔽 Lint 时,就需要使用这个 id。
  • summary:概况地描述问题,不需要给出解决办法。
  • explanation:详细地描述问题以及给出解决办法。
  • category:问题类别,在系统给出的分类中选择,后面会详述。
  • priority:1-10 的数字,表示优先级,10 为最严重。
  • severity:严重级别,在 Fatal,Error,Warning,Informational,Ignore 中选择一个。
  • Implementation:Detector 与 Issue 的映射关系,需要传入当前的 Detector 类,以及扫描代码的范围,例如 Java 文件、Resource 文件或目录等范围。
    如下图,产生问题时,问题的提醒信息就就会显示相关的 Issue 的 id 等信息。


    123.png

3.4 Category

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

  • Lint
  • Correctness (子分类 Messages)
  • Security
  • Performance
  • Usability (子分类 Typography, Icons)
  • A11Y (Accessibility)
  • I18N (Internationalization,子分类 Rtl)
    如果系统分类不能满足需求,也可以创建自定义的分类:
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 实践 —— 简介及常见问题分析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,471评论 25 707
  • 重庆一隅,人才济济。 水墨丹青,坚定自信。
    玉鲁人阅读 219评论 0 0
  • 这个时代,每个人都有些自顾不暇;没有时间去想多余的事情,到底是没时间还是思维的懒散呢! 不要让自己变成自己所讨厌的...
    双鱼L阅读 172评论 0 0