自定义 Lint 规则简介

上个月,笔者在巴黎 Droidcon 的 BarCamp 研讨会上聆听了 Matthew Compton 关于编写本身的 Lint 规则的讲话。深受启发以后,笔者想就此话题作进一步的探索。html

定义

若是你是安卓开发者,那你必定已经知道 Lint 的定义。java

Lint 是一款静态代码分析工具,能检查安卓项目的源文件,从而查找潜在的程序错误以及优化提高的方案。android

当你忘记在Toast上调用show()时,Lint 就会提醒你。它也会确保你的ImageView中添加了contentDescription,以支持可用性。相似的例子还有成千上万个。诚然,Lint 能在诸多方面提供帮助,包括:正确性,安全,性能,易用性,可用性,国际化等等。git

Lint 易于使用,经过简单的 Gradle 任务:./gradlew lint 就能在任意安卓项目上运行。它会生成一份报告,指出它的发现并按照种类、优先级和严重程度对问题进行分类。这份报告能确保代码质量,防止 app 中出现代码错误,所以应该时刻进行监控。github

在简单的介绍以后,笔者但愿你们能达成共识:Lint 是理解一些安卓 API 框架使用状况的好帮手。算法

为何要本身写 Lint 规则?

大多数开发者可能都不知道:你能够本身写 Lint 规则。其实,在不少使用案例中,自定义的 Lint 规则每每大有用处:api

  1. 若是你在写一个代码库/SDK,你想帮助开发者正确地使用它,Lint 规则就能派上用场。有了 Lint,你能够轻易地提醒他们忽略或作错的事情。安全

  2. 若是你的团队有了新加入的开发者,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,这样即使存在该问题,代码也能安全运行。

  • ImplementationDetector间的桥梁,用于发现问题。Scope则用于分析问题。在本例中,咱们必须处于资源文件层面才能分析前缀问题。

你可能也发现了,其实所需的代码很是简单易懂。你只需当心所用的范围以及为Issue输入的值便可。

Lint 报告可能得出的结果以下:

App screenshot

规则二:生产环境下禁止 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`");
        }
    }
}

如你所见,规则二的模式与规则一相同。方法getApplicableCallNamesgetApplicableMethodNames用于明确寻找的目标。以后,咱们找出问题并建立之。惟一的不一样在于,咱们再也不继承XmlResourceDetector类,而是仅继承Detector类,并实现ClassScanner接口以处理 Java 类检查。因此,实际上,规则二的变化没有不少。若是仔细查看XmlResourceDetector类,会发现它只是实现XmlScannerDetector类。所以,全部规则都适用的总结以下:咱们只需继承Detector并实现合适的Scanner接口便可。

最后,改变Issue的范围并关闭CLASS_FILE_SCOPE。此处,要想找到问题,只需分析一个 Java 类文件便可。有时,你须要分析多个 Java 类文件才能发现问题,因此你须要使用ALL_CLASS_FILES。范围的选择很是重要,所以请当心谨慎。点击此处可查看所有范围。

虽然问题描述可能不很清楚,但一个Detector能够发现多个问题。此外,经过一次运行就能处理全部问题,所以能够有效提升应用性能。

规则二的 Lint 报告结果举例以下:

App screenshot

登记

此处,咱们遗漏了一项重要的事情:登记!咱们须要将新建立的问题登记到全部处理过的 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),还有许多其余类型:GradleScannerOtherFileScanner等着你发现。多多尝试,找到最适合你的类。

笔者建议,在编写自定义规则以前,首先阅读系统 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 官方博客

相关文章
相关标签/搜索