文章目录
- 关于静态代码扫描工具
- Lint的简单使用
- 一、Lint 与 IDE 的结合使用
- 二、Lint 与 gradle 命令的结合使用
- 具体位置如下图:
- 生成的HTML在浏览器打开如图:
- 自定义 Lint
- 为什么需要自定义 Lint?
- Lint 需要自定义检查的问题
- 参考美团的方案针对 Lint 实施的思考
- 开发自定义 Lint 前的准备工作
- Android SDK
- 自定义 Lint 规则的流程
- **下面着重于如何编写自定义规则,也就是我们 lintjar 这个 module 的开发**
- 一 、创建 Java 工程,配置 Gradle
- 二 、选定一个规则用 Lint 实现
- Issue
- 划重点 --> Scanner
- JavaPsiScanner
- XmlScaner
- 实践
- 实践一
- 实践二
- 后续会附上demo 以及其他规则的实现过程
关于静态代码扫描工具
在我们项目迭代过程中,线上问题频繁发生。开发时很容易写出一些问题代码,例如 Serializable 的使用:实现了 Serializable 接口的类,如果其成员变量引用的对象没有实现 Serializable 接口,序列化时就会 Crash。再例如,如果 XML 资源文件包含未使用的命名空间,则不仅占用空间,还会导致不必要的处理。其他结构问题,例如使用目标 API 版本不支持的已弃用的元素或 API 调用等,可能导致代码无法正常运行。所以为了进一步减少问题发生,我们逐步完善了一些规范,包括制定代码规范,加强代码 Review,完善测试流程等。但这些措施仍然存在各种不足,包括代码规范难以实施,沟通成本高,因此其效果有限,相似问题仍然不时发生。
有没有办法从技术角度减少或减轻上述问题呢?
我们调研发现,静态代码检查是一个很好的思路。静态代码检查框架有很多种,例如 FindBugs、PMD、Coverity,主要用于检查 Java 源文件或 class 文件;再例如 Checkstyle,主要关注代码风格;但我们最终选择从 Lint 框架入手,因为它有诸多优势:
-
Lint 工具可检查 Android 项目源文件是否包含潜在错误,以及在正确性、安全性、性能、易用性、便利性和国际化方面是否需要优化改进。并且支持 class 文件、资源文件、Gradle 等文件的检查。
-
扩展性强,支持开发自定义 Lint 规则
-
配套工具完善,Android Studio、Android Gradle 插件原生支持Lint工具。
-
Lint 专为 Android 设计,原生提供了几百个实用的 Android 相关检查规则。
Lint的简单使用
一、Lint 与 IDE 的结合使用
点击 Analyze 的 Inspect Code 选项,即可开启 lint 检查,在Inspection窗口中可以看到lint检查的结果。并且 android 自带的lint规则的更改可以在 Setting 的 Edit 选项下选择 Inspections(File > Settings > Project Settings),对已有的 lint 规则进行自定义选择。
二、Lint 与 gradle 命令的结合使用
AS 的控制台,进入要使用 Lint 检查规则的模块目录,使用 gradle lint 命令。输出的结果会生成一个 xml 以及 HTML(可以在浏览器打开,页面非常简单直观)。路径信息也可以在控制台看到,比如我的就是:
Wrote HTML report to file:///D:/cccx_3.0/app/build/reports/lint-results.html
Wrote XML report to file:///D:/cccx_3.0/app/build/reports/lint-results.xml
具体位置如下图:

生成的HTML在浏览器打开如图:

