自定義 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 官方博客

相關文章
相關標籤/搜索