移动开发:美团外卖Android Lint代码检查实践
概述
Lint是Google提供的Android静态代码检查工具,可以扫描并发现代码中潜在的问题,提醒开发人员及早修正,提高代码质量。除了Android原生提供的几百个Lint规则,还可以开发自定义Lint规则以满足实际需要。
为什么要使用Lint
在美团外卖Android App的迭代过程中,线上问题频繁发生。开发时很容易写出一些问题代码,例如Serializable的使用:实现了Serializable接口的类,如果其成员变量引用的对象没有实现Serializable接口,序列化时就会Crash。我们对一些常见问题的原因和解决方法做分析总结,并在开发人员组内或跟测试人员一起分享交流,帮助相关人员主动避免这些问题。
为了进一步减少问题发生,我们逐步完善了一些规范,包括制定代码规范,加强代码Review,完善测试流程等。但这些措施仍然存在各种不足,包括代码规范难以实施,沟通成本高,特别是开发人员变动频繁导致反复沟通等,因此其效果有限,相似问题仍然不时发生。另一方面,越来越多的总结、规范文档,对于组内新人也产生了不小的学习压力。
有没有办法从技术角度减少或减轻上述问题呢?
我们调研发现,静态代码检查是一个很好的思路。静态代码检查框架有很多种,例如FindBugs、PMD、Coverity,主要用于检查Java源文件或class文件;再例如Checkstyle,主要关注代码风格;但我们最终选择从Lint框架入手,因为它有诸多优势:
- 功能强大,Lint支持Java源文件、class文件、资源文件、Gradle等文件的检查。
- 扩展性强,支持开发自定义Lint规则。
- 配套工具完善,Android Studio、Android Gradle插件原生支持Lint工具。
- Lint专为Android设计,原生提供了几百个实用的Android相关检查规则。
- 有Google官方的支持,会和Android开发工具一起升级完善。
在对Lint进行了充分的技术调研后,我们根据实际遇到的问题,又做了一些更深入的思考,包括应该用Lint解决哪些问题,怎么样更好的推广实施等,逐步形成了一套较为全面有效的方案。
Lint API简介
为了方便后文的理解,我们先简单看一下Lint提供的主要API。
主要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列表。
举例来说,原生的ShowToast就是一个Issue,该规则检查调用Toast.makeText()
方法后是否漏掉了Toast.show()
的调用。其Detector为ToastDetector,要检查的Scope为JAVA_FILE_SCOPE
,ToastDetector实现了JavaPsiScanner,示意代码如下。
public class ToastDetector extends Detector implements JavaPsiScanner {
public static final Issue ISSUE = Issue.create(
"ShowToast",
"Toast created but not shown",
"...",
Category.CORRECTNESS,
6,
Severity.WARNING,
new Implementation(
ToastDetector.class,
Scope.JAVA_FILE_SCOPE));
IssueRegistry的示意代码如下。
public class MyIssueRegistry extends IssueRegistry {
@Override
public List<Issue> getIssues() {
return Arrays.asList(
ToastDetector.ISSUE,
LogDetector.ISSUE,
// ...
);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
Scanner
Lint开发过程中最主要的工作就是实现Scanner。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。
PSI是JetBrains在IDEA中解析Java源码生成语法树后提供的API。相比之前的Lombok AST,PSI可以支持Java 1.8、类型解析等。使用JavaPsiScanner实现的自定义Lint规则,可以被加载到Android Studio 2.2+版本中,在编写Android代码时实时执行。
在Android Studio 3.0和lint-api 25.4.0版本中,Lint工具将PSI替换为UAST,同时推荐使用新的UastScanner。
UAST是JetBrains在IDEA新版本中用于替换PSI的API。UAST更加语言无关,除了支持Java,还可以支持Kotlin。
本文目前仍然基于PsiJavaScanner做介绍。根据UastScanner源码中的注释,可以很容易的从PsiJavaScanner迁移到UastScanner。
Lint规则
我们需要用Lint检查代码中的哪些问题呢?
开发过程中,我们比较关注App的Crash、Bug率等指标。通过长期的整理总结发现,有不少发生频率很高的代码问题,其原理和解决方案都很明确,但是在写代码时却很容易遗漏且难以发现;而Lint恰好很容易检查出这些问题。
Crash预防
Crash率是App最重要的指标之一,避免Crash也一直是开发过程中比较头疼的一个问题,Lint可以很好的检查出一些潜在的Crash。例如:
原生的NewApi,用于检查代码中是否调用了Android高版本才提供的API。在低版本设备中调用高版本API会导致Crash。
自定义的SerializableCheck。实现了Serializable接口的类,如果其成员变量引用的对象没有实现Serializable接口,序列化时就会Crash。我们制定了一条代码规范,要求实现了Serializable接口的类,其成员变量(包括从父类继承的)所声明的类型都要实现Serializable接口。
自定义的ParseColorCheck。调用
Color.parseColor()
方法解析后台下发的颜色时,颜色字符串格式不正确会导致IllegalArgumentException,我们要求调用这个方法时必须处理该异常。
Bug预防
有些Bug可以通过Lint检查来预防。例如:
SpUsage:要求所有SharedPrefrence读写操作使用基础工具类,工具类中会做各种异常处理;同时定义SPConstants常量类,所有SP的Key都要在这个类定义,避免在代码中分散定义的Key之间冲突。
ImageViewUsage:检查ImageView有没有设置ScaleType,加载时有没有设置Placeholder。
TodoCheck:检查代码中是否还有TODO没完成。例如开发时可能会在代码中写一些假数据,但最终上线时要确保删除这些代码。这种检查项比较特殊,通常在开发完成后提测阶段才检查。
性能/安全问题
一些性能、安全相关问题可以使用Lint分析。例如:
ThreadConstruction:禁止直接使用
new Thread()
创建线程(线程池除外),而需要使用统一的工具类在公用线程池执行后台操作。LogUsage:禁止直接使用
android.util.Log
,必须使用统一工具类。工具类中可以控制Release包不输出Log,提高性能,也避免发生安全问题。
代码规范
除了代码风格方面的约束,代码规范更多的是用于减少或防止发生Bug、Crash、性能、安全等问题。很多问题在技术上难以直接检查,我们通过封装统一的基础库、制定代码规范的方式间接解决,而Lint检查则用于减少组内沟通成本、新人学习成本,并确保代码规范的落实。例如:
前面提到的SpUsage、ThreadConstruction、LogUsage等。
ResourceNaming:资源文件命名规范,防止不同模块之间的资源文件名冲突。
代码检查的实施
当检查出代码问题时,如何提醒开发者及时修正呢?
早期我们将静态代码检查配置在Jenkins上,打包发布AAR/APK时,检查代码中的问题并生成报告。后来发现虽然静态代码检查能找出来不少问题,但是很少有人主动去看报告,特别是报告中还有过多无关紧要的、优先级很低的问题(例如过于严格的代码风格约束)。
因此,一方面要确定检查哪些问题,另一方面,何时、通过什么样的技术手段来执行代码检查也很重要。我们结合技术实现,对此做了更多思考,确定了静态代码检查实施过程中的主要目标:
重点关注高优先级问题,屏蔽低优先级问题。正如前面所说,如果代码检查报告中夹杂了大量无关紧要的问题,反而影响了关键问题的发现。
高优问题的解决,要有一定的强制性。当检查发现高优先级的代码问题时,给开发者明确直接的报错,并通过技术手段约束,强制要求开发者修复。
某些问题尽可能做到在第一时间发现,从而减少风险或损失。有些问题发现的越早越好,例如业务功能开发中使用了Android高版本API,通过Lint原生的NewApi可以检查出来。如果在开发期间发现,当时就可以考虑其他技术方案,实现困难时可以及时和产品、设计人员沟通;而如果到提代码、提测,甚至发版、上线时才发现,可能为时已晚。
优先级定义
每个Lint规则都可以配置Sevirity(优先级),包括Fatal、Error、Warning、Information等,我们主要使用Error和Warning,如下。
- Error级别:明确需要解决的问题,包括Crash、明确的Bug、严重性能问题、不符合代码规范等,必须修复。
- Warning级别:包括代码编写建议、可能存在的Bug、一些性能优化等,适当放松要求。
执行时机
Lint检查可以在多个阶段执行,包括在本地手动检查、编码实时检查、编译时检查、commit检查,以及在CI系统中提Pull Request时检查、打包发版时检查等,下面分别介绍。
手动执行
在Android Studio中,自定义Lint可以通过Inspections功能(Analyze - Inspect Code
)手动运行。
在Gradle命令行环境下,可直接用./gradlew lint
执行Lint检查。
手动执行简单易用,但缺乏强制性,容易被开发者遗漏。
编码阶段实时检查
编码时检查即在Android Studio中写代码时在代码窗口实时报错。其好处很明显,开发者可以第一时间发现代码问题。但受限于Android Studio对自定义Lint的支持不完善,开发人员IDE的配置不同,需要开发者主动关注报错并修复,这种方式不能完全保证效果。
IDEA提供了Inspections功能和相应的API来实现代码检查,Android原生Lint就是通过Inspections集成到了Android Studio中。对于自定义Lint规则,官方似乎没有给出明确说明,但实际研究发现,在Android Studio 2.2+版本和基于JavaPsiScanner开发的条件下(或Android Studio www.yibaoyule1.com 3.0+和JavaPsiScanner/UastScanner),IDE会尝试加载并实时执行自定义Lint规则。
技术细节:
在Android Studio 2.x版本中,菜单
Preferences - Editor - Inspections - Android - www.cnzhaotai.com 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版本中,打开Android工程源码后,IDE会加载工程中的自定义Lint规则,在设置菜单的Inspections列表里可以查看,和原生Lint效果相同(Android Studio会在打开源文件时触发对该文件的代码检查)。
分析自定义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
在Android Studio中的实际效果如图:
本地编译时自动检查
配置Gradle脚本可实现编译Android工程时执行Lint检查。好处是既可以尽早发现问题,又可以有强制性;缺点是对编译速度有一定的影响。
编译Android工程执行的是assemble任务,让assemble依赖lint任务,即可在编译时执行Lint检查;同时配置LintOptions,发现Error级别问题时中断编译。
在Android Application工程(APK)中配置如下,Android Library工程(AAR)把applicationVariants
换成libraryVariants
即可。
android.applicationVariants.all { variant ->
variant.outputs.each { output www.thd178.com/ ->
def lintTask = tasks["lint${variant.name.capitalize()}"]
output.assemble.dependsOn lintTask
}
}
- 1
- 2
- 3
- 4
- 5
- 6
LintOptions的配置:
android.lintOptions {
abortOnError true
}
- 1
- 2
- 3
本地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时触发Jenkins执行Lint检查的Job。
在本地编译和CI系统中做代码检查,都可以通过执行Gradle的Lint任务实现。可以在CI环境下给Gradle传递一个StartParameter,Gradle脚本中如果读取到这个参数,则配置LintOptions检查所有Lint问题;否则在本地编译环境下只检查部分高优先级Lint问题,减少对本地编译速度的影响。
Lint生成报告的效果如图所示:
打包发布时检查
即使每次提代码时用CI系统执行Lint检查,仍然不能保证所有人的代码合并后一定没有问题;另外对于一些特殊的Lint规则,例如前面提到的TodoCheck,还希望在更晚的时候检查。
于是在CI系统打包发布APK/AAR用于测试或发版时,还需要对所有代码再做一次Lint检查。
最终确定的检查时机
综合考虑多种检查方式的优缺点以及我们的目标,最终确定结合以下几种方式做代码检查:
- 编码阶段IDE实时检查,第一时间发现问题。
- 本地编译时,及时检查高优先级问题,检查通过才能编译。
- 提代码时,CI检查所有问题,检查通过才能合代码。
- 打包阶段,完整检查工程,确保万无一失。
移动开发:美团外卖Android Lint代码检查实践的更多相关文章
- Android ------ 美团的Lint代码检查实践
概述 Lint是Google提供的Android静态代码检查工具,可以扫描并发现代码中潜在的问题,提醒开发人员及早修正,提高代码质量.除了Android原生提供的几百个Lint规则,还可以开发自定义L ...
- 美团外卖Android Crash治理之路
Crash率是衡量一个App好坏的重要指标之一,如果你忽略了它的存在,它就会愈演愈烈,最后造成大量用户的流失,进而给公司带来无法估量的损失.本文讲述美团外卖Android客户端团队在将App的Cras ...
- 美团外卖Android平台化的复用实践
美团外卖平台化复用主要是指多端代码复用,正如美团外卖iOS多端复用的推动.支撑与思考文章所述,多端包含有两层意思:其一是相同业务的多入口,指美团外卖业务需要在美团外卖App(下文简称外卖App)和美团 ...
- WMRouter:美团外卖Android开源路由框架
WMRouter是一款Android路由框架,基于组件化的设计思路,功能灵活,使用也比较简单. WMRouter最初用于解决美团外卖C端App在业务演进过程中的实际问题,之后逐步推广到了美团其他App ...
- git 操作 :从远程仓库gitLab上拉取指定分支到本地仓库;git如何利用分支进行多人开发 ;多人合作代码提交实践
例如:将gitLab 上的dev分支拉取到本地 git checkout -b dev origin/dev 在本地创建分支dev并切换到该分支 git pull origin dev 就可以把git ...
- Android添加代码检查权限
1,首先创建一个项目,然后创建一个类,hello.java public class hello { public static final String PERMISSION_SAY_HELLO = ...
- 【开发技术】java中代码检查checkStyle结果分析
编写Javadoc代码在Java代码的类.函数.数据成员前中输入/**回车,Eclipse能够自动生成相应的Javadoc代码.可以在后面添加相关的文字说明. Type is missing a ja ...
- Android Studio使用Lint进行代码检查
Android Studio目前已经更新到1.4版本,它作为Google官方推荐的IDE,功能非常强大,其中提供了一套静态代码分析工具,它可以帮助我们检查项目中存在的问题,让我们更有规范性的开发App ...
- Android Lint的使用
一.概述 Android Lint是在ADT 16(和 Tools 16)引入的一个新工具,可以扫描Android 项目源码中潜在的bug .例如: 缺少翻译(和未使用的翻译)布局性能问题(老的lay ...
随机推荐
- Java : 多态表现:静态绑定与动态绑定(向上转型的运行机制)
本来想自己写写的,但是看到有人分析的可以说是很清晰了,故转过来. 原文地址:http://www.cnblogs.com/ygj0930/p/6554103.html 一:绑定 把一个方法与其所在的类 ...
- Java源码解析——集合框架(二)——ArrayBlockingQueue
ArrayBlockingQueue源码解析 ArrayBlockingQueue是一个阻塞式的队列,继承自AbstractBlockingQueue,间接的实现了Queue接口和Collection ...
- php 电商系统SKU库存设计
sku 全称为:Stock Keeping Unit,是库存进出计量的基本单元. 我们一般会在电商网站基本都会看到 比如淘宝,JD 淘宝和JD的 方式可能不一样,因为我不清楚他们具体是如何设计的, J ...
- 数据分析处理库Pandas——索引进阶
Series结构 筛选数据 指定值 备注:查找出指定数值的索引和数值. 逻辑运算 备注:查找出值大于2的数据. 复合索引 DataFrame结构 显示指定列 筛选显示 备注:值小于0的显示原值,否则显 ...
- 文件 I/O字符流
import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOExceptio ...
- vue2.0 Axios 的简单用法
安装 使用 npm: $ npm install axios 使用 bower: $ bower install axios 使用 cdn: <script src="https:// ...
- RelativeSource设定绑定方向
<Window x:Class="Yingbao.Chapter2.RelativeEx.AppWin" xmlns="http://schemas.microso ...
- 永无BUG 注释
/** * _ooOoo_ * o8888888o * 88" . "88 ...
- Hibernate-ORM:03.Hibernate主键生成策略
------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- 此篇博客简单记录五种常用的主键生成策咯: 不同的主键生成策略,生成的sql语句,以及hibernate的操作都 ...
- 多个Target的使用
背景介绍 开发过程中,我们会在内网搭建一个测试服务器,开发.测试都是在内网进行的.这样产生脏数据不会影响外网的服务器.外网服务器只有最后发布时才会进行一些必要的测试. 还有就是要对同一份代码生成不同的 ...