自定义 Lint
为什么需要自定义 Lint?
由于每个业务线自身的需求,Lint 默认的检查项目可能不能满足我们的需求。 比如司机端一个自定义控件需要抽成一个库给其他项目使用,但是我们希望使用者必须在 XML 中定义一个属性,否则组件无法正常运行,我们希望Lint能够对此进行检查,并在忘记添加此属性时给出明确的错误提示。
再比如,我们的基础组件有一个日志库,能够方便的在 release 版本中关闭日志输出,还能够把日志输出到指定的文件中方便事后分析,这时如果来了一个新同学,他可能还是习惯性的用 android.util.Log 来打印日志,我们希望能够检测到本项目中所有使用了 android.util.Log 的代码,并发出警告。 要满足这些自定义需求,我们就需要通过 Android Lint 的扩展机制自己定制 Lint 规则。
Lint 需要自定义检查的问题
一、Crash 预防
Crash 率是我们司机端和乘客端的最重要的指标之一,我们期望使用 Lint 检查出一些潜在的 Crash,例如:
- 原生的
NewApi,用于检查代码中是否调用了 Android 高版本才提供的 API。在低版本设备中调用高版本 API 会导致 Crash。 - 实现了
Serializable接口的类,如果其成员变量引用的对象没有实现Serializable接口,序列化时就会Crash。 - 调用
Color.parseColor()方法解析后台下发的颜色时,颜色字符串格式不正确会导致IllegalArgumentException。
二、Bug 预防
由于目前 Bug 数已经作为部门衡量的指标,我们也期望使用 Lint 来检车和预防,例如:
- 所有的页面跳转使用统一的路由
UXRouter,并且PATH维护在统一的常量类里方便查阅和修改。 - 使用 Fastjson 解析 JSON 数据时,用基础类型来接收的不要用get包装类型的方法。比如:
getInteger()和getIntValue()。
三、性能/安全问题
对于司机端和乘客端来讲,性能和安全非常重要。我们期望使用Lint来检测一些可以规避的影响性能和安全的问题,例如:
- 使用 PendingIntent 时,使用了空 Intent 会导致恶意用户劫持修改 Intent 的内容。所以使用 PendingIntent 时,禁止使用空 intent,同时禁止使用隐式 Intent。
- 在 Android API level 8 以后增加了
android:allowBackup属性值。默认情况下这个属性值为 true,故
当 allowBackup 标志值为 true 时,即可通过 adb backup 和 adb restore 来备份和恢复应用程序数据。所以就需要强制将android:allowbackup属性设置为 false,防止 adb backup 导出数据。
四、代码编写规范
对于代码的编写规范之前已经讨论过并且给出了具体的实行方案,我们希望使用 Lint 来检测以便于减少沟通成本,Code Review 的时间以及新人的学习成本。例如:
- 对 Activity 的命名和 Fragment 的命名,不要使用简写 Act、Frg 等。
- 资源文件命名需要按照规范加入模块作为前缀,防止不同模块之间的资源文件名冲突。
参考美团的方案针对 Lint 实施的思考
- 明确优先级,代码检查报告中重点体现高优先级问题,屏蔽无关紧要的问题
- 高优问题,强制要求开发者修复,否则代码不予提交。
- 执行时机可选。以下列出针对 Lint 执行时机的一些参考:
- 编码阶段IDE实时检查,第一时间发现问题
- 本地编译时,及时检查高优先级问题,检查通过才能编译
- 提代码时,CI 检查所有问题,检查通过才能合代码
- 打包阶段,完整检查工程,确保万无一失
开发自定义 Lint 前的准备工作
Android SDK
Android SDK 中涉及 Lint 的主要有下面几个包,均包含在 Android Gradle 插件 com.android.tools.build:gradle 中。
com.android.tools.lint:lint-api:这个包提供了 Lint 的 API,包括 Context、Project、Detector、Issue、IssueRegistry 等,后面会做介绍。com.android.tools.lint:lint-checks:这个包含了 Lint 支持的200多种规则。com.android.tools.lint:lint:这个包用于运行 Lint 的检查:com.android.tools.lint.XxxReporter:检查结果报告,包括纯文本、XML、HTML 格式等com.android.tools.lint.LintCliClient:用于在命令行中执行 Lintcom.android.tools.lint.Main:这个类是命令行版本 Lint 的 Java 入口(Command line driver),主要是解析参数、输出结果
com.android.tools.build:gradle-core:这个包提供 Gradle 插件核心功能,其中与 Lint 相关的主要有:com.android.build.gradle.tasks.Lint: Gradle 中 Lint 任务的实现com.android.build.gradle.internal.LintGradleClient:用于在 Gradle 中执行 Lint,集成自 LintCliClientcom.android.build.gradle.internal.LintGradleProject:继承自 lint-api 中的 Project 类。Gradle 执行 Lint 检查时使用的 Project 对象,可获取 Manifest、依赖等信息。其中又包含了AppGradleProject和LibraryProject两个内部类。
###Lint 的主要API
Lint 规则通过调用 Lint API 实现,其中最主要的几个 API 如下:
- Issue:问题的描述,其实就是表示一个 Lint 规则。
- Detector:中文是探测器。顾名思义,用于检测并报告代码中的 Issue,每个 Issue 都要指定 Detector。
- Scope:翻译过来是表示范围的意思。这是用于声明 Detector 要扫描的代码范围,例如
JAVA_FILE_SCOPE、CLASS_FILE_SCOPE、RESOURCE_FILE_SCOPE、GRADLE_SCOPE等,一个 Issue 可包含一到多个 Scope。 - Scanner:翻译过来就是扫描器的意思。用于扫描并发现代码中的 Issue,每个 Detector 可以实现一到多个 Scanner。
- IssueRegistry: Lint 规则加载的入口,提供要检查的 Issue 列表。
如果要查看 lint 工具支持的 issue 的完整列表和它们所对应的 issue ID,可以使用 lint --list 命令。
Lint 中包括多种类型的 Scanner 如下,其中最常用的是扫描 Java 源文件和 XML 文件的 Scanner:
- JavaScanner / JavaPsiScanner / UastScanner:扫描 Java 源文件
- XmlScanner:扫描 XML 文件
- ClassScanner:扫描 class 文件
- BinaryResourceScanner:扫描二进制资源文件
- ResourceFolderScanner:扫描资源文件夹
- GradleScanner:扫描 Gradle 脚本
- OtherFileScanner:扫描其他类型文件
我们需要注意的是,扫描Java源文件的Scanner先后经历了三个版本:
- 最开始使用的是 JavaScanner,Lint 通过 Lombok 库将 Java 源码解析成 AST(抽象语法树),然后由 JavaScanner 扫描。
- 在Android Studio 2.2和 lint-api 25.2.0版本中,Lint工具将 Lombok AST 替换为 PSI,同时弃用 JavaScanner,推荐使用 JavaPsiScanner。
- 在 Android Studio 3.0和 lint-api 25.4.0 版本中,Lint 工具将 PSI 替换为 UAST,同时推荐使用新的 UastScanner
UAST 是 JetBrains 在 IDEA 新版本中用于替换 PSI 的 API。UAST 跟加语言无关,除了支持 Java,还可以支持 Kotlin.扩展 —— 关于 PSI 的介绍在这里
自定义 Lint 规则的流程
自定义 Lint 和编写 gradle 插件一样,是一个纯 Java 项目,以 jar 的形式提供依赖。有了包含 Lint 规则的 jar 后,有两种使用方案:
- 方案一:把此 Jar 拷贝到 ~/.android/lint/ 目录中(文件名任意)。此时,这些 Lint 规则针对所有项目生效。
- 方案二:继续创建一个 Android library 项目,用来输出包含 Lint.jar 的 aar;然后,让目标项目依赖此 aar 即可使自定义 Lint 规则生效。
由于方案一是全局生效的策略,无法单独针对目标项目,用处不大。在工程实践中,我们主要使用方案二。
aar 是 Android Library 的一种新的二进制分发格式,它把资源也一起打包,这样一来图片和布局资源文件也能够被同时分发。aar 格式文件能够包含一个可选的 lint.jar 文件,如果一个 app 依赖了一个包含 lint.jar 的 aar 文件,那么这个 lint.jar 中的规则就会在 app 的 lint 任务中被用来做lint检查。
下面是一个自定义 Lint 项目的目录结构:

