Lint规则
Lint规则有Fatal、Error、Warning、Information等级别,自定义Lint主要使用Error和Warnning。考虑到实际实施,侧重于检查明确需要解决的Error级别问题,通过技术手段强制要求所有开发人员遵守;对于优先级较低、技术上难以明确判断是否需要解决的Warnning级别问题,则适当放松要求。
- Error级别:包括Crash、严重性能问题、不符合代码规范等,必须修复。
- Warnning级别:用于代码编写建议、潜在BUG提醒、无关紧要的优化等,适当放松要求。
Crash预防
Lint检查最重要的应用是Crash预防。一是因为Crash率是App最重要的指标之一,二是因为Lint很适合防治一些Crash问题。
例如:
-
原生的NewApi,用于检查代码中是否调用了Android高版本才提供的API。
-
自定义的SerializableCheck。实现了Serializable接口的类,成员变量没有实现Serializable,会导致序列化时Crash。我们制定一条代码规范,要求实现了Serializable的类,其声明的成员变量(包括从父类继承的)都要实现Serializable。
可以设置一些白名单类型,例如数据Model中允许成员变量申明成Collection和Map(或其子类)。主要是考虑到用的场景比较多,实际上赋值一般也都是实现了Serializable的ArrayList和HashMap,所以做折中处理。
Bug预防
有些Bug可以通过Lint检查来预防。不过Bug往往涉及业务逻辑,其定义比较模糊,检查比较困难,更多时候只能起到提醒的作用。
例如:
-
SpUsage:要求所有SharedPrefrence读写操作都要使用共通工具类,工具类中进行各种异常处理;同时定义SPConstants常量类,所有SP的Key都要在这个类定义,避免在代码中分散定义的Key之间冲突。
-
ImageViewUsage:检查ImageView有没有设置ScaleType,加载时有没有设置Placeholder。
-
TodoCheck:检查代码中是否还有TODO没完成。例如开发时可能会在代码中写一些假数据,但最终上线时要确保删除这些代码。这种检查项比较特殊,通常可以在开发完成后提测阶段才检查。
性能/安全问题
-
ThreadConstruction:禁止直接使用
new Thread()创建线程,而需要使用统一的工具类,在公用的线程池执行后台操作。 -
LogUsage:禁止直接使用
android.util.Log,必须使用统一工具类,控制Release包不输出Log,提高性能,也避免发生安全问题。
代码规范
除了代码风格方面的约束,代码规范更多的是用于减少或防止发生BUG、Crash、性能、安全问题等。很多问题在技术上难以直接检查,但可以通过封装统一的基础库、制定代码规范的方式间接解决,而Lint检查则用于减少组内沟通成本、新人学习成本,并确保代码规范的落实。
例如:
-
前面提到的SpUsage、ThreadConstruction、LogUsage等。
-
ResourceNaming:资源文件命名规范,防止不同模块之间的资源文件名冲突。
Lint检查时机
Lint检查可以在多个阶段执行。有两个目标:
- 高优先级问题希望通过技术手段进行约束,强制要求所有开发人员遵守;
- 有些问题希望做到在第一时间发现,从而减少其带来的风险或损失。
有些Lint问题发现的越早越好。例如使用了Android高版本API,在低版本设备上可能发生Crash,通过原生的NewApi可以检查出来。如果在开发期间发现,当时就可以考虑其他技术方案,实现困难时可以及时和产品、设计人员沟通;而如果到提代码、提测,甚至发版、上线时才发现,可能为时已晚。
因此,除了确定要检查哪些问题,何时、通过什么样的技术手段来执行代码检查也很重要。
手动执行
手动执行简单易用,但缺乏强制性,容易被开发者遗漏。
在Android Studio中,自定义Lint可以通过Inspections功能(Analyze - Inspect Code)手动运行。
在Gradle命令行环境下,可直接用./gradlew lint执行Lint检查。
编码阶段实时检查
编码时检查即在Android Studio中写代码时实时报错。这种方式的好处很明显,开发者可以第一时间发现代码问题。其缺点在于,受限于Android Studio对自定义Lint的支持不完善,不同开发人员IDE的配置,以及需要开发者主动查看报错并修复,不能确保高优先级问题都会被解决。
Android原生的Lint规则已经通过Inspections功能被集成到Android Studio中,因此可以直接实时检查并报错,开发过程中经常看到的一些Android API相关的Warning和Error就是这样实现的。
对于自定义的Lint规则,官方似乎没有给出明确的说明,但实际研究发现,在Android Studio 2.2+版本和基于JavaPsiScanner开发的条件下,IDE也会尝试加载并实时执行自定义Lint规则。
技术实现相关:
-
在Android Studio 2.x版本中,菜单
Preferences - Editor - Inspections - Android - Lint - Correctness - Error from Custom Lint Check(avaliable for Analyze|Inspect Code)中指出,自定义Lint只支持命令行或手动运行,不支持实时检查。Error from Custom Rule When custom (third-party) lint rules are integrated in the IDE, they are not available as native IDE inspections, so the explanation text (which must be statically registered by a plugin) is not available. As a workaround, run the lint target in Gradle instead; the HTML report will include full explanations.
-
在Android Studio 3.x版本中,可以看到自定义Lint会被直接加载到设置中的Lint列表里,和原生Lint效果相同。
-
实际分析自定义Lint的
IssueRegistry.getIssues()方法调用堆栈,可以看到Android Studio环境下,是由org.jetbrains.android.inspections.lint.AndroidLintExternalAnnotator调用LintDriver加载执行自定义Lint规则。参考代码: https://github.com/JetBrains/android/tree/master/android/src/org/jetbrains/android/inspections/lint
本地编译时自动检查
通过配置Gradle脚本,可以在本地编译Android工程时进行Lint检查。好处是既可以尽早发现问题,又可以强制检查高优问题;缺点是对本地编译速度有一定的影响。
在Android Studio中编译项目执行的Gradle Task是assemble,可通过下面的脚本,让assemble任务依赖lint任务,每次本地编译时检查Error级别Lint问题,发现未解决的问题则直接编译失败。
对于Android Application工程(APK):
1 | android.applicationVariants.all { variant -> |
对于Android Library工程(AAR):
1 | android.libraryVariants.all { variant -> |
本地commit时检查
利用git pre-commit hook,可以在本地commit代码前执行Lint检查,检查不通过则无法提交代码。这种方式的优势在于不影响开发时的编译速度,但发现问题相对有些滞后。
技术实现方面,可以编写Gradle脚本,在每次同步工程时自动将hook脚本从工程拷贝到.git/hooks/文件夹下。
提代码时CI检查
作为代码提交流程规范的一部分,发Pull Request提代码时用CI系统检查Lint问题是一个常见、可行、有效的思路。可配置CI检查通过后,代码才能被合并。
CI系统常用Jenkins。如果使用Stash做代码管理,可以在Stash上配置Pull Request Notifier for Stash插件,或在Jenkins上配置Stash Pull Request Builder插件,实现发Pull Request时执行Job,从而执行Gradle任务检查Lint问题。
在本地编译和CI系统中做代码检查,都可以通过执行Gradle的Lint任务实现。在CI环境下可以给Gradle传递一个StartParameter,Gradle脚本中如果读取到这个参数,则配置LintOptions检查所有Lint问题;否则在本地编译环境下只检查部分高优先级Lint问题,减少对本地编译速度的影响。
打包发布时检查
即使每次提代码时用CI系统执行Lint检查通过,仍然不能保证所有人的代码合并后一定没有问题;另外对于一些特殊的问题,例如前面提到的TodoCheck,还希望在更晚的时候检查。
于是在CI系统打包发布APK/AAR用于测试或发版时,还可以对所有代码再做一次Lint检查,确保万无一失。
方案确定
综上,可选择结合多种方式做代码检查:
- 编码阶段IDE实时检查,第一时间发现问题
- 本地编译时,及时检查高优先级问题
- 提代码时,CI检查所有问题
- 打包阶段,完整检查工程
自定义Lint代码管理
自定义Lint的源码可以直接在实际项目中新建模块实现,不需单独打包、管理方便;更常见的方式是作为一个独立的工程来开发,最终生成AAR发布到Maven仓库,AAR中包含lint.jar。被检查的Android工程依赖这个AAR,即可使用自定义Lint规则。
Lint AAR嵌套依赖问题
有时可能会有一套自定义Lint规则专用于某个Library,如果直接让Library依赖LintAAR,会导致依赖这个Library的其他工程也会引入这套Lint规则。
解决方法:依赖这个Library时,用exclude去掉其对LintAAR的依赖。
1 | dependencies { |
配置文件支持
自定义Lint通常虽然是一个独立工程,但往往又和被检查Android工程中的代码规范、封装的基础库等关系密切。
有时希望在不修改Lint工程的情况下,在被检查工程中对Lint规则进行一些配置(例如资源文件命名的检查,配置文件名要满足的正则表达式);另一方面,也希望同一套Lint代码能用于多个Android工程,甚至在多个团队之间共享和复用,通过配置文件定义其中不同的部分(例如LogUsage检查,同样是禁用Android自带的Log,不同工程中封装的Log工具类不同,于是报错时的提示信息也不一样)。
配置文件可以是任意格式,放在被检查Android工程目录下的特定路径,例如custom-lint-config.json。
以LogUsage为例,Android工程A的配置文件可能是:
1 | { |
而Android工程B的配置文件是:
1 | { |
技术实现上,可在Lint代码的Scanner回调方法中,从传入的Context对象获取被检查工程目录,从而读取配置文件。关键代码如下:
1 | import com.android.tools.lint.detector.api.Context; |
1 | public class LogUsageDetector extends Detector implements Detector.JavaPsiScanner { |
模板Lint规则
对一些相似的Lint规则,可以实现一个模板,直接在被检查工程中直接通过配置文件配置,而不需要在Lint工程中开发。
如下为一个配置文件示例:
1 | { |
示例配置中定义了两种类型的模板规则:
- DeprecatedApi:禁止直接调用指定API
- HandleException:调用指定API时,需要加try-catch处理指定类型的异常
问题API的匹配,包括方法调用(method)、成员变量引用(field)、构造函数(construction)、继承(super-class)等类型;匹配字符串支持glob语法或正则表达式(和lint.xml中ignore的配置一致)。
实现方面,主要是遍历Java语法树中特定类型的节点并转换成完整字符串(例如方法调用android.content.Intent.getIntExtra),然后检查是否有模板规则与其匹配;匹配成功后,DeprecatedApi规则直接输出message报错;HandleException规则会检查匹配到的节点是否处理了特定Exception(或Exception的父类),没有处理则报错。
按git版本检查新增文件
有时新增一些低优先级Lint规则,但已经有很多历史代码用了,且没有导致明显问题。例如新增代码规范,要求使用统一的线程工具类而不允许直接用Handler以避免内存泄露,这时如果接入禁用Handler的Lint检查,就需要修改所有历史代码,成本较高而且有一定风险。
一个折中的方案是,只检查指定git commit之后新增的文件。在配置文件中添加两个配置项,git-base指定commit ID;git-based-issues指定哪些Issue要按git版本检查。也可以更细化一些,给每个Issue配置不同的git-base。
获取Git新增文件
获取新增文件有两种方法如下。推荐使用方法二,因为每次检查通常只读一次文件列表(提高性能),如果用方法一读完文件之后新增的文件检查不出来。
- 用diff查看当前和指定commit之间的新增文件
- 获取指定commit已有文件,剩下的就是新增文件了。
获取git工程根目录的路径:可在工程目录下执行如下指令(被检查的工程目录例如app包含在git工程根目录下)。
1 | $ git rev-parse --show-toplevel |
获取指定commit后新增文件:可执行如下命令。
1 | # 新增且从没被git追踪的文件(untracked files),diff命令默认不会显示, |
获取指定commit已有文件列表:可在工程目录下执行如下命令。
1 | # `--full-tree`指定输出git工程所有文件而不是当前目录下的文件, |
获取到文件列表的字符串后,将其按行分割并保存到HashSet中。如果只考虑Java和XML文件,也可以用grep过滤掉其他文件。
判断是否要检查
按git版本检查主要是处理Java文件和XML文件。在JavaPsiScanner.visitMethod()/XmlScanner.visitElement()等方法中调用LintConfig.shouldCheckFile(xxx)方法,通过Context.getLocation(node).getFile()获取PsiElement/Node所在文件,判断是否在指定commit的已有文件列表中,从而决定要不要检查。
关键代码如下。
1 | import com.android.tools.lint.detector.api.Context; |
参考资料与扩展阅读
- 使用 Lint 改进您的代码
https://developer.android.com/studio/write/lint.html
- AndAndroid Plugin DSL Reference:LintOptions
- How Do We Configure Android Studio to Run Its Lint on Every Build?
- Writing custom lint rules and integrating them with Android Studio inspections
- 你可能不知道的Android Studio/IDEA使用技巧
http://www.xiaoming.io/android-studio-skill
- Android自定义Lint实践
http://tech.meituan.com/android_custom_lint.html
- Android Gradle配置快速入门
http://www.xiaoming.io/android-gradle-basics
- Gradle开发快速入门——DSL语法原理与常用API介绍
http://www.xiaoming.io/gradle-develop-basics
- Viewing PSI Structure
https://www.jetbrains.com/help/idea/viewing-psi-structure.html
- Git - Documentation
https://git-scm.com/documentation
如果觉得文章有帮助,欢迎分享转发,也欢迎关注我的公众号“搬砖的小明”,及时获取更新