前言
Lint 是 Android Studio 內置的一個靜態代碼掃描工具,它可以檢查 Android 項目源文件是否包含潛在錯誤,以及在正確性、安全性、性能、易用性、便利性和國際化方面是否需要優化改進。AS 已經內置了大量的 Lint 檢查規則,但是當我們需要定製化規則時,就需要考慮自定義 Lint 了。
首先,先要仔細想想,什麼時候需要用到自定義 Lint 呢?
這就要發揮你的想象力了,我最初是想推 webp,用以替換掉 png,這樣就需要開發者每次自覺的將 png 轉化為 webp,但是又不可能結對編程,監督他進行轉化,所以自定義 Lint 就派上用場了。
但是,目前網上的絕大多數博客都還停留在 AS 2.x 的 Lint API 上,關於 AS 3.x Lint API 的幾乎是空白。從 AS 2.x 到 AS 3.x,Lint API 變化還挺大的,特別是對 Java 源文件的檢測,從 JavaScanner 過渡到 JavaPsiScanner 再到 UastScanner,自定義 Lint 的註冊以及引用方式也都發生了變化。本文也是從 Google 的一個 Sample 再參考系統內置的 Lint 實現,一點點摸索出來的。
以下是項目中的自定義 Lint 實現效果:
以上 Detector 都有什麼用呢?
ToastDetector:Android 自帶的 Lint 規則,用於提示 Toast 忘記調用 show 方法,在 BuiltinIssueRegistry 類可以查看內置的所有的 Detector,這也是我實現自定義 Lint 的主要參考依據。
SampleCodeDetector:Google Sample 中的 Demo,用於檢測文本表達式中是否包含特定的字符串。
PngDetector:用於檢測所有 layout 或 java 文件中引用的 png 資源,提示使用 webp。
LogDetector:用於檢測使用 Log 類的 i、d、e 等日誌輸出的方法,提示使用統一的日誌工具類。嚴重程度為 Error,所以默認情況下,會中斷編譯流程。如果嚴重程度為 Warning,僅僅是報黃警告。
ThreadDetector:用於檢測直接通過 new Thread 創建線程,提示應該使用統一線程池。
源碼地址:https://github.com/Omooo/CustomLint
正文
先來看一個簡單的自定義 Lint 是怎麼樣一步一步寫出來的,該例子實際上來自 https://github.com/googlesamples/android-custom-lint-rules ,該 Rep 就一個 SampleCodeDetector,也就是上文中所說的。
自定義 Lint 一共可以分為四步:
第一步:創建 java library 工程
在 build.gradle 文件裡添加依賴:
<code>compileOnly "com.android.tools.lint:lint-api:26.4.1"
compileOnly "com.android.tools.lint:lint-checks:26.4.1"/<code>
第二步:創建 Detector
<code>public class SampleDetector extends Detector implements Detector.UastScanner {
//第一步:定義 ISSUE
public static final Issue ISSUE = Issue.create(
"ShortUniqueId", //唯一 ID
"Lint Mentions", //簡單描述
"Blah blah blah.", //詳細描述
Category.CORRECTNESS, //問題種類(正確性、安全性等)
6, //權重
Severity.WARNING, //問題嚴重程度(忽略、警告、錯誤)
new Implementation( //實現,包括處理實例和作用域
SampleCodeDetector.class,
Scope.JAVA_FILE_SCOPE));
//第二步:定義檢測類型以及處理邏輯
//檢測類型包括文本表達式、調用相關表達式等
@Override
public List<class>> getApplicableUastTypes() {
return Collections.singletonList(ULiteralExpression.class);
}
@Override
public UElementHandler createUastHandler(@NotNull JavaContext context) {
return new UElementHandler() {
@Override
public void visitLiteralExpression(@NotNull ULiteralExpression expression) {
String string = UastLiteralUtils.getValueIfStringLiteral(expression);
if (string == null) {
return;
}
if (string.contains("Omooo") && string.matches(".*\\\\bOmooo\\\\b.*")) {
//第三步:符合條件,上報 ISSUE
context.report(ISSUE, expression, context.getLocation(expression),
"This code mentions `Omooo`");
}
}
};
}
}/<class>/<code>
Lint API 中內置了很多 Scanner:
Scanner 類型DescUastScanner掃描 Java、Kotlin 源文件XmlScanner掃描 XML 文件ResourceFolderScanner掃描資源文件夾ClassScanner掃描 Class 文件BinaryResourceScanner掃描二進制資源文件
更多請參考:https://static.javadoc.io/com.android.tools.lint/lint-api/25.3.0/com/android/tools/lint/detector/api/package-summary.html
注意:
這裡需要注意的一點是,如果對應的 ISSUE 嚴重程度為錯誤(Severity.ERROR),那麼在默認情況下,會中斷編譯流程,當然,你也可以配置 LintOptions 來抑制 Lint 錯誤。
第三步:註冊 Detector
<code>public class CustomIssueRegistry extends IssueRegistry {
@NotNull
@Override
public List<issue> getIssues() {
return Arrays.asList(
SampleCodeDetector.ISSUE);
}
@Override
public int getApi() {
return ApiKt.CURRENT_API;
}
}/<issue>/<code>
這裡可以註冊多個 Detector,目前最新版本的 Lint 內置了 360 種 Detector,都在 BuiltinIssueRegistry 類中,可以作為我們編寫自定義 Lint 的最佳參考案例。
第四步:引入自定義 Lint
首先需要在 lint_library 中的 build.gradle 文件中添加,完整代碼為:
<code>apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
compileOnly "com.android.tools.lint:lint-api:26.4.1"
compileOnly "com.android.tools.lint:lint-checks:26.4.1"
}
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
jar {
manifest {
attributes("Lint-Registry-v2": "top.omooo.lint_library.CustomIssueRegistry")
}
}/<code>
然後就可以在 app module 中的 build.gradle 中引入並使用了:
<code>dependencies {
lintChecks project(":lint_library")
}/<code>
Lint 進階
以上,一個簡單的自定義 Lint 就寫完了,但是有點意猶未盡的感覺。這時候就需要你發揮想象力,想想自己需要什麼。你可以參考我給的源碼,或者參考 Android 內置的 Lint 源碼,看看它們能做什麼。
這一小節很重要,但是我並不會給你講如何去實現某某功能,自己看源碼學習,因為真的不難哇。
最後
如果你很懶,很煩每次都敲一遍 ./gradlew lint 去查看 Lint 輸出,那麼可以把執行 Lint 任務掛載在每次安裝 Debug 包之前,即:
<code>/**
* 在執行 assembleDebug Task 之前掛載 lintDebug
*/
project.afterEvaluate {
def assembleDebugTask = project.tasks.find { it.name == 'assembleDebug' }
def lintTask = project.tasks.find { it.name == 'lintDebug' }
assembleDebugTask.dependsOn(lintTask)
}/<code>
文章不易,如果大家喜歡這篇文章,或者對你有幫助希望大家多多,點贊,轉發,關注 哦。文章會持續更新的。絕對乾貨!!!
閱讀更多 Android進階小劉 的文章