天天看點

你所不知道的LintLint自定義Lint如何使用這一切值得嗎

文章目錄

  • Lint
  • 自定義Lint
    • Implementation
    • Issue
    • Detector
  • 如何使用
  • 這一切值得嗎
原文位址:What is Android Lint and how it helps write maintainable code

一些開發人員由于不夠謹慎,進而導緻某些代碼會有瑕疵。以下列舉幾個經常容易犯錯的場景:比如舊版本的代碼不支援新版本的功能,比如需要某些特定的權限,比如缺少翻譯等等。

更加最重要的是,Java、 Kotlin 和其他的程式設計語言一樣,都有自己的一套程式設計結構。如果不注意的話,在某種情況下,某些編碼習慣,可能會導緻性能低下。

Lint

我們可以通過使用Lint 去避免上述問題的發生。它是個用于在代碼編譯之前檢測靜态代碼的工具。它針對源碼進行多次檢測,可以檢測出類似:未使用的變量、方法參數,待簡化的判斷條件,錯誤的作用域,未定義的變量、方法,待優化代碼等問題。對于安卓的開發人員來說,我們有一套已經寫好的Lint checks,可以參考 此處。

但是有時候,我們需要檢查代碼中比較特殊的問題,而且已經存在的Lint check 并不滿足要求的時候,我們需要自定義Lint check。

自定義Lint

在我們開始編碼之前,我們先确定我們的目标,并了解如何根據Lint Api去實作我們的目标。 我們的目标是:建立一個Lint check 去檢查一個對象的錯的方法調用。具體來說就是檢測在安卓視圖元件上設定點選監聽器的方法是否會多次連續點選,這樣我們就可以避免打開相同的Activity 或者 多次調用API了。

自定義Lint check的工程和标準的java module類似,最簡單的辦法就是,建立一個簡單的基于gradle的項目(并不一定是Android項目)。(可以建立java library,這個也可以)

下一步,你需要添加lint的依賴關系,再你剛建立的module 的build.gradle 檔案中添加如下依賴:

compileOnly "com.android.tools.lint:lint-api:$lintVersion"
compileOnly "com.android.tools.lint:lint-checks:$lintVersion"
           

這個将會同時設定你的編寫(writing)和測試(testing)相關的依賴,我通過研究這個話題,學到了一些技巧,如上面配置的 lintVersion ,必須是你的 gradlePluginVersion+ 23.0.0。 對于使用Android Studio 建立的項目來說,gradlePluginVersion 被定義在根檔案下的項目級别的build.gradle檔案夾内(比如:

classpath 'com.android.tools.build:gradle:3.1.2'

中gradlePluginVersion就是3.1.2)。 最後一個正式版本是3.3.0 這也就意味着lintVersion 必須是26.3.0

每一個 lint check 包含四部分

  • Issue:一個需要我們嘗試去阻止在我們代碼中發生的問題。 當lint check 檢測未通過的時候,它需要通知使用者。 (将代碼中的問題通知使用者)
  • Detector:一個使用Lint Api來發現我們源碼中暴露的問題的工具 (使用lint api檢查問題)
  • Implementation:問題可能發生的範圍(源碼檔案 xml檔案 編譯後的代碼)(确定問題檢查範圍)
  • Registry:自定義的lint check的 registry 同之前預定義的已經存在的 register 一起使用 (注冊使其生效)

下面将分别介紹上面的四部分,跟随着一步步去實踐,去完成對Lint的認識。

Implementation

讓我們針對我們的自定義lint check,開始建立一個 implementation 。 對于每一個implementation 的實作都需要傳入一個具體的實作detector 接口的類和指定的檢查範圍,如下方代碼:

此處的Scope.JAVA_FILE_SCOPE 同樣也适用于檢查Kotlin

Issue

下一步就是使用我們的implementation 去建立我們的Issue,每個Issue 都包含以下幾個部分

  • ID:唯一辨別
  • Description:剪短的摘要(5-6個字)
  • Explanation:完整的說明,包含建議和如何修複
  • Category: 分類(性能、注釋、安全)
  • Priority:用于表明Issue的重要性,共10檔,從1-10,10是最重要,這個可以在執行Lint
  • Task之後生成的報告中用于排序。
  • Severity:嚴重性(fatal(緻命錯誤)、error(一般性錯誤)、warning(警告)、info(提示)、ignore(忽略))
  • Implementation:用于檢查代碼尋找對應的問題
    val ISSUE_CLICK_LISTENER = Issue.create(
        id = "UnsafeClickListener",
        briefDescription = "Unsafe click listener", 
        explanation = """"
            This check ensures you call click listener that is throttled 
            instead of a normal one which does not prevent double clicks.
            """.trimIndent(),
        category = Category.CORRECTNESS,
        priority = 6,
        severity = Severity.WARNING,
        implementation = correctClickListenerImplementation
    )
               

