自定义 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(返回 ...
随机推荐
- 20160503-spring入门2
使用Spring需要的jar 到http://www.springsource.org/download下载spring,然后进行解压缩,在解压目录中找到下面jar文件,拷贝到类路径下 dist\sp ...
- C# is 强制转换
在平时开发中,经常遇上强制转换,在这过程中经常遇上null对象转换为值类型,如果不判断的情况下在编译的时候不会出错,但程序一运行就抛出错误.好在C#为我们提供了is ,它判断一个对象如果成立就转换,如 ...
- Jsp与servlet之间页面跳转及参数传递实例(转)
原网址:http://blog.csdn.net/ssy_shandong/article/details/9328985 11. jsp与servlet之间页面跳转及参数传递实例 分类: Java ...
- make fontconfig 时出现No package ‘libxml-2.0′ found的解决方法
这里显示一个错误信息:checking for LIBXML2… configure: error: Package requirements (libxml-2.0 >= 2.6) were ...
- [01] Oracle数据库简介
Oracle关系型数据库:建立在关系模型上. Oracle10g:g(grid)网格技术,网格计算(Grid Computing)通过网络共享,将大量的计算机连接起来,联合各个计算机的多余处理能力,产 ...
- Mysql 创建联合索引注意事项
当一个表有多条索引可走时, Mysql 根据查询语句的成本来选择走哪条索引, 联合索引的话, 它往往计算的是第一个字段(最左边那个), 这样往往会走错索引. 如: 索引Index_1(Crea ...
- Codevs 1173 最优贸易 2009年NOIP全国联赛提高组
1173 最优贸易 2009年NOIP全国联赛提高组 时间限制: 1 s 空间限制: 128000 KB 题目等级 : 钻石 Diamond 题目描述 Description [问题描述] C 国有n ...
- ORA-22275: invalid LOB locator specified
性能测试20行和20000行的出现错误 20行的字符没有问题,两万行的出行问题如下 [2014-02-11 09:21:03.343665][17694862] Level 0 cicmpub.C: ...
- [cocos2d-x 2.0.4][iOS7]图片加载错误
本篇文章由:http://www.sollyu.com/cocos2d-x-2-0-4-ios7-image-loading-errors/ 说明 错误提示 <Error>: CGBitm ...
- iOS 多态的简单思想
/** * 多态:多种形态,是对于对象而言的,如果没有继承就没有多态 * * 在代码中体现:就是通过定义父类指针指向子类的对象 * * 好处:在函数或者方法的参数中如果传入的是父类指针,那么实现的时候 ...