主要包含了两个部分:
- lintjar 主要是编写自定义 Lint 的规则,编译后生成 Lint.jar 文件
- lintaar 主要将 Lint.jar 打包成 aar 方便引用
下面着重于如何编写自定义规则,也就是我们 lintjar 这个 module 的开发
一 、创建 Java 工程,配置 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'compile 'com.android.tools.build:gradle-core:2.3.3'compile 'com.android.tools.lint:lint:25.3.0'
}jar {manifest {attributes("Lint-Registry": "com.customer.lint.core.MyIssueRegistry")}
}configurations {lintChecks
}dependencies {lintChecks files(jar)
}//指定编译的编码
tasks.withType(JavaCompile){options.encoding = "UTF-8"
}
这里只需要注意下 Lint-Registry 是透露给lint工具的注册类的方法,也就是说 MyIssueRegistry 是 Lint 工具的入口,并且也通过 jar 这个命令来打包。
二 、选定一个规则用 Lint 实现
先用一个简单实现为例:

如图,我们看到简单实现一个检测 Log 的规则非常简单:
- 继承 Detector,并实现相应的 Scanner 接口
- 实例化 Issue 对象
- 使用 Lint 的 API 扫描代码、定义规则,也就是我们实现的 Scanner 接口。
-------------------------------------------下面重点介绍实现过程和对应的源码 Api 分析-------------------------------
Issue
Issue 由 Detector 发现并报告,是我们自己定义的需要统一的代码规则、代码优化点。Issue对象使用静态工厂方法构造实例对象,其方法参数的意义如下:
-
id:唯一值,应该能简短描述当前问题。利用 Java 注解或者 XML 属性进行屏蔽时,使用的就是这个 id,所以最好使用英文。
-
briefDescription:简短的总结,尽量控制在10字以内,描述问题而不是修复措施。
-
explanation:完整的问题解释和修复建议。
-
category:问题类别。在 API 当中已经定义当包括:CORRECTNESS(正确性)、SECURITY(安全性)、PERFORMANCE(性能)、USABILITY(易用性)、A11Y(便利性)、I18N(国际化),还有几个隶属于这些大类别中的子类别,详情可查阅源码
com.android.tools.lint.detector.api.Category。 -
priority:优先级。1-10 的数字,10 为最重要/最严重
-
Implementation:为 Issue 和 Detector 提供映射关系,Detector 就是当前 Detector。声明扫描检测的范围 Scope,Scope 用来描述 Detector 需要分析时需要考虑的文件集,包括:Resource 文件或目录、Java 文件、Class 文件。
划重点 --> Scanner
自定义 Detector 的实际核心逻辑部分就是实现什么样的 Scanner 接口以及 Scanner 中的方法实现。上文也有说明,目前常用的是扫描 Java 源文件和 XML 文件的 Scanner,下面就以 JavaPsiScanner 和 XmlScaner 为例介绍一下关键 Api ,其他的目前由于没有精力研究,暂且搁置。
JavaPsiScanner
下面是 JavaPsiScanner 这个扫描器的 6 组 12 个回调方法:
-
createPsiVisitor()构造一个代码访问器。一个 Detector 对象必须通过这个方法返回一个代码访问器对象,除非 Detector 对象的 appliesToResourceRefs() 返回了True或者 getApplicableMethodNames() 返回的不是Null。 -
getApplicablePsiTypes()返回访问器需要访问的Java元素类型。 -
getApplicableMethodNames()和visitMethod()一般配对使用,访问特定方法 -
getApplicableConstructorTypes()和visitConstructor()配对使用,访问特定构造器 -
getApplicableReferenceNames()和visitReference()配对使用,访问特定的引用 -
applicableSuperClasses()和checkClass()访问特定的超类 -
appliesToResourceRefs()和visitResourceReference()访问 Java 代码中的资源引用,例如R.layout.main
用法可以参考 Lint 源码
com.android.tools.lint.checks.LayoutInflationDetector和com.android.tools.lint.checks.StringFormatDetector
在实际编码过程中,仅知道 Api 的作用是不够的,还需要知道 Api 接口的返回参数是做什么的,这个也非常关键:
-
JavaContext 这个是在代码扫描的时候获得 Java 文件的一个上下对象。其中核心方法包括:
-
report()检查的规则命中之后上报就需要调用这个方法 -
getLocation()返回传入元素的位置,可以帮助我们定位需要报告问题的代码区域 -
getProject()返回一个 project 对象。这个是 JavaContext 继承过来的方法,当我们需要了解一些 project 的属性,比如编译版本之类的信息的时候,这个对象就会很有用 -
getEvaluator()返回一个 JavaEvaluator对象,用于帮助分析具体的java源文件元素
-
-
JavaEvaluator 这个对象可以帮助我们分析 Java 源文件,在自定义 lint 规则的时候,所有的 Java 源文件元素都被抽象成了另一种语法树结构表示,在新的 API 版本当中,这种语法树结构使用的是 interllij 的 java-psi 的 API。
-
extendsClass()判断某个对象是否继承于某个类 -
implementsInterface()判断某个对象是否实现某接口 -
inheritsFrom()以上两个的结合版 -
methodMatches()某个方法是否与某个类当中的方法匹配
-
-
Java-psi 的语法树抽象和 Java 元素的映射关系:
- PsiClass -> 类
- PsiMethod -> 方法
- PsiField -> 字段属性
- PsiVariable -> 变量(方法参数、属性、本地变量)
- PsiExpression -> 表达式
XmlScaner
举个例子,ResourceXmlDetector 就是实现了 XmlScaner的 一个Detector,扫描 Xml 文件并获取 Xml Dom 元素以执行检查。
-
createPsiVisitor():构造一个代码访问器。一个 Detector 对象必须通过这个方法返回一个代码访问器对象,除非 Detector 对象的appliesToResourceRefs()返回了 True 或者getApplicableMethodNames()返回的不是 Null。 -
getApplicableElements()返回的是指定检查的元素(可以是多个)。 -
visitElement()这个方法在 XmlScanner 扫描到了对应的元素时调用。如代码,我在这里添加了自己的逻辑,当扫描 Textview 时,会去判断是否含有textAppearance,如果没有的话就抛出自定义的 Issue。 -
再介绍几个用到的方法,
context.report()是用于报告问题并生成报告的,context.getLocation()精确的定位出现问题的位置,包含文件路径,行号,列数。
这里附上 Lint 的 API 文档,点击查看
实践
实践一
了解了这么多,其实还是得归于实践,关于自定义 Lint 规则的 demo 网上都是千篇一律,不是检测 Log 就是检测 Thead,部分还是老的实现方式(实现的 JavaScanner 接口),而我们现在是基于 JavaPsiScanner 实现的扫描器。以下是我的实践结果,检测 Color.parseColor() 避免解析的字符串格式不正确导致抛出异常。
另外。代码中包含注释,可以结合 API 说明与代码一起看。
/*** @ProjectName: AndroidLint-master* @Package: com.paincker.lint.core* @Author: yao.dang* @CreateDate: 2019/3/6 15:49* @UpdateUser: 更新者* @UpdateDate: 2019/3/6 15:49* @UpdateRemark: 更新说明*/
public class ColorParseDetector extends Detector implements Detector.JavaPsiScanner {public static final String ISSUE_ID = "ColorParse";public static final String ISSUE_DESCRIPTION = "避免 parseColor 解析出现异常";public static final String ISSUE_EXPLANATION = "当解析错误时会抛出异常,请加入try catch防护";public static final Category ISSUE_CATEGORY = Category.SECURITY;/*** 优先级,1到10的数字,10是最重要/最严重的*/private static final int ISSUE_PRIORITY = 6;private static final Severity ISSUE_SEVERITY = Severity.ERROR;/*** 特定的方法名*/static final String PARSECOLOR = "parseColor";public static final Issue ISSUE = Issue.create(ISSUE_ID,ISSUE_DESCRIPTION,ISSUE_EXPLANATION,ISSUE_CATEGORY,ISSUE_PRIORITY,ISSUE_SEVERITY,new Implementation(ColorParseDetector.class, Scope.JAVA_FILE_SCOPE));@Overridepublic List<String> getApplicableMethodNames() {return Collections.singletonList(PARSECOLOR);}@Overridepublic void visitMethod(JavaContext context, JavaElementVisitor visitor, PsiMethodCallExpression call, PsiMethod method) {//如果方法名不一致就不走判断逻辑if (method.getName().equals(PARSECOLOR)) {JavaEvaluator evaluator = context.getEvaluator();//接着要确定是哪个类的方法if (evaluator.isMemberInClass(method, "android.graphics.Color")) {/*** 在AST抽象语法树中,调用 parseColor 的节点应该是 try 的子节点,* 向上追溯,查到的对应的是 Try 那么就说明已经在调用 parseColor 前做了try-catch处理*/PsiElement psiElement = PsiTreeUtil.getParentOfType(method, Try.class);if (psiElement == null) {context.report(ISSUE,context.getLocation(call.getMethodExpression()),ISSUE_EXPLANATION);}}} }
}
实践二
在实践的过程中,有一点必不可少,那就是调试。普通的 debug 调试大家应该都没问题,但是对于 aar 的调试呢,如果不了解的话,可以看下文:
首先,打开Run -> Edit Configuration 然后点击加号,创建一个 Remote 的配置,注意要将 JVM 命令拷贝。

然后,在右边的 gradle 状态栏,找到主工程下的 assembleDebug,右键 create 一个新的 Run Configurations,将上一步拷贝的内容粘贴到 VM Options 内,注意要将 suspend 对应的值改为 y。

最后,双击上一步新生成的调试任务,这时候就会进入挂起状态等待调试,接着在状态栏切换到之前未命名的 Remote 任务,点击右侧的 debug 调试按钮,就可以进入调试流程了,当然,你需要提前打好断点。









![[C++]TscanCode代码扫描工具](https://img-blog.csdnimg.cn/a7518f554dc94843956046ac8ae376ae.png)




