Lint是Google提供的Android靜態代碼檢查工具,能夠掃描並發現代碼中潛在的問題,提醒開發人員及早修正,提升代碼質量。除了Android原生提供的幾百個Lint規則,還能夠開發自定義Lint規則以知足實際須要。html
在美團外賣Android App的迭代過程當中,線上問題頻繁發生。開發時很容易寫出一些問題代碼,例如Serializable的使用:實現了Serializable接口的類,若是其成員變量引用的對象沒有實現Serializable接口,序列化時就會Crash。咱們對一些常見問題的緣由和解決方法作分析總結,並在開發人員組內或跟測試人員一塊兒分享交流,幫助相關人員主動避免這些問題。java
爲了進一步減小問題發生,咱們逐步完善了一些規範,包括制定代碼規範,增強代碼Review,完善測試流程等。但這些措施仍然存在各類不足,包括代碼規範難以實施,溝通成本高,特別是開發人員變更頻繁致使反覆溝通等,所以其效果有限,類似問題仍然不時發生。另外一方面,愈來愈多的總結、規範文檔,對於組內新人也產生了不小的學習壓力。node
有沒有辦法從技術角度減小或減輕上述問題呢?android
咱們調研發現,靜態代碼檢查是一個很好的思路。靜態代碼檢查框架有不少種,例如FindBugs、PMD、Coverity,主要用於檢查Java源文件或class文件;再例如Checkstyle,主要關注代碼風格;但咱們最終選擇從Lint框架入手,由於它有諸多優點:git
在對Lint進行了充分的技術調研後,咱們根據實際遇到的問題,又作了一些更深刻的思考,包括應該用Lint解決哪些問題,怎麼樣更好的推廣實施等,逐步造成了一套較爲全面有效的方案。github
爲了方便後文的理解,咱們先簡單看一下Lint提供的主要API。正則表達式
Lint規則經過調用Lint API實現,其中最主要的幾個API以下。json
Issue:表示一個Lint規則。api
Detector:用於檢測並報告代碼中的Issue,每一個Issue都要指定Detector。安全
Scope:聲明Detector要掃描的代碼範圍,例如JAVA_FILE_SCOPE
、CLASS_FILE_SCOPE
、RESOURCE_FILE_SCOPE
、GRADLE_SCOPE
等,一個Issue可包含一到多個Scope。
Scanner:用於掃描並發現代碼中的Issue,每一個Detector能夠實現一到多個Scanner。
IssueRegistry:Lint規則加載的入口,提供要檢查的Issue列表。
舉例來講,原生的ShowToast就是一個Issue,該規則檢查調用Toast.makeText()
方法後是否漏掉了Toast.show()
的調用。其Detector爲ToastDetector,要檢查的Scope爲JAVA_FILE_SCOPE
,ToastDetector實現了JavaPsiScanner,示意代碼以下。
public class ToastDetector extends Detector implements JavaPsiScanner {
public static final Issue ISSUE = Issue.create(
"ShowToast",
"Toast created but not shown",
"...",
Category.CORRECTNESS,
6,
Severity.WARNING,
new Implementation(
ToastDetector.class,
Scope.JAVA_FILE_SCOPE));
// ...
}
複製代碼
IssueRegistry的示意代碼以下。
public class MyIssueRegistry extends IssueRegistry {
@Override
public List<Issue> getIssues() {
return Arrays.asList(
ToastDetector.ISSUE,
LogDetector.ISSUE,
// ...
);
}
}
複製代碼
Lint開發過程當中最主要的工做就是實現Scanner。Lint中包括多種類型的Scanner以下,其中最經常使用的是掃描Java源文件和XML文件的Scanner。
值得注意的是,掃描Java源文件的Scanner前後經歷了三個版本。
最開始使用的是JavaScanner,Lint經過Lombok庫將Java源碼解析成AST(抽象語法樹),而後由JavaScanner掃描。
在Android Studio 2.2和lint-api 25.2.0版本中,Lint工具將Lombok AST替換爲PSI,同時棄用JavaScanner,推薦使用JavaPsiScanner。
PSI是JetBrains在IDEA中解析Java源碼生成語法樹後提供的API。相比以前的Lombok AST,PSI能夠支持Java 1.八、類型解析等。使用JavaPsiScanner實現的自定義Lint規則,能夠被加載到Android Studio 2.2+版本中,在編寫Android代碼時實時執行。
在Android Studio 3.0和lint-api 25.4.0版本中,Lint工具將PSI替換爲UAST,同時推薦使用新的UastScanner。
UAST是JetBrains在IDEA新版本中用於替換PSI的API。UAST更加語言無關,除了支持Java,還能夠支持Kotlin。
本文目前仍然基於PsiJavaScanner作介紹。根據UastScanner源碼中的註釋,能夠很容易的從PsiJavaScanner遷移到UastScanner。
咱們須要用Lint檢查代碼中的哪些問題呢?
開發過程當中,咱們比較關注App的Crash、Bug率等指標。經過長期的整理總結髮現,有很多發生頻率很高的代碼問題,其原理和解決方案都很明確,可是在寫代碼時卻很容易遺漏且難以發現;而Lint剛好很容易檢查出這些問題。
Crash率是App最重要的指標之一,避免Crash也一直是開發過程當中比較頭疼的一個問題,Lint能夠很好的檢查出一些潛在的Crash。例如:
原生的NewApi,用於檢查代碼中是否調用了Android高版本才提供的API。在低版本設備中調用高版本API會致使Crash。
自定義的SerializableCheck。實現了Serializable接口的類,若是其成員變量引用的對象沒有實現Serializable接口,序列化時就會Crash。咱們制定了一條代碼規範,要求實現了Serializable接口的類,其成員變量(包括從父類繼承的)所聲明的類型都要實現Serializable接口。
自定義的ParseColorCheck。調用Color.parseColor()
方法解析後臺下發的顏色時,顏色字符串格式不正確會致使IllegalArgumentException,咱們要求調用這個方法時必須處理該異常。
有些Bug能夠經過Lint檢查來預防。例如:
SpUsage:要求全部SharedPrefrence讀寫操做使用基礎工具類,工具類中會作各類異常處理;同時定義SPConstants常量類,全部SP的Key都要在這個類定義,避免在代碼中分散定義的Key之間衝突。
ImageViewUsage:檢查ImageView有沒有設置ScaleType,加載時有沒有設置Placeholder。
TodoCheck:檢查代碼中是否還有TODO沒完成。例如開發時可能會在代碼中寫一些假數據,但最終上線時要確保刪除這些代碼。這種檢查項比較特殊,一般在開發完成後提測階段才檢查。
一些性能、安全相關問題可使用Lint分析。例如:
ThreadConstruction:禁止直接使用new Thread()
建立線程(線程池除外),而須要使用統一的工具類在公用線程池執行後臺操做。
LogUsage:禁止直接使用android.util.Log
,必須使用統一工具類。工具類中能夠控制Release包不輸出Log,提升性能,也避免發生安全問題。
除了代碼風格方面的約束,代碼規範更多的是用於減小或防止發生Bug、Crash、性能、安全等問題。不少問題在技術上難以直接檢查,咱們經過封裝統一的基礎庫、制定代碼規範的方式間接解決,而Lint檢查則用於減小組內溝通成本、新人學習成本,並確保代碼規範的落實。例如:
前面提到的SpUsage、ThreadConstruction、LogUsage等。
ResourceNaming:資源文件命名規範,防止不一樣模塊之間的資源文件名衝突。
當檢查出代碼問題時,如何提醒開發者及時修正呢?
早期咱們將靜態代碼檢查配置在Jenkins上,打包發佈AAR/APK時,檢查代碼中的問題並生成報告。後來發現雖然靜態代碼檢查能找出來很多問題,可是不多有人主動去看報告,特別是報告中還有過多可有可無的、優先級很低的問題(例如過於嚴格的代碼風格約束)。
所以,一方面要肯定檢查哪些問題,另外一方面,什麼時候、經過什麼樣的技術手段來執行代碼檢查也很重要。咱們結合技術實現,對此作了更多思考,肯定了靜態代碼檢查實施過程當中的主要目標:
重點關注高優先級問題,屏蔽低優先級問題。正如前面所說,若是代碼檢查報告中夾雜了大量可有可無的問題,反而影響了關鍵問題的發現。
高優問題的解決,要有必定的強制性。當檢查發現高優先級的代碼問題時,給開發者明確直接的報錯,並經過技術手段約束,強制要求開發者修復。
某些問題儘量作到在第一時間發現,從而減小風險或損失。有些問題發現的越早越好,例如業務功能開發中使用了Android高版本API,經過Lint原生的NewApi能夠檢查出來。若是在開發期間發現,當時就能夠考慮其餘技術方案,實現困難時能夠及時和產品、設計人員溝通;而若是到提代碼、提測,甚至發版、上線時才發現,可能爲時已晚。
每一個Lint規則均可以配置Sevirity(優先級),包括Fatal、Error、Warning、Information等,咱們主要使用Error和Warning,以下。
Lint檢查能夠在多個階段執行,包括在本地手動檢查、編碼實時檢查、編譯時檢查、commit檢查,以及在CI系統中提Pull Request時檢查、打包發版時檢查等,下面分別介紹。
在Android Studio中,自定義Lint能夠經過Inspections功能(Analyze - Inspect Code
)手動運行。
在Gradle命令行環境下,可直接用./gradlew lint
執行Lint檢查。
手動執行簡單易用,但缺少強制性,容易被開發者遺漏。
編碼時檢查即在Android Studio中寫代碼時在代碼窗口實時報錯。其好處很明顯,開發者能夠第一時間發現代碼問題。但受限於Android Studio對自定義Lint的支持不完善,開發人員IDE的配置不一樣,須要開發者主動關注報錯並修復,這種方式不能徹底保證效果。
IDEA提供了Inspections功能和相應的API來實現代碼檢查,Android原生Lint就是經過Inspections集成到了Android Studio中。對於自定義Lint規則,官方彷佛沒有給出明確說明,但實際研究發現,在Android Studio 2.2+版本和基於JavaPsiScanner開發的條件下(或Android Studio 3.0+和JavaPsiScanner/UastScanner),IDE會嘗試加載並實時執行自定義Lint規則。
技術細節:
在Android Studio 2.x版本中,菜單Preferences - Editor - Inspections - Android - Lint - Correctness - Error from Custom Lint Check(avaliable for Analyze|Inspect Code)
中指出,自定義Lint只支持命令行或手動運行,不支持實時檢查。
Error from Custom Rule When custom (third-party) lint rules are integrated in the IDE, they are not available as native IDE inspections, so the explanation text (which must be statically registered by a plugin) is not available. As a workaround, run the lint target in Gradle instead; the HTML report will include full explanations.
在Android Studio 3.x版本中,打開Android工程源碼後,IDE會加載工程中的自定義Lint規則,在設置菜單的Inspections列表裏能夠查看,和原生Lint效果相同(Android Studio會在打開源文件時觸發對該文件的代碼檢查)。
分析自定義Lint的IssueRegistry.getIssues()
方法調用堆棧,能夠看到Android Studio環境下,是由org.jetbrains.android.inspections.lint.AndroidLintExternalAnnotator
調用LintDriver
加載執行自定義Lint規則。
參考代碼: https://github.com/JetBrains/android/tree/master/android/src/org/jetbrains/android/inspections/lint
在Android Studio中的實際效果如圖:
配置Gradle腳本可實現編譯Android工程時執行Lint檢查。好處是既能夠儘早發現問題,又能夠有強制性;缺點是對編譯速度有必定的影響。
編譯Android工程執行的是assemble任務,讓assemble依賴lint任務,便可在編譯時執行Lint檢查;同時配置LintOptions,發現Error級別問題時中斷編譯。
在Android Application工程(APK)中配置以下,Android Library工程(AAR)把applicationVariants
換成libraryVariants
便可。
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
def lintTask = tasks["lint${variant.name.capitalize()}"]
output.assemble.dependsOn lintTask
}
}
複製代碼
LintOptions的配置:
android.lintOptions {
abortOnError true
}
複製代碼
利用git pre-commit hook,能夠在本地commit代碼前執行Lint檢查,檢查不經過則沒法提交代碼。這種方式的優點在於不影響開發時的編譯速度,但發現問題相對滯後。
技術實現方面,能夠編寫Gradle腳本,在每次同步工程時自動將hook腳本從工程拷貝到.git/hooks/
文件夾下。
做爲代碼提交流程規範的一部分,發Pull Request提代碼時用CI系統檢查Lint問題是一個常見、可行、有效的思路。可配置CI檢查經過後代碼才能被合併。
CI系統經常使用Jenkins,若是使用Stash作代碼管理,能夠在Stash上配置Pull Request Notifier for Stash插件,或在Jenkins上配置Stash Pull Request Builder插件,實現發Pull Request時觸發Jenkins執行Lint檢查的Job。
在本地編譯和CI系統中作代碼檢查,均可以經過執行Gradle的Lint任務實現。能夠在CI環境下給Gradle傳遞一個StartParameter,Gradle腳本中若是讀取到這個參數,則配置LintOptions檢查全部Lint問題;不然在本地編譯環境下只檢查部分高優先級Lint問題,減小對本地編譯速度的影響。
Lint生成報告的效果如圖所示:
即便每次提代碼時用CI系統執行Lint檢查,仍然不能保證全部人的代碼合併後必定沒有問題;另外對於一些特殊的Lint規則,例如前面提到的TodoCheck,還但願在更晚的時候檢查。
因而在CI系統打包發佈APK/AAR用於測試或發版時,還須要對全部代碼再作一次Lint檢查。
綜合考慮多種檢查方式的優缺點以及咱們的目標,最終肯定結合如下幾種方式作代碼檢查:
爲了方便代碼管理,咱們給自定義Lint建立了一個獨立的工程,該工程打包生成一個AAR發佈到Maven倉庫,而被檢查的Android工程依賴這個AAR(具體開發過程能夠參考文章末尾連接)。
自定義Lint雖然在獨立工程中,但和被檢查的Android工程中的代碼規範、基礎組件等存在較多耦合。
例如咱們使用正則表達式檢查Android工程的資源文件命名規範,每次業務邏輯變更要新增資源文件前綴時,都要修改Lint工程,發佈新的AAR,再更新到Android工程中,很是繁瑣。另外一方面,咱們的Lint工程除了在外賣C端Android工程中使用,也但願能直接用在其餘端的其餘Android工程中,而不一樣工程之間存在差別。
因而咱們嘗試使用配置文件來解決這一問題。以檢查Log使用的LogUsage爲例,不一樣工程封裝了不一樣的Log工具類,報錯時提示信息也應該不同。定義配置文件名爲custom-lint-config.json
,放在被檢查Android工程的模塊目錄下。在Android工程A中的配置文件是:
{
"log-usage-message": "請勿使用android.util.Log,建議使用LogUtils工具類"
}
複製代碼
而Android工程B的配置文件是:
{
"log-usage-message": "請勿使用android.util.Log,建議使用Logger工具類"
}
複製代碼
從Lint的Context對象可獲取被檢查工程目錄從而讀取配置文件,關鍵代碼以下:
import com.android.tools.lint.detector.api.Context;
public final class LintConfig {
private LintConfig(Context context) {
File projectDir = context.getProject().getDir();
File configFile = new File(projectDir, "custom-lint-config.json");
if (configFile.exists() && configFile.isFile()) {
// 讀取配置文件...
}
}
}
複製代碼
配置文件的讀取,能夠在Detector的beforeCheckProject、beforeCheckLibraryProject回調方法中進行。LogUsage中檢查到錯誤時,根據配置文件定義的信息報錯。
public class LogUsageDetector extends Detector implements Detector.JavaPsiScanner {
// ...
private LintConfig mLintConfig;
@Override
public void beforeCheckProject(@NonNull Context context) {
// 讀取配置
mLintConfig = new LintConfig(context);
}
@Override
public void beforeCheckLibraryProject(@NonNull Context context) {
// 讀取配置
mLintConfig = new LintConfig(context);
}
@Override
public List<String> getApplicableMethodNames() {
return Arrays.asList("v", "d", "i", "w", "e", "wtf");
}
@Override
public void visitMethod(JavaContext context, JavaElementVisitor visitor, PsiMethodCallExpression call, PsiMethod method) {
if (context.getEvaluator().isMemberInClass(method, "android.util.Log")) {
// 從配置文件獲取Message
String msg = mLintConfig.getConfig("log-usage-message");
context.report(ISSUE, call, context.getLocation(call.getMethodExpression()), msg);
}
}
}
複製代碼
Lint規則開發過程當中,咱們發現了一系列類似的需求:封裝了基礎工具類,但願你們都用起來;某個方法很容易拋出RuntimeException,有必要作處理,但Java語法上RuntimeException並不強制要求處理從而常常遺漏……
這些類似的需求,每次在Lint工程中開發一樣會很繁瑣。咱們嘗試實現了幾個模板,能夠直接在Android工程中經過配置文件配置Lint規則。
以下爲一個配置文件示例:
{
"lint-rules": {
"deprecated-api": [{
"method-regex": "android\\.content\\.Intent\\.get(IntExtra|StringExtra|BooleanExtra|LongExtra|LongArrayExtra|StringArrayListExtra|SerializableExtra|ParcelableArrayListExtra).*",
"message": "避免直接調用Intent.getXx()方法,特殊機型可能發生Crash,建議使用IntentUtils",
"severity": "error"
},
{
"field": "java.lang.System.out",
"message": "請勿直接使用System.out,應該使用LogUtils",
"severity": "error"
},
{
"construction": "java.lang.Thread",
"message": "避免單首創建Thread執行後臺任務,存在性能問題,建議使用AsyncTask",
"severity": "warning"
},
{
"super-class": "android.widget.BaseAdapter",
"message": "避免直接使用BaseAdapter,應該使用統一封裝的BaseListAdapter",
"severity": "warning"
}],
"handle-exception": [{
"method": "android.graphics.Color.parseColor",
"exception": "java.lang.IllegalArgumentException",
"message": "Color.parseColor須要加try-catch處理IllegalArgumentException異常",
"severity": "error"
}]
}
}
複製代碼
示例配置中定義了兩種類型的模板規則:
問題API的匹配,包括方法調用(method)、成員變量引用(field)、構造函數(construction)、繼承(super-class)等類型;匹配字符串支持glob語法或正則表達式(和lint.xml中ignore的配置語法一致)。
實現方面,主要是遍歷Java語法樹中特定類型的節點並轉換成完整字符串(例如方法調用android.content.Intent.getIntExtra
),而後檢查是否有模板規則與其匹配。匹配成功後,DeprecatedApi規則直接輸出message報錯;HandleException規則會檢查匹配到的節點是否處理了特定Exception(或Exception的父類),沒有處理則報錯。
隨着Lint新規則的不斷開發,咱們又遇到了一個問題。Android工程中存在大量歷史代碼,不符合新增Lint規則的要求,但也沒有致使明顯問題,這時接入新增Lint規則要求修改全部歷史代碼,成本較高並且有必定風險。例如新增代碼規範,要求使用統一的線程工具類而不容許直接用Handler以免內存泄露等。
咱們嘗試了一個折中的方案:只檢查指定git commit以後新增的文件。在配置文件中添加配置項,給Lint規則配置git-base
屬性,其值爲commit ID,只檢查這次commit以後新增的文件。
實現方面,執行git rev-parse --show-toplevel
命令獲取git工程根目錄的路徑;執行git ls-tree --full-tree --full-name --name-only -r <commit-id>
命令獲取指定commit時已有文件列表(相對git根目錄的路徑)。在Scanner回調方法中經過Context.getLocation(node).getFile()
獲取節點所在文件,結合git文件列表判斷是否須要檢查這個節點。須要注意的是,代碼量較大時要考慮Lint檢查對電腦的性能消耗。
通過一段時間的實踐發現,Lint靜態代碼檢查在解決特定問題時的效果很是好,例如發現一些語言或API層面比較明確的低級錯誤、幫助進行代碼規範的約束。使用Lint前,很多這類問題剛好對開發人員來講又很容易遺漏(例如原生的NewApi檢查、自定義的SerializableCheck);相同問題反覆出現;代碼規範的執行,特別是有新人蔘與開發時,須要很高的學習和溝通成本,還常常出現新人提交代碼時因爲沒有遵照代碼規範反覆被要求修改。而使用Lint後,這些問題都能在第一時間獲得解決,節省了大量的人力,提升了代碼質量和開發效率,也提升了App的使用體驗。
參考資料:
Lint和Gradle相關技術細節還能夠閱讀個人我的博客:
子健,Android高級工程師,2015年畢業於西安電子科技大學並校招加入美團外賣。前期前後負責過外賣App首頁、商家容器、評價等核心業務模塊的開發維護,目前重點負責參與外賣打包自動化、代碼檢查、平臺化等技術工做。
對咱們團隊感興趣,能夠關注咱們的專欄。美團外賣App團隊誠招Android/iOS高級工程師/技術專家,工做地北京/上海可選,歡迎有興趣的同窗投遞簡歷到wukai05#meituan.com。