自定义 Lint 规则简介
上个月,笔者在巴黎 Droidcon 的 BarCamp 研讨会上聆听了 Matthew Compton 关于编写自己的 Lint 规则的讲话。深受启发之后,笔者想就此话题做进一步的探索。
定义
如果你是安卓开发者,那你一定已经知道 Lint 的定义。
Lint 是一款静态代码分析工具,能检查安卓项目的源文件,从而查找潜在的程序错误以及优化提升的方案。
当你忘记在Toast
上调用show()
时,Lint 就会提醒你。它也会确保你的ImageView
中添加了contentDescription
,以支持可用性。类似的例子还有成千上万个。诚然,Lint 能在诸多方面提供帮助,包括:正确性,安全,性能,易用性,可用性,国际化等等。
Lint 易于使用,通过简单的 Gradle 任务:./gradlew lint
就能在任意安卓项目上运行。它会生成一份报告,指出它的发现并按照种类、优先级和严重程度对问题进行分类。这份报告能确保代码质量,防止 app 中出现代码错误,因此应该时刻进行监控。
在简单的介绍之后,笔者希望大家能达成共识:Lint 是理解一些安卓 API 框架使用情况的好帮手。
为什么要自己写 Lint 规则?
大多数开发者可能都不知道:你可以自己写 Lint 规则。其实,在很多使用案例中,自定义的 Lint 规则往往大有用处:
如果你在写一个代码库/SDK,你想帮助开发者正确地使用它,Lint 规则就能派上用场。有了 Lint,你可以轻易地提醒他们忽略或做错的事情。
如果你的团队有了新加入的开发者,Lint 可以帮助他快速了解团队的最佳实践,或命名惯例。
一些例子
你可能知道,笔者最近加入了 CaptainTrain 安卓团队。下面的例子基于笔者为自己的 app 创建的两条 Lint 规则,这些规则完美地展示了 Lint 确保开发者遵循项目编码实践的妙用。
Gradle
自定义的 Lint 规则必须实现在一个新的模块中。以下是一个 build.gradle
例子:
apply plugin: 'java'
targetCompatibility = JavaVersion.VERSION_1_7
sourceCompatibility = JavaVersion.VERSION_1_7
configurations {
lintChecks
}
dependencies {
compile 'com.android.tools.lint:lint-api:24.3.1'
compile 'com.android.tools.lint:lint-checks:24.3.1'
lintChecks files(jar)
}
jar {
manifest {
attributes('Lint-Registry': 'com.captaintrain.android.lint.CaptainRegistry')
}
}
defaultTasks 'assemble'
task install(type: Copy, dependsOn: build) {
from configurations.lintChecks
into System.getProperty('user.home') + '/.android/lint/'
}
如你所见,为了实现自定义 Lint 规则,需要两个编译依赖关系。此外,还需要确切的 Lint-Registry,后文会介绍这是什么,现在只需记住这是强制要求。最后,创建一个小任务来快速安装新的 Lint 规则。
接着,使用../gradlew clean install
编译并部署该模块。
配置好模块之后,让我们来看看如何编写第一条规则。
规则一:Attr (属性)必须有前缀
在 CaptainTrain 项目中,我们都会在属性前面添加ct
前缀,从而避免与其他代码库发生冲突。新的开发者很容易忘记这一点,因此笔者写了如下规则:
public class AttrPrefixDetector extends ResourceXmlDetector {
public static final Issue ISSUE = Issue.create("AttrNotPrefixed",
"You must prefix your custom attr by `ct`",
"We prefix all our attrs to avoid clashes.",
Category.TYPOGRAPHY,
5,
Severity.WARNING,
new Implementation(AttrPrefixDetector.class,
Scope.RESOURCE_FILE_SCOPE));
// Only XML files
@Override
public boolean appliesTo(@NonNull Context context,
@NonNull File file) {
return LintUtils.isXmlFile(file);
}
// Only values folder
@Override
public boolean appliesTo(ResourceFolderType folderType) {
return ResourceFolderType.VALUES == folderType;
}
// Only attr tag
@Override
public Collection<String> getApplicableElements() {
return Collections.singletonList(TAG_ATTR);
}
// Only name attribute
@Override
public Collection<String> getApplicableAttributes() {
return Collections.singletonList(ATTR_NAME);
}
@Override
public void visitElement(XmlContext context, Element element) {
final Attr attributeNode = element.getAttributeNode(ATTR_NAME);
if (attributeNode != null) {
final String val = attributeNode.getValue();
if (!val.startsWith("android:") && !val.startsWith("ct")) {
context.report(ISSUE,
attributeNode,
context.getLocation(attributeNode),
"You must prefix your custom attr by `ct`");
}
}
}
}
如你所见,我们继承了ResourceXmlDetector
类。Detector
类允许我们发现问题,并报告Issue
。首先,我们必须明确寻找什么:
- 第一个
appliesTo
方法会只保留 XML 文件。 - 第二个
appliesTo
方法会只保留资源文件夹中的values
。 getApplicableElements
方法会只保留attr
XML 元素。getApplicableAttributes
方法会只保留name
XML 属性。
过滤之后,我们使用简单的算法实现visitElement
方法。一旦发现某个attr
XML 标记的name
属性不源自安卓也不以ct
前缀,我们就报告一个Issue
。该Issue
按照如下方式声明在类的头部:
public static final Issue ISSUE = Issue.create("AttrNotPrefixed",
"You must prefix your custom attr by `ct`",
"To avoid clashes, we prefixed all our attrs.",
Category.TYPOGRAPHY,
5,
Severity.WARNING,
new Implementation(AttrPrefixDetector.class,
Scope.RESOURCE_FILE_SCOPE));
其中,每个参数都很重要,而且是强制性参数。
- AttrNotPrefixed 是 Lint 规则的 id,必须是唯一的。
You must prefix your custom attr by ct
(必须以 ct 作为自定义属性的前缀)是简述。To avoid clashes, we prefixed all our attrs.
(为避免冲突,所有属性均添加前缀。)是更为详细的解释。5
是优先级系数。必须是1到10之间的某个值。WARNING
是严重程度。此处我们只选择WARNING
,这样即便存在该问题,代码也能安全运行。Implementation
是Detector
间的桥梁,用于发现问题。Scope
则用于分析问题。在本例中,我们必须处于资源文件层面才能分析前缀问题。
你可能也发现了,其实所需的代码非常简单易懂。你只需小心所用的范围以及为Issue
输入的值即可。
Lint 报告可能得出的结果如下:
规则二:生产环境下禁止 log
在 CaptainTrain 应用中,我们将所有Log
调用都包装到一个新的类里。由于在生产环境下,日志有可能妨碍应用性能与用户数据的安全,该类旨在BuildConfig.DEBUG
为非时禁用日志。此外,该类还能帮助日志排版,以及提供一些其他特性。举例如下:
public class LoggerUsageDetector extends Detector
implements Detector.ClassScanner {
public static final Issue ISSUE = Issue.create("LogUtilsNotUsed",
"You must use our `LogUtils`",
"Logging should be avoided in production for security and performance reasons. Therefore, we created a LogUtils that wraps all our calls to Logger and disable them for release flavor.",
Category.MESSAGES,
9,
Severity.ERROR,
new Implementation(LoggerUsageDetector.class,
Scope.CLASS_FILE_SCOPE));
@Override
public List<String> getApplicableCallNames() {
return Arrays.asList("v", "d", "i", "w", "e", "wtf");
}
@Override
public List<String> getApplicableMethodNames() {
return Arrays.asList("v", "d", "i", "w", "e", "wtf");
}
@Override
public void checkCall(@NonNull ClassContext context,
@NonNull ClassNode classNode,
@NonNull MethodNode method,
@NonNull MethodInsnNode call) {
String owner = call.owner;
if (owner.startsWith("android/util/Log")) {
context.report(ISSUE,
method,
call,
context.getLocation(call),
"You must use our `LogUtils`");
}
}
}
如你所见,规则二的模式与规则一相同。方法getApplicableCallNames
与getApplicableMethodNames
用于明确寻找的目标。之后,我们找出问题并创建之。唯一的不同在于,我们不再继承XmlResourceDetector
类,而是仅继承Detector
类,并实现ClassScanner
接口以处理 Java 类检查。所以,实际上,规则二的变化没有很多。如果仔细查看XmlResourceDetector
类,会发现它只是实现XmlScanner
的Detector
类。因此,所有规则都适用的总结如下:我们只需继承Detector
并实现合适的Scanner
接口即可。
最后,改变Issue
的范围并关闭CLASS_FILE_SCOPE
。此处,要想找到问题,只需分析一个 Java 类文件即可。有时,你需要分析多个 Java 类文件才能发现问题,所以你需要使用ALL_CLASS_FILES
。范围的选择非常重要,因此请小心谨慎。点击此处可查看全部范围。
虽然问题描述可能不很清楚,但一个Detector
可以发现多个问题。此外,通过一次运行就能处理所有问题,因此可以有效提高应用性能。
规则二的 Lint 报告结果举例如下:
登记
此处,我们遗漏了一项重要的事情:登记!我们需要将新创建的问题登记到所有处理过的 lint 检查列表中:
public final class CaptainRegistry extends IssueRegistry {
@Override
public List<Issue> getIssues() {
return Arrays.asList(LoggerUsageDetector.ISSUE, AttrPrefixDetector.ISSUE);
}
}
如你所见,登记过程也非常简单。我们只需继承IssueRegistry
类并实现getIssues
方法,从而返回我们的自定义问题。该类必须与早前在build.gradle
中声明的类保持一致。
结论
虽然只展示了两个简单的例子,但笔者希望大家能知道:Lint 是非常强大的。只是你要编写适合自己的规则。
本文只展示了两种类型(Detector/Scanner
),还有许多其他类型:GradleScanner
,OtherFileScanner
等着你发现。多多尝试,找到最适合你的类。
笔者建议,在编写自定义规则之前,首先阅读系统 Lint 规则,从而帮助你理解其用处及用法。其源码可以在此处下载。
最后,Lint 能帮助你解决开发中的错误,请一定要用哦!
Find below all materials that helped me:
以下为笔者的参考资料:
- https://github.com/bignerdranch/linette
- https://speakerdeck.com/ambergleam/linty-fresh
- Source code
- http://tools.android.com/tips/lint-custom-rules
- https://github.com/googlesamples/android-custom-lint-rules
原文地址:http://jeremie-martinez.com/2015/12/15/custom-lint-rules/
OneAPM Mobile Insight,监控网络请求及网络错误,提升用户留存。访问 OneAPM 官方网站感受更多应用性能优化体验,想阅读更多技术文章,请访问 OneAPM 官方技术博客。
本文转自 OneAPM 官方博客
自定义 Lint 规则简介的更多相关文章
- 团队项目必备神器——自定义Lint
Lint 在android studio中内置了大概200个左右的lint检查,比如定义变量未使用,直接Handler报内存泄漏提醒,时时刻刻在监督着我们的代码.自己定制了一些Lint规则,项目开源在 ...
- 微信分享网页时自定义缩略图和简介(.net版本)
要实现微信分享网页时自定义缩略图和简介,需开发者在公众平台网站中创建公众号.获取接口权限后,通过微信JS-SDK的分享接口,来实现微信分享功能. 下面来说明实现步骤. 第一部分 准备步骤 步骤一:注册 ...
- MVC系列——MVC源码学习:打造自己的MVC框架(三:自定义路由规则)
前言:上篇介绍了下自己的MVC框架前两个版本,经过两天的整理,版本三基本已经完成,今天还是发出来供大家参考和学习.虽然微软的Routing功能已经非常强大,完全没有必要再“重复造轮子”了,但博主还是觉 ...
- struts2 自定义校验规则
自定义校验规则:(了解) 在Struts2自定义校验规则: 1.实现一个Validator 接口. 2.一般开发中继承ValidatorSupport 或者 FieldValidatorSupport ...
- yii2中自定义验证规则rules
作者:白狼 出处:www.manks.top/article/yii2_custom_rules 本文版权归作者,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追 ...
- CI 框架中的自定义路由规则
在 CI 框架中,一个 URL 和它对应的控制器中的类以及类中的方法是一一对应的,如: www.test.com/user/info/zhaoyingnan 其中 user 对应的就是控制器中的 us ...
- easyui的validatebox重写自定义验证规则的几个实例
validatebox已经实现的几个规则: 验证规则是根据使用需求和验证类型属性来定义的,这些规则已经实现(easyui API): email:匹配E-Mail的正则表达式规则. url:匹配URL ...
- 在ubuntu16.04中安装apache2+modsecurity以及自定义WAF规则详解
一.Modsecurity规则语法示例 SecRule是ModSecurity主要的指令,用于创建安全规则.其基本语法如下: SecRule VARIABLES OPERATOR [ACTIONS] ...
- Scarpy 起始url 自定义代理 自定义去重规则
- start_urls - 内部原理 """ scrapy引擎来爬虫中去起始的URL: 1. 调用start_requests并获取返回值 2. v = iter(返回 ...
随机推荐
- 使用jvisualvm和飞行记录器分析Java程序cpu占用率过高
一.jvisualvm使用 JDK1.6中Oracle提供了一个新的JVM监控工具:jvisualvm.下面重点介绍如何在本地通过远程的方式打开Linux服务器上的jvisualvm. 1.Xmana ...
- C#前端頁面判斷控件
var chbClass = document.getElementById("<%=DDL_CheckboxUserClass1.ClientID %>" + &qu ...
- JAXB - XML Schema Types, Defining Types for XML Elements With Content
Content: A Value The content of an XML element may be some value, or one or more subordinate element ...
- 【JQuery基础教程(第三版)图灵】笔记
第1章 jQuery入门 1.jQuery官方网站:http://jquery.com 2.开发工具:Firebug 第2章 选择元素 1.属性选择符:属性选择符通过HTML元素的 ...
- JavaScript学习笔记(6)——JavaScript语法之对象
JavaScript 中的所有事物都是对象:字符串.数字.数组.日期,等等. 在 JavaScript 中,对象是拥有属性和方法的数据. 属性是与对象相关的值. 方法是能够在对象上执行的动作. 提示: ...
- bzoj1016:[JSOI2008]最小生成树计数
思路:模拟kruskal的过程,可以发现对于所有权值相同的边,有很多种选择的方案,而且权值不同的边并不会相互影响,因为先考虑权值较小的边,权值比当前权值大的边显然不在考虑范围之内,而权值比当前权值小的 ...
- Convert CString to TCHAR
Quote from: http://vctipsplusplus.wordpress.com/2008/05/21/cstring-to-tchar/ CString is a very usefu ...
- 4.MySQL连接并选择数据库(SQL & C)
在连接了MySQL数据库之后,可以通过SQL命令或者C.PHP.JAVA等程序来指定需要操作的数据库.这里主要介绍SQL命令和相应的C程序. 首先创建用户rick(赋予所有权限) mysql> ...
- 七牛云覆盖上传 php
使用七牛云过程中遇到了需要上传覆盖的情况,最终解决,分享给大家. 七牛云sdk上传示例中是这样写的 <?php require_once 'path_to_sdk/vendor/autoload ...
- jQuery Mobile里xxx怎么用呀? (事件篇)
jQuery Mobile里$(document).ready()怎么用呀? 相关链接: http://stackoverflow.com/questions/14468659/jquery-mobi ...