Android Studio 工具:Lint 程式碼掃描工具(含自定義lint)
什麼是 Lint
Android Lint 是 SDK Tools 16(ADT 16)開始引入的一個程式碼掃描工具,通過對程式碼進行靜態分析,可以幫助開發者發現程式碼質量問題和提出一些改進建議。除了檢查 Android 專案原始碼中潛在的錯誤,對於程式碼的正確性、安全性、效能、易用性、便利性和國際化方面也會作出檢查。
Android Lint 作為專案的程式碼檢測工具,是因為它具有以下幾個特性:
- 已經被整合到 Android Studio,使用方便。
- 能在編寫程式碼時實時反饋出潛在的問題。
- 可以自定義規則。Android Lint 本身包含大量已經封裝好的介面,能提供豐富的程式碼資訊,開發者可以基於這些資訊進行自定義規則的編寫。
先來一張神圖lint後具體資訊.png
一、開始使用
Android Lint 的工作過程比較簡單,一個基礎的 Lint 過程由 Lint Tool(檢測工具),Source Files(專案原始檔) 和 lint.xml(配置檔案) 三個部分組成,Lint Tool 讀取 Source Files,根據 lint.xml 配置的規則(issue)輸出結果(如下圖)。
123.png
1.1Android studio使用
Android Studio 中,Android Lint 已經被整合,只需要點選選單 —— Analyze —— Inspect Code 即可執行 Android Lint,在彈出的對話方塊中可以設定執行 Lint 的範圍,可以選擇整個專案,也可以只選擇當前的子模組或者其他自定義的範圍:
123.png
檢查完畢後會彈出 Inspection 的控制檯,並在其中列出詳細的檢查結果:
123.png
如上圖所展示的,Android Lint 對檢查的結果進行了分類,同一個規則(issue)下的問題會聚合,其中針對 Android 的規則類別會在分類前說明是 Android 相關的,主要是六類:
- Accessibility 無障礙,例如 ImageView 缺少contentDescription 描述,String 編碼字串等問題。
- Correctness 正確性
- Internationalization 國際化,如字元缺少翻譯等問題。
- Performance 效能
- Security 安全性,例如沒有使用 HTTPS 連線 Gradle,AndroidManifest 中的許可權問題等。
- Usability 易用性,例如缺少某些倍數的切圖,重複圖示等。
其他的結果條目則是針對 Java 語法的問題,另外每一個問題都有區分嚴重程度(severity),從高到底依次是:
Fatal
Error
Warning
Information
Ignore
其中 Fatal 和 Error 都是指錯誤,但是 Fatal 型別的錯誤會直接中斷 ADT 匯出 APK,更為嚴重。
在結果列表中點選一個條目,可以看到詳細的原始檔名和位置,以及命中的錯誤規則(issue)、解決方案或者遮蔽提示
除了直接在選單中執行 Lint 外,大部分問題程式碼在編寫時 Android Studio 就會給出提醒:
1.2配置
對於執行 Lint 操作的相關配置,是定義在 gradle 檔案的 lintOptions 中,可定義的選項及其預設值
android {
lintOptions {
// 設定為 true,則當 Lint 發現錯誤時停止 Gradle 構建
abortOnError false
// 設定為 true,則當有錯誤時會顯示檔案的全路徑或絕對路徑 (預設情況下為true)
absolutePaths true
// 僅檢查指定的問題(根據 id 指定)
check 'NewApi', 'InlinedApi'
// 設定為 true 則檢查所有的問題,包括預設不檢查問題
checkAllWarnings true
// 設定為 true 後,release 構建都會以 Fatal 的設定來執行 Lint。
// 如果構建時發現了致命(Fatal)的問題,會中止構建(具體由 abortOnError 控制)
checkReleaseBuilds true
// 不檢查指定的問題(根據問題 id 指定)
disable 'TypographyFractions','TypographyQuotes'
// 檢查指定的問題(根據 id 指定)
enable 'RtlHardcoded','RtlCompat', 'RtlEnabled'
// 在報告中是否返回對應的 Lint 說明
explainIssues true
// 寫入報告的路徑,預設為構建目錄下的 lint-results.html
htmlOutput file("lint-report.html")
// 設定為 true 則會生成一個 HTML 格式的報告
htmlReport true
// 設定為 true 則只報告錯誤
ignoreWarnings true
// 重新指定 Lint 規則配置檔案
lintConfig file("default-lint.xml")
// 設定為 true 則錯誤報告中不包括原始碼的行號
noLines true
// 設定為 true 時 Lint 將不報告分析的進度
quiet true
// 覆蓋 Lint 規則的嚴重程度,例如:
severityOverrides ["MissingTranslation": LintOptions.SEVERITY_WARNING]
// 設定為 true 則顯示一個問題所在的所有地方,而不會截短列表
showAll true
// 配置寫入輸出結果的位置,格式可以是檔案或 stdout
textOutput 'stdout'
// 設定為 true,則生成純文字報告(預設為 false)
textReport false
// 設定為 true,則會把所有警告視為錯誤處理
warningsAsErrors true
// 寫入檢查報告的檔案(不指定預設為 lint-results.xml)
xmlOutput file("lint-report.xml")
// 設定為 true 則會生成一個 XML 報告
xmlReport false
// 將指定問題(根據 id 指定)的嚴重級別(severity)設定為 Fatal
fatal 'NewApi', 'InlineApi'
// 將指定問題(根據 id 指定)的嚴重級別(severity)設定為 Error
error 'Wakelock', 'TextViewEdits'
// 將指定問題(根據 id 指定)的嚴重級別(severity)設定為 Warning
warning 'ResourceAsColor'
// 將指定問題(根據 id 指定)的嚴重級別(severity)設定為 ignore
ignore 'TypographyQuotes'
}
}
lint.xml 這個檔案則是配置 Lint 需要禁用哪些規則(issue),以及自定義規則的嚴重程度(severity),lint.xml 檔案是通過 issue 標籤指定對一個規則的控制,在專案根目錄中建立一個 lint.xml 檔案後 Android Lint 會自動識別該檔案,在執行檢查時按照 lint.xml 的內容進行檢查。如上面提到的那樣,開發者也可以通過 lintOptions 中的 lintConfig 選項來指定配置檔案。一個 lint.xml 示例如下:
123.png
issue 標籤中使用 id 指定一個規則,severity="ignore" 則表明禁用這個規則。需要注意的是,某些規則可以通過 ignore 標籤指定僅對某些屬性禁用,例如上面的 Deprecated,表示檢查是否有使用不推薦的屬性和方法,而在 issue 標籤中包裹一個 ignore 標籤,在 ignore 標籤的 regexp 屬性中使用正則表示式指定了 singleLine,則表明對 singleLine 這個屬性遮蔽檢查。
另外開發者也可以使用 @SuppressLint(issue id) 標註針對某些程式碼忽略某些 Lint 檢查,這個標註既可以加到成員變數之前,也可以加到方法宣告和類宣告之前,分別針對不同範圍進行遮蔽。
二、展開敘述
2.2.1Correctness (不是全部,常見的)
-
Appcompat Custom Widgets
Appcompat自定義小部件一般會讓你繼承自 android.support.v7.widget.AppCompat...
不要直接擴充套件android.widget類,而應該擴充套件android.support.v7.widget.AppCompat中的一個委託類。 -
Attribute unused on older versions
舊版本未使用的屬性 針對具有minSdkVersion
這不是一個錯誤; 應用程式將簡單地忽略該屬性。
可以選擇在layout-vNN資料夾中建立一個佈局的副本 將在API NN或更高版本上使用,您可以利用更新的屬性。 -
Class is not registered in the manifest
類未在清單中註冊
Activities, services and content providers should be registered in the AndroidManifest.xml file -
Combining Ellipsize and Maxlines
Ellipsize和Maxlines相結合
結合ellipsize和maxLines = 1可能導致某些裝置崩潰。 早期版本的lint建議用maxLines = 1替換singleLine = true,但在使用ellipsize時不應該這樣做 -
Extraneous text in resource files
資原始檔中的無關文字 -
Hardcoded reference to /sdcard
硬編碼參考/ SD卡
程式碼不應該直接引用/ sdcard路徑; 而是使用:Environment.getExternalStorageDirectory().getPath()
不要直接引用/ data / data /路徑; 它可以在多使用者場景中有所不同。
-
Implied default locale in case conversion
在轉換的情況下預設的預設語言環境 -
Implied locale in date format
隱含的日期格式的區域設定
呼叫者都應該使用getDateInstance(),getDateTimeInstance()或getTimeInstance()來獲得適合使用者語言環境的SimpleDateFormat的現成例項。 -
Likely cut & paste mistakes
可能剪下和貼上錯誤
剪下和貼上呼叫findViewById但忘記更新R.id欄位的情況。 有可能你的程式碼只是(冗餘)重複查詢欄位 -
Mismatched Styleable/Custom View Name
不匹配的樣式/自定義檢視名稱
自定義檢視的慣例是使用名稱與自定義檢視類名稱相匹配的宣告樣式。 -
Missing Permissions
缺少許可權 -
Nested scrolling widgets
巢狀的滾動小部件
A scrolling widget such as a ScrollView should not contain any nested scrolling widgets since this has various usability issues -
Obsolete Gradle Dependency 已過時的Gradle依賴關係
-
Target SDK attribute is not targeting latest version 目標SDK屬性未定位到最新版本
-
Using 'px' dimension 使用“px”維度
-
Using android.media.ExifInterface 使用android.media.ExifInterface
舊版的有一些漏洞,使用支援庫中的 -
Using dp instead of sp for text sizes 使用dp代替文字大小的sp
-
Using Private APIs 使用私有API
-
Using private resources 使用私人資源
2.2.2Internationalization
-
Hardcoded text
硬編碼文字
直接在佈局檔案中對文字屬性進行硬編碼是有缺陷的
should use @string resource -
Overlapping items in RelativeLayout
在RelativeLayout中重疊專案
如果相對佈局的文字或按鈕項左右對齊,則由於本地化的文字擴充套件,它們可以相互重疊,除非它們具有toEndOf / toStartOf之類的相互約束。 -
Padding and margin symmetry
填充和邊緣對稱
如果您在佈局的左側指定填充或邊距,則應該也可以在右側指定填充(反之亦然),以便從右到左佈局對稱。
-
TextView Internationalization
TextView國際化
永遠不要呼叫Number#toString()來格式化數字; 它不會正確處理分數分隔符和區域特定的數字
使用具有適當格式規範(%d或%f)的String#格式
不要傳遞字串(例如“Hello”)來顯示文字。 硬編碼文字無法正確翻譯成其他語言,考慮使用Android資源字串
不要通過連線文字塊來構建訊息。 這樣的訊息不能被正確翻譯。 -
Using left/right instead of start/end attributes
使用左/右而不是開始/結束屬性
2.2.3Performance
- Handler reference leaks
handler導致的洩漏
由於該Handler被宣告為內部類,所以可以防止外部類被垃圾收集。 如果處理程式對主執行緒以外的執行緒使用Looper或MessageQueue,則不存在問題。 如果處理程式正在使用主執行緒的Looper或MessageQueue,則需要修復Handler宣告,
解決:將Handler宣告為靜態類; 在外部類中,例項化WeakReference到外部類,並在例項化Handler時將此物件傳遞給Handler; 使用WeakReference物件來引用外部類的所有成員。
-
HashMap can be replaced with SparseArray
HashMap可以用SparseArray替換
對於鍵型別為integer的對映,使用Android SparseArray API通常效率更高。 -
Inefficient layout weight
低效的佈局權重
當LinearLayout中只有一個控制元件定義了一個權重時,為它指定一個0dp的寬度/高度會更有效率,因為它將吸收所有的剩餘空間。 如果宣告的寬度/高度為0dp,則不必首先測量其自己的大小。 -
Layout has too many views
佈局有太多的意見
在單個佈局中使用太多的檢視對效能不利。 考慮使用複合繪圖或其他技巧來減少此佈局中的檢視數量。 最大檢視數量預設為80,但可以使用環境變數ANDROID_LINT_MAX_VIEW_COUNT進行配置。 -
Layout hierarchy is too deep
佈局層次太深
巢狀太多的佈局對效能不利。 考慮使用更平坦的佈局(比如RelativeLayout或GridLayout)。預設的最大深度是10,但可以使用環境變數ANDROID_LINT_MAX_DEPTH進行配置。 -
Memory allocations within drawing code
記憶體分配在繪圖程式碼
應該避免在繪圖或佈局操作中分配物件。 這些被頻繁地呼叫,所以平滑的UI可以被物件分配造成的垃圾收集暫停中斷。 通常處理的方式是預先分配所需的物件,併為每個繪圖操作重新使用它們。 有些方法代表您分配記憶體(如Bitmap.create),並且應該以相同的方式處理這些記憶體。 -
Missing @Keep for Animated Properties
屬性動畫缺少@Keep
當你使用屬性動畫師時,屬性可以通過反射來訪問。 這些方法應該使用@Keep註釋,以確保在釋出構建期間,這些方法不會被視為未被使用和刪除,或者被視為內部的,並被重新命名為更短。 這個檢查還會標記出其他可能遇到的反射問題,比如缺少屬性,錯誤的引數型別等等。 -
Missing baselineAligned attribute
缺少baselineAligned屬性
當使用LinearLayout在巢狀佈局之間按比例分配空間時,應關閉基線對齊屬性以使佈局計算速度更快。 -
Node can be replaced by a TextView with compound drawables
節點可以用複合可繪製的TextView替換
包含ImageView和TextView的LinearLayout可以更有效地處理為複合可繪製(單個TextView,使用drawableTop,drawableLeft,drawableRight和/或drawableBottom屬性在文字旁邊繪製一個或多個影象)。 如果這兩個小部件彼此之間有空白,則可以用drawablePadding屬性替換。 -
Obsolete layout params
過時的佈局引數
-
Obsolete SDK_INT Version Check
已過時的SDK_INT版本檢查
此檢查標誌版本檢查不是必需的,因為minSdkVersion(或周圍已知的API級別)已經至少與檢查的版本一樣高。
它還會在-vNN資料夾中查詢資源,如版本限定符小於或等於minSdkVersion的values-v14,其中內容應合併到最佳資料夾中。 -
Static Field Leaks
靜態常量--持有fragment及activity的引用
非靜態內部類對其外部類具有隱式引用。
如果該外部類是例如fragment或activity,如果長時間執行的處理程式/載入程式/任務將持有對該activity的引用,長時間沒有被回收掉。 -
Useless parent layout
無用的父母佈局
具有沒有兄弟的孩子的佈局不是滾動檢視或根佈局,並且沒有背景,可以被移除並且其子節點直接移動到父節點以獲得更平坦和更高效的佈局分層結構。 -
View Holder Candidates
檢視持有人候選人
Should use View Holder pattern
2.2.4Security
-
Cipher.getInstance with ECB
Cipher.getInstance與ECB
不應使用ECB作為cipher mode或不設定cipher mode來呼叫Cipher#getInstance,因為android上的預設模式是ECB,這是不安全的。(加解密) -
Content provider does not require permission
內容提供者不需要許可權
內容提供程式預設匯出,系統上的任何應用程式都可能使用它們來讀取和寫入資料。 如果內容提供者提供對敏感資料的訪問,則應該通過在清單中指定export = false來保護它,或者通過可以授予其他應用程式的許可權來保護它。 -
Exported service does not require permission
匯出的服務不需要許可權
匯出的服務(設定了exported = true或者包含intent-filter並且不指定exported = false的服務)應該定義一個實體為了啟動服務或繫結到服務而必須擁有的許可權。 沒有這個,任何應用程式都可以使用此服務。 -
Hardware Id Usage
硬體ID使用情況
不建議使用這些裝置識別符號,除了高價值欺詐預防和高階電話使用情況。
getLine1Number獲取手機號,getDeviceId裝置IMEI,getMacAddressMAC地址
-
Incorrect constant
不正確的常量 -
Insecure TLS/SSL trust manager
不安全的TLS / SSL信任管理器 -
Missing @JavascriptInterface on methods
缺少@JavascriptInterface方法 -
openFileOutput() or similar call passing MODE_WORLD_READABLE
openFileOutput()或類似的呼叫傳遞MODE_WORLD_READABLE -
openFileOutput() or similar call passing MODE_WORLD_WRITEABLE
openFileOutput()或類似的呼叫傳遞MODE_WORLD_WRITEABLE
在某些情況下,應用程式可以編寫世界可寫檔案,但應仔細檢查這些檔案以確保它們不包含私人資料,並且如果檔案被惡意應用程式修改,則不會欺騙或破壞應用程式。 -
Receiver does not require permission
接收者不需要許可 -
Using setJavaScriptEnabled 使用setJavaScriptEnabled
如果您不確定您的應用程式確實需要JavaScript支援,那麼您的程式碼不應該呼叫setJavaScriptEnabled。
2.2.6Usability
- Button should be borderless
按鈕應該是無邊界的
兩個 Buttons 放在一個佈局裡會被判斷為按鈕欄,需要新增樣式取消它的邊框
在 Buttons 上新增屬性 style="?android:attr/buttonBarButtonStyle" 。系統提示也可以在按鈕的父佈局上新增 style="? android:attr/buttonBarStyle" 屬性
-
Ellipsis string can be replaced with ellipsis character
省略號字串可以用省略號字元替換
Replace "..." with ellipsis character (…, …) ? -
Hyphen can be replaced with dash
連字元可以用短劃線代替
Replace "-" with an "en dash" character (–, –) ? -
Missing View constructors for XML inflation
缺少XML通貨膨脹的檢視建構函式 -
Text size is too small
文字太小
避免使用小於12sp的尺寸。小於12sp的字型會太小導致使用者看不清
2.2其他型別
Class structure 類結構
Code maturity issues 程式碼成熟度問題
Code style issues 程式碼樣式問題
Compiler issues 編譯器問題
Control flow issues 控制流量問題
Data flow issues 資料流問題
Declaration redundancy 宣告冗餘
Error handling 錯誤處理
General 一般
Imports 進口
J2ME issues J2ME問題
Java 5 Java 5
Java 7 Java 7
Java language level migration aids Java語言級別的遷移輔助
Javadoc issues Javadoc問題
Naming conventions 命名約定
Numeric issues 數字問題
Performance issues 效能問題
Probable bugs 可能的錯誤
Properties Files 屬性檔案
Spelling 拼字
Style 樣式
Verbose or redundant code constructs 詳細或冗餘的程式碼結構
XML XML
2.2.1Class structure
Field can be local欄位可以是本地的
Parameter can be local引數可以是本地的
'private' method declared 'final'
'static' method declared 'final''
2.2.2Code maturity issues 程式碼成熟度問題
Deprecated API usage不推薦使用API
Deprecated member is still used不推薦使用的成員仍在使用
2.2.3Code style issues 程式碼樣式問題
Unnecessary enum modifier不必要的列舉修飾符
Unnecessary interface modifier不必要的介面修飾符
Unnecessary semicolon不必要的分號
private public
2.2.4Compiler issues 編譯器問題
Unchecked warning未經檢查的警告
2.2.5Control flow issues 控制流問題
Double negation雙重否定
Pointless boolean expression無意義的布林表示式
Redundant 'if' statement冗餘“if”語句
Redundant conditional expression冗餘的條件表示式
Simplifiable boolean expression簡化布林表示式
Simplifiable conditional expression簡化條件表示式
Unnecessary 'return' statement不必要的“return”宣告
return;
2.2.6Data flow issues 資料流問題
Boolean method is always inverted布林方法總是倒置的
Redundant local variable冗餘區域性變數
2.2.7Declaration redundancy 宣告冗餘
Access static member via instance reference通過例項引用訪問靜態成員
this.minsize = this.maxsize;
Actual method parameter is the same constant實際的方法引數是相同的常量
Actual value of parameter ''register'' is always ''true''
Declaration access can be weaker宣告訪問許可權可以再弱
Can be private
Declaration can have final modifier宣言可以有最終的修改
Duplicate throws重複丟擲
Empty method空方法
Method can be void方法可以是無效的
Method returns the same value方法返回相同的值
All implementations of this method always return '3'
Redundant throws clause冗餘丟擲子句
The declared exception 'UnsupportedEncodingException' is never thrown
Unnecessary module dependency不必要的模組依賴
Unused declaration未使用的宣告(方法,變數)
2.2.8Error handling 錯誤處理
Caught exception is immediately rethrown捕獲到的異常立即被重新丟擲
Empty 'catch' block空'catch'塊
'return' inside 'finally' block在'finally'塊中'返回'
'throw' inside 'finally' block在“finally”塊內“丟擲”
2.2.9General
Annotator註解者
Default File Template Usage預設檔案模板的用法
2.2.10Imports 匯入
Unused import沒有用到的匯入
2.2.11J2ME issues J2ME問題
'if'語句可以用&&或||代替 表達
2.2.12Java 5 Java 5
'for' loop replaceable with 'foreach''for'迴圈可替換為'foreach'
'indexOf()' expression is replaceable with 'contains()''indexOf()'表示式可以用'contains()'來替換
'StringBuffer' may be 'StringBuilder''StringBuffer'可能是'StringBuilder'
Unnecessary boxing不必要的裝箱
Unnecessary unboxing不必要的拆箱
'while' loop replaceable with 'foreach''while'迴圈可以替換'foreach'
2.2.13Java 7 Java 7
Explicit type can be replaced with <>顯式型別可以用<>來替換
'試試最後'用資源替換'試用'
2.2.14Java language level migration aids Java語言級別的遷移輔助
'if' replaceable with 'switch'
2.2.15Javadoc issues Javadoc問題
Dangling Javadoc comment 搖搖晃晃的Javadoc評論
Declaration has Javadoc problems 宣言有Javadoc問題
Declaration has problems in Javadoc 宣告在Javadoc引用中有問題
2.2.16Naming conventions 命名約定
2.2.17Numeric issues 數字問題
數字溢位 Numeric overflow
八進位制整數 Octal integer
無意義的算術表示式 Pointless arithmetic expression
2.2.18Performance issues 效能問題
Redundant 'String.toString()' 冗餘'String.toString()'
Redundant 'substring(0)' call 冗餘'substring(0)'呼叫
Redundant call to 'String.format()' 冗餘呼叫'String.format()'
String concatenation as argument to 'StringBuffer.append()' call 字串連線作為“StringBuffer.append()”呼叫的引數
String concatenation in loop 迴圈中的字串連線
'StringBuffer' can be replaced with 'String' 'StringBuffer'可以替換為'String'
2.2.19Probable bugs 可能的錯誤
Collection added to self Collection新增到自我
Constant conditions & exceptions 不變的條件和例外
Mismatched query and update of collection 不匹配的查詢和集合更新
Mismatched query and update of StringBuilder 不匹配的查詢和更新的StringBuilder
@NotNull/@Nullable problems @NotNull / @可空問題
Result of method call ignored 方法呼叫的結果被忽略
Statement with empty body 宣告與空的實現
String comparison using '==', instead of 'equals()' 使用'=='進行字串比較,而不是'equals()'
Suspicious collections method calls 可疑collections方法呼叫
Suspicious variable/parameter name combination 可疑變數/引數名稱組合
Unused assignment 沒用的賦值操作
2.2.20Properties Files 屬性檔案
Unused Property未使用的屬性
2.2.21Spelling 拼字
2.2.22Style 樣式
Unnecessary semicolon沒必要的分號
2.2.23Verbose or redundant code constructs 詳細或冗餘的程式碼結構
Redundant array creation建立冗餘陣列
Redundant type cast冗餘型別轉換
2.2.24XML XML
Deprecated API usage in XML 在XML中不推薦使用API
Unbound XML namespace prefix 未繫結的XML名稱空間字首
Unused XML schema declaration 未使用的XML模式宣告
XML highlighting XML突出顯示
XML tag empty body XML標籤為空的正文
三、自定義lint
3.1建立工程
建立自定義 Lint 需要建立一個 Java 專案,專案中需要引入 Android Lint 的包,專案的 build.gradle 如下:
apply plugin: 'java'
configurations {
lintChecks
}
dependencies {
compile "com.android.tools.lint:lint-api:25.1.2"
compile "com.android.tools.lint:lint-checks:25.1.2"
lintChecks files(jar)
}
jar {
manifest {
attributes('Lint-Registry': 'com.qmuiteam.qmui.lint.QMUIIssueRegistry')
}
}
其中 lint-api 是 Android Lint 的官方介面,基於這些介面可以獲取原始碼資訊,從而進行分析,lint-checks 是官方已有的檢查規則。Lint-Registry 表示給自定義規則註冊,以及打包為 jar.
3.2 Detector
Detector 是自定義規則的核心,它的作用是掃描程式碼,從而獲取程式碼中的各種資訊,然後基於這些資訊進行提醒和報告,在本場景中,我們需要掃描 Java 程式碼,找到 getDrawable 方法的呼叫,然後分析其中傳入的 Drawable 是否為 Vector Drawable,如果是則需要進行報告,完整程式碼如下:
/**
* 檢測是否在 getDrawable 方法中傳入了 Vector Drawable,在 4.0 及以下版本的系統中會導致 Crash
*/
public class QMUIJavaVectorDrawableDetector extends Detector implements Detector.JavaScanner {
public static final Issue ISSUE_JAVA_VECTOR_DRAWABLE =
Issue.create("QMUIGetVectorDrawableWithWrongFunction",
"Should use the corresponding method to get vector drawable.",
"Using the normal method to get the vector drawable will cause a crash on Android versions below 4.0",
Category.ICONS, 2, Severity.ERROR,
new Implementation(QMUIJavaVectorDrawableDetector.class, Scope.JAVA_FILE_SCOPE));
@Override
public List<String> getApplicableMethodNames() {
return Collections.singletonList("getDrawable");
}
@Override
public void visitMethod(@NonNull JavaContext context, AstVisitor visitor, @NonNull MethodInvocation node) {
StrictListAccessor<Expression, MethodInvocation> args = node.astArguments();
if (args.isEmpty()) {
return;
}
Project project = context.getProject();
List<File> resourceFolder = project.getResourceFolders();
if (resourceFolder.isEmpty()) {
return;
}
String resourcePath = resourceFolder.get(0).getAbsolutePath();
for (Expression expression : args) {
String input = expression.toString();
if (input != null && input.contains("R.drawable")) {
// 找出 drawable 相關的引數
// 獲取 drawable 名字
String drawableName = input.replace("R.drawable.", "");
try {
// 若 drawable 為 Vector Drawable,則檔案字尾為 xml,根據 resource 路徑,drawable 名字,檔案字尾拼接出完整路徑
FileInputStream fileInputStream = new FileInputStream(resourcePath + "/drawable/" + drawableName + ".xml");
BufferedReader reader = new BufferedReader(new InputStreamReader(fileInputStream));
String line = reader.readLine();
if (line.contains("vector")) {
// 若檔案存在,並且包含首行包含 vector,則為 Vector Drawable,丟擲警告
context.report(ISSUE_JAVA_VECTOR_DRAWABLE, node, context.getLocation(node), expression.toString() + " 為 Vector Drawable,請使用 getVectorDrawable 方法獲取,避免 4.0 及以下版本的系統產生 Crash");
}
fileInputStream.close();
} catch (Exception ignored) {
}
}
}
}
}
QMUIJavaVectorDrawableDetector 繼承於 Detector,並實現了 Detector.JavaScanner 介面,實現什麼介面取決於自定義 Lint 需要掃描什麼內容,以及希望從掃描的內容中獲取何種資訊。Android Lint 提供了大量不同範圍的 Detector:
- Detector.BinaryResourceScanner 針對二進位制資源,例如 res/raw 等目錄下的各種 Bitmap
- Detector.ClassScanner 相對於 Detector.JavaScanner,更針對於類進行掃描,可以獲取類的各種資訊
- Detector.GradleScanner 針對 Gradle 進行掃描
- Detector.JavaScanner 針對 Java 程式碼進行掃描
- Detector.ResourceFolderScanner 針對資源目錄進行掃描,只會掃描目錄本身
- Detector.XmlScanner 針對 xml 檔案進行掃描
- Detector.OtherFileScanner 用於除上面6種情況外的其他檔案
不同的介面定義了各種方法,實現自定義 Lint 實際上就是實現 Detector 中的各種方法,在上面的例子中,getApplicableMethodNames 的返回值指定了需要被檢查的方法,visitMethod 則可以接收檢查到的方法對應的資訊,這個方法包含三個引數,其作用分別是:
- context 這裡的 context 是一個 JavaContext,主要的功能是獲取主專案的資訊,以及進行報告(包括獲取需要被報告的程式碼的位置等)。
- visitor visitor 是一個 ASTVisitor,即 AST(抽象語法樹)的訪問者類,Android Lint 把掃描到的程式碼抽象成 AST,方便開發者以節點 - 屬性的形式獲取資訊,visitor 則可以方便地獲取當前節點的相關節點。
- node 這是一個 MethodInvocation 例項,MethodInvocation 是 Android Lint 裡的 AST 子類,在上面的例子中,node 表示的是被掃描到的方法,所以我們可以通過節點 - 屬性的形式獲取被掃描的方法的引數等各種資訊。
在例子中我們獲取方法的引數,通過遍歷引數拿到 Drawable 引數,分解出 Drawable 的檔名,然後通過 context 獲取主專案的資源路徑,配合 Drawable 的檔名拼接檔案的實際路徑,確定檔案存在後檢查檔案內容開頭是否包含 “vector” 這個字串,如果是則表示開發者在普通的 getDrawable 方法中傳入了 Vector Drawable,最後呼叫 context 的 report 方法進行報告。
值得注意的是,在例子中我們並沒有直接例項 Drawable,然後通過 Drawable 的方法判斷是否為 Vector Drawable,而是通過較為繁瑣的步驟檢查檔案內容,這是因為 Android Lint 的專案是一個純 Java 專案,不能使用 android.graphics 等包,因而開發時會比較繁瑣。
3.3 Issue
在檢查出問題需要進行報告時,context.report 方法中傳入了一個 ISSUE_JAVA_VECTOR_DRAWABLE,這裡的"issue"是宣告一個規則,因此自定義一個 Lint 規則就需要定義一個 issue。issue 由類方法 Issue.create 建立,引數如下:
- id:標記 issue 的唯一值,語義上要能簡短描述問題,使用 Java 註解和 XML 屬性遮蔽 Lint 時,就需要使用這個 id。
- summary:概況地描述問題,不需要給出解決辦法。
- explanation:詳細地描述問題以及給出解決辦法。
- category:問題類別,在系統給出的分類中選擇,後面會詳述。
- priority:1-10 的數字,表示優先順序,10 為最嚴重。
- severity:嚴重級別,在 Fatal,Error,Warning,Informational,Ignore 中選擇一個。
-
Implementation:Detector 與 Issue 的對映關係,需要傳入當前的 Detector 類,以及掃描程式碼的範圍,例如 Java 檔案、Resource 檔案或目錄等範圍。
如下圖,產生問題時,問題的提醒資訊就就會顯示相關的 Issue 的 id 等資訊。123.png
3.4 Category
Category 用於給 Issue 分類,系統已經提供了幾個常用的分類,系統 Issue(即 Android Lint 自帶的檢查規則)也是使用這個 Category:
- Lint
- Correctness (子分類 Messages)
- Security
- Performance
- Usability (子分類 Typography, Icons)
- A11Y (Accessibility)
- I18N (Internationalization,子分類 Rtl)
如果系統分類不能滿足需求,也可以建立自定義的分類:
public class QMUICategory {
public static final Category UI_SPECIFICATION = Category.create("UI Specification", 105);
}
使用如下:
public static final Issue ISSUE_JAVA_VECTOR_DRAWABLE =
Issue.create("QMUIGetVectorDrawableWithWrongFunction",
"Should use the corresponding method to get vector drawable.",
"Using the normal method to get the vector drawable will cause a crash on Android versions below 4.0",
QMUICategory.UI_SPECIFICATION, 2, Severity.ERROR,
new Implementation(QMUIJavaVectorDrawableDetector.class, Scope.JAVA_FILE_SCOPE));
3.5 Registry
建立自定義 Lint 的最後一步是 “Lint-Registry”,如前面所述,build.gradle 中需要宣告 Regisry 類,打包成 jar:
jar {
manifest {
attributes('Lint-Registry': 'com.qmuiteam.qmui.lint.QMUIIssueRegistry')
}
}
而 registry 類中則是註冊建立好的 Issue,以 QMUIIssueRegistry 為例:
public final class QMUIIssueRegistry extends IssueRegistry {
@Override public List<Issue> getIssues() {
return Arrays.asList(
QMUIFWordDetector.ISSUE_F_WORD,
QMUIJavaVectorDrawableDetector.ISSUE_JAVA_VECTOR_DRAWABLE,
QMUIXmlVectorDrawableDetector.ISSUE_XML_VECTOR_DRAWABLE,
QMUIImageSizeDetector.ISSUE_IMAGE_SIZE,
QMUIImageScaleDetector.ISSUE_IMAGE_SCALE
);
}
}
QMUIIssueRegistry 繼承與 IssueRegistry,IssueRegistry 中註冊了 Android Lint 自帶的 Issue,而自定義的 Issue 則可以通過 getIssues 系列方法傳入。
到這一步,這個用於自定義 Lint 的 Java 專案編寫完畢了。
3.6 接入專案
Google 官方的方案是把 jar 檔案放到 ~/.android/lint/,如果本地沒有 lint 目錄可以自行建立,這個使用方式較為簡單,但也使得 Android Lint 作用於本地所有的專案,不大靈活。
在主專案中新建一個 Module,打包為 aar,把 jar 檔案放到該 aar 中,這樣各個專案可以以 aar 的方式自行引入自定義 Lint,比較靈活,專案之間不會造成干擾。
Module 的 build.gradle 內容如下(以 QMUI Lint 為例):
apply plugin: 'com.android.library'
configurations {
lintChecks
}
dependencies {
lintChecks project(path: ':qmuilintrule', configuration: 'lintChecks')
}
task copyLintJar(type: Copy) {
from(configurations.lintChecks) {
rename { 'lint.jar' }
}
into 'build/intermediates/lint/'
}
project.afterEvaluate {
def compileLintTask = project.tasks.find { it.name == 'compileLint' }
compileLintTask.dependsOn(copyLintJar)
}
其中 qmuilintrule 是自定義 Lint 規則的 Module,這樣這個需要進行 aar 打包的 Module 即可獲取到 jar 檔案,並放到 build/intermediates/lint/ 這個路徑中。把 aar 釋出到 Bintray 後,需要用到自定義 Lint 的地方只需要引入 aar 即可,例如:
compile 'com.qmuiteam:qmuilint:1.0.0'
另外需要注意,在編寫自定義規則的 Lint 程式碼時,編寫後重新構建 gradle,新程式碼也不一定生效,需要重啟 Android Studio 才能確保新程式碼已經生效。