你所不知道的Lint

原文地址: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 接口的類和指定的檢查範圍,如下方代碼:

val correctClickListenerImplementation = Implementation(CorrectClickListenerDetector::class.java, Scope.JAVA_FILE_SCOPE)

此處的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並不容易,但是一旦學會使用並應用起來,可以節省大量的時間和精力,所以它們絕對值得去學習。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章