您的位置:网站首页 > net源码 > 正文

移动开发:美团外卖Android Lint代码检查实践

类别:net源码 日期:2018-4-14 18:15:55 人气: 来源:

  Lint是Google提供的Android静态代码检查工具,可以扫描并发现代码中潜在的问题,提醒开发人员及早修正,提高代码质量。除了Android原生提供的几百个Lint规则,还可以开发自定义Lint规则以满足实际需要。

  在美团外卖Android App的迭代过程中,线上问题频繁发生。开发时很容易写出一些问题代码,例如Serializable的使用:实现了Serializable接口的类,如果其变量引用的对象没有实现Serializable接口,序列化时就会Crash。我们对一些常见问题的原因和解决方法做分析总结,并在开发人员组内或跟测试人员一起分享交流,帮助相关人员主动避免这些问题。

  为了进一步减少问题发生,我们逐步完善了一些规范,包括制定代码规范,加强代码Review,完善测试流程等。但这些措施仍然存在各种不足,包括代码规范难以实施,沟通成本高,特别是开发人员变动频繁导致反复沟通等,因此其效果有限,相似问题仍然不时发生。另一方面,越来越多的总结、规范文档,对于组内新人也产生了不小的学习压力。

  我们调研发现,静态代码检查是一个很好的思。静态代码检查框架有很多种,例如FindBugs、PMD、Coverity,主要用于检查Java源文件或class文件;再例如Checkstyle,主要关注代码风格;但我们最终选择从Lint框架入手,因为它有诸多优势:

  在对Lint进行了充分的技术调研后,我们根据实际遇到的问题,又做了一些更深入的思考,包括应该用Lint解决哪些问题,怎么样更好的推广实施等,逐步形成了一套较为全面有效的方案。

  Lint开发过程中最主要的工作就是实现Scanner。Lint中包括多种类型的Scanner如下,其中最常用的是扫描Java源文件和XML文件的Scanner。

  开发过程中,我们比较关注App的Crash、Bug率等指标。通过长期的整理总结发现,有不少发生频率很高的代码问题,其原理和解决方案都很明确,但是在写代码时却很容易遗漏且难以发现;而Lint恰好很容易检查出这些问题。

  Crash率是App最重要的指标之一,避免Crash也一直是开发过程中比较头疼的一个问题,Lint可以很好的检查出一些潜在的Crash。例如:

  原生的NewApi,用于检查代码中是否调用了Android高版本才提供的API。在低版本设备中调用高版本API会导致Crash。

  自定义的SerializableCheck。实现了Serializable接口的类,如果其变量引用的对象没有实现Serializable接口,序列化时就会Crash。我们制定了一条代码规范,要求实现了Serializable接口的类,其变量(包括从父类继承的)所声明的类型都要实现Serializable接口。

  SpUsage:要求所有SharedPrefrence读写操作使用基础工具类,工具类中会做各种异常处理;同时定义SPConstants常量类,所有SP的Key都要在这个类定义,避免在代码中分散定义的Key之间冲突。

  TodoCheck:检查代码中是否还有TODO没完成。例如开发时可能会在代码中写一些假数据,但最终上线时要确保删除这些代码。这种检查项比较特殊,通常在开发完成后提测阶段才检查。

  ThreadConstruction:直接使用new Thread()创建线程(线程池除外),而需要使用统一的工具类在公用线程池执行后台操作。

  LogUsage:直接使用android.util.Log,必须使用统一工具类。工具类中可以控制Release包不输出Log,提高性能,也避免发生安全问题。

  除了代码风格方面的约束,代码规范更多的是用于减少或防止发生Bug、Crash、性能、安全等问题。很多问题在技术上难以直接检查,我们通过封装统一的基础库、制定代码规范的方式间接解决,而Lint检查则用于减少组内沟通成本、新人学习成本,并确保代码规范的落实。例如:

  早期我们将静态代码检查配置在Jenkins上,打包发布AAR/APK时,检查代码中的问题并生成报告。后来发现虽然静态代码检查能找出来不少问题,但是很少有人主动去看报告,特别是报告中还有过多无关紧要的、优先级很低的问题(例如过于严格的代码风格约束)。

  因此,一方面要确定检查哪些问题,另一方面,何时、通过什么样的技术手段来执行代码检查也很重要。我们结合技术实现,对此做了更多思考,确定了静态代码检查实施过程中的主要目标:

  重点关注高优先级问题,屏蔽低优先级问题。正如前面所说,如果代码检查报告中夹杂了大量无关紧要的问题,反而影响了关键问题的发现。

  高优问题的解决,要有一定的强制性。当检查发现高优先级的代码问题时,给开发者明确直接的报错,并通过技术手段约束,强制要求开发者修复。

  某些问题尽可能做到在第一时间发现,从而减少风险或损失。有些问题发现的越早越好,例如业务功能开发中使用了Android高版本API,通过Lint原生的NewApi可以检查出来。如果在开发期间发现,当时就可以考虑其他技术方案,实现困难时可以及时和产品、设计人员沟通;而如果到提代码、提测,甚至发版、上线时才发现,可能为时已晚。

  Error级别:明确需要解决的问题,包括Crash、明确的Bug、严重性能问题、不符合代码规范等,必须修复。

  Lint检查可以在多个阶段执行,包括在本地手动检查、编码实时检查、编译时检查、commit检查,以及在CI系统中提Pull Request时检查、打包发版时检查等,下面分别介绍。

  编码时检查即在Android Studio中写代码时在代码窗口实时报错。其好处很明显,开发者可以第一时间发现代码问题。但受限于Android Studio对自定义Lint的支持不完善,开发人员IDE的配置不同,需要开发者主动关注报错并修复,这种方式不能完全效果。

  在Android Studio 3.x版本中,打开Android工程源码后,IDE会加载工程中的自定义Lint规则,在设置菜单的Inspections列可以查看,和原生Lint效果相同(Android Studio会在打开源文件时触发对该文件的代码检查)。

  配置Gradle脚本可实现编译Android工程时执行Lint检查。好处是既可以尽早发现问题,又可以有强制性;缺点是对编译速度有一定的影响。

  利用git pre-commit hook,可以在本地commit代码前执行Lint检查,检查不通过则无法提交代码。这种方式的优势在于不影响开发时的编译速度,但发现问题相对滞后。

  技术实现方面,可以编写Gradle脚本,在每次同步工程时自动将hook脚本从工程拷贝到.git/hooks/文件夹下。

  作为代码提交流程规范的一部分,发Pull Request提代码时用CI系统检查Lint问题是一个常见、可行、有效的思。可配置CI检查通过后代码才能被合并。

  在本地编译和CI系统中做代码检查,都可以通过执行Gradle的Lint任务实现。可以在CI下给Gradle传递一个StartParameter,Gradle脚本中如果读取到这个参数,则配置LintOptions检查所有Lint问题;否则在本地编译下只检查部分高优先级Lint问题,减少对本地编译速度的影响。

  即使每次提代码时用CI系统执行Lint检查,仍然不能所有人的代码合并后一定没有问题;另外对于一些特殊的Lint规则,例如前面提到的TodoCheck,还希望在更晚的时候检查。

  于是在CI系统打包发布APK/AAR用于测试或发版时,还需要对所有代码再做一次Lint检查。

  为了方便代码管理,我们给自定义Lint创建了一个的工程,该工程打包生成一个AAR发布到Maven仓库,而被检查的Android工程依赖这个AAR(具体开发过程可以参考文章末尾链接)。

  自定义Lint虽然在工程中,但和被检查的Android工程中的代码规范、基础组件等存在较多耦合。

  例如我们使用正则表达式检查Android工程的资源文件命名规范,每次业务逻辑变动要新增资源文件前缀时,都要修改Lint工程,发布新的AAR,再更新到Android工程中,非常繁琐。另一方面,我们的Lint工程除了在外卖C端Android工程中使用,也希望能直接用在其他端的其他Android工程中,而不同工程之间存在差异。

  于是我们尝试使用配置文件来解决这一问题。以检查Log使用的LogUsage为例,不同工程封装了不同的Log工具类,报错时提示信息也应该不一样。定义配置文件名为custom-lint-config.json,放在被检查Android工程的模块目录下。在Android工程A中的配置文件是:

  Lint规则开发过程中,我们发现了一系列相似的需求:封装了基础工具类,希望大家都用起来;某个方法很容易抛出RuntimeException,有必要做处理,但Java语法上RuntimeException并不强制要求处理从而经常遗漏……

  这些相似的需求,每次在Lint工程中开发同样会很繁琐。我们尝试实现了几个模板,可以直接在Android工程中通过配置文件配置Lint规则。

  实现方面,主要是遍历Java语法树中特定类型的节点并转换成完整字符串(例如方法调用android.content.Intent.getIntExtra),然后检查是否有模板规则与其匹配。匹配成功后,DeprecatedApi规则直接输出message报错;HandleException规则会检查匹配到的节点是否处理了特定Exception(或Exception的父类),没有处理则报错。

  随着Lint新规则的不断开发,我们又遇到了一个问题。Android工程中存在大量历史代码,不符合新增Lint规则的要求,但也没有导致明显问题,这时接入新增Lint规则要求修改所有历史代码,成本较高而且有一定风险。例如新增代码规范,要求使用统一的线程工具类而不允许直接用Handler以避免内存泄露等。

  我们尝试了一个折中的方案:只检查指定git commit之后新增的文件。在配置文件中添加配置项,给Lint规则配置git-base属性,其值为commit ID,只检查此次commit之后新增的文件。

  经过一段时间的实践发现,Lint静态代码检查在解决特定问题时的效果非常好,例如发现一些语言或API层面比较明确的低级错误、帮助进行代码规范的约束。使用Lint前,不少这类问题恰好对开发人员来说又很容易遗漏(例如原生的NewApi检查、自定义的SerializableCheck);相同问题反复出现;代码规范的执行,特别是有新人参与开发时,需要很高的学习和沟通成本,还经常出现新人提交代码时由于没有遵守代码规范反复被要求修改。而使用Lint后,这些问题都能在第一时间得到解决,节省了大量的人力,提高了代码质量和开发效率,也提高了App的使用体验。

  子健,Android高级工程师,2015年毕业于西安电子科技大学并校招加入美团外卖。前期先后负责过外卖App首页、商家容器、评价等核心业务模块的开发,目前重点负责参与外卖打包自动化、代码检查、平台化等技术工作。

  美团外卖App团队诚招Android/iOS高级工程师/技术专家,工作地/上海可选,欢迎有兴趣的同学投递简历到

  

0
0
0
0
0
0
0
0
下一篇:没有资料

网友评论 ()条 查看

姓名: 验证码: 看不清楚,换一个

推荐文章更多

热门图文更多

最新文章更多

关于联系我们 - 广告服务 - 友情链接 - 网站地图 - 版权声明 - 人才招聘 - 帮助

CopyRight 2002-2012 技术支持 源码吧 FXT All Rights Reserved

赞助合作: