上个月,笔者在巴黎 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 getApplicableElements() {
return Collections.singletonList(TAG_ATTR);
}
// Only name attribute
@Override
public Collection 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 getApplicableCallNames() {
return Arrays.asList("v", "d", "i", "w", "e", "wtf");
}
@Override
public List 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 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:
以下为笔者的参考资料:
原文地址:http://jeremie-martinez.com/2015/12/15/custom-lint-rules/
OneAPM Mobile Insight ,监控网络请求及网络错误,提升用户留存。访问 OneAPM 官方网站感受更多应用性能优化体验,想阅读更多技术文章,请访问 OneAPM 官方技术博客。
本文转自 OneAPM 官方博客