Detector

Lint Api提供的接口,你可以實作它的接口然後去檢查你想要校驗的範圍内的東西。針對這些接口暴露的方法你可以針對你感興趣的去重寫(overrrid)或 獲得其源代碼。

  • UastScanner: 用于掃描Java和kotlin的檔案
  • ClassScanner:用于掃描編譯後的檔案(位元組碼)
  • BinaryResourceScanner:二進制的資源,類似 bitmaps 或者在res/raw的檔案
  • ResourceFolderScanner:資源檔案
  • XmlScanner: xml files
  • GradleScanner: gradle files
  • OtherFileScanner: 任何檔案

此外,Detector類是一個基類,它具有上述每個接口公開的所有方法的虛拟實作(抽象類),是以,如果隻需要一個方法,則不必強制實作完整的接口。

現在,我們準備實作一個Detector,它将用于實作剛才我們定的目标的那個功能。

private const val REPORT_MESSAGE = "Use setThrottlingClickListener"

/**
 * Custom detector class that extends base Detector class and specific
 * interface depending on which part of the code we want to analyze.
 */
class CorrectClickListenerDetector : Detector(), Detector.UastScanner {

/**
* Method that defines which elements of the code we want to analyze.
* There are many similar methods for different elements in the code,
* but for our use-case, we want to analyze method calls so we return
* just one element representing method calls.
*/
override fun getApplicableUastTypes(): List<Class<out UElement>>? {
    return listOf<Class<out UElement>>(UCallExpression::class.java)
}

/**
    * Since we've defined applicable UAST types, we have to override the
    * method that will create UAST handler for those types.
    * Handler requires implementation of an UElementHandler which is a
    * class that defines a number of different methods that handle
    * element like annotations, breaks, loops, imports, etc. In our case,
    * we've defined only call expressions so we override just this one method.
    * Method implementation is pretty straight-forward - it checks if a method
    * that is called has the name we want to avoid and it reports an issue otherwise.
    */
    override fun createUastHandler(context: JavaContext): UElementHandler? {
        return object: UElementHandler() {

            override fun visitCallExpression(node: UCallExpression) {
                if (node.methodName != null && node.methodName?.equals("setOnClickListener", ignoreCase = true) == true) {
                    context.report(ISSUE_CLICK_LISTENER, node, context.getLocation(node), REPORT_MESSAGE, createFix())
                }
            }
        }
    }

    /**
     * Method will create a fix which can be trigger within IDE and
     * it will replace incorrect method with a correct one.
     */
    private fun createFix(): LintFix {
        return fix().replace().text("setOnClickListener").with("setThrottlingClickListener").build()
    }
}
           

我們需要做的最後一件事是向Registry中添加我們的Issue,并告訴Lint,我們有自定義的Registry,他應該和預設問題一起使用。

class MyIssueRegistry : IssueRegistry() {
    override val issues: List<Issue> = listOf(ISSUE_CLICK_LISTENER)
}
           

在目前的module中的build.gradle中按照如下設定

jar {
    manifest {
        attributes("Lint-Registry-v2": "co.infinum.lint.MyIssueRegistry")
    }
}
           

其中co.infinum.lint是MyIssueRegistry類的包。你需要根據你的實際情況,填寫你的對應的包名類名,現在,您可以使用gradlew腳本運作jar任務,庫應該出現在build / libs目錄中。

如何使用

新的Lint checks已準備好用于項目。如果此Lint checks可以應用于所有項目,則可以将其放在.android / lint檔案夾中(如果它不存在,則可以建立它),該檔案夾應位于您的主檔案夾中。(windows 中c 盤的位置,mac中全局的位置,不是project的根目錄)

此外,您可以将檢查作為項目中的子產品進行開發,并使用lintChecks方法将該子產品包含為任何其他依賴項。如果您将jar檔案上傳到Bintray,也可以使用此方法。

這一切值得嗎

Lint是每個開發人員都應該非常熟練掌握的工具,因為它具有能夠預先檢測代碼中潛在問題的能力。雖然因為其Api過于複雜導緻自定義Lint checks并不容易,但是一旦學會使用并應用起來,可以節省大量的時間和精力,是以它們絕對值得去學習。