minSdkVersion 21 的情況下使用 lint 檢查低版本呼叫高版本方法
前言
公司專案近期正在將 XML 佈局檔案轉換為純程式碼編寫,但是由於之前為了避免 65535 問題和開發環境編譯速度,所以build.gradle
中配置了一個minSdkVersion
為 21 的productFlavors
。這就導致在轉換工程中出現了很多呼叫高版本方法(比如View#setElevation
)的問題,Lint 也不會提示開發者修改。
為了避免這個問題,開始尋找解決的辦法,經過搜尋後發現沒有類似的問題,這裡記錄一下我的方法,希望能給有類似問題的朋友一點幫助。由於個人水平有限,有錯誤請指出。
思路
- 找到能夠設定 Lint 檢查 minSdkVersion 的方法。
很遺憾,沒能找到,有的話也就沒有此文章了。
不過我已經向 Google 提出了Feature Request,也不知道會不會被採納。
-
檢視 AGP(Android Gradle Plugin) 和 Lint 原始碼, 找到關鍵步驟,通過 Gradle 插入 Task 呼叫
setMinSdkVersion()
。
很遺憾,由個人水平有限,而且這個方法花費時間較多,所以耗費一段時間後就放棄了。這裡貼出一下相應原始碼解析:
Android Lint工作原理剖析 從Android Plugin原始碼開始徹底理解gradle構建:初識AndroidDSL(一) Android Gradle Plugin原始碼分析-
自定義 Lint Rules,複製 Api 相應的原始碼,更改原始碼中的
minSdkVersion
,偷樑換柱。
可行性高,本文後續就講解該方案的實現過程。
自定義 Lint Rules,偷樑換柱
注意: 為了能夠讓自定義 lint 能夠在編碼階段實時檢查,請將根目錄下的 AGP 版本與 Android Studio 版本保持一致,否則可能不會生效!
由於上述文章都是以 AGP 2.X 版本為背景進行開發的,但是 AGP 應該都是 3.X 版本了,所以這裡主要講一下其中的區別:
-
引入自定義
lint.jar
不再需要採用LinkedIn 的方案,官方已提高lintChecks
支援。程式碼請參考googlesamples/android-custom-lint-rules/android-studio-3 。
在 AGP 3.5.0 中,我還發現了lintPublish
這個與 lint 相關的關鍵字,沒發現與lintChecks
的區別。目前發現的作用是,如果要像 AGP 2.X 版本將 lint.jar 打包進 aar 的話,使用lintChecks
是不行的,lintPublish
才會生效。
有興趣的朋友可以AGP 3.5.0-alpha10 下載 jar 包檢視對應原始碼, 位置是com.android.build.gradle.internal.TaskManager#createCustomLintChecksConfig(Project)
。
-
自定義 lint 的 java 工程中的 manifest 配置引數有所改變。
jar {
// 向 java 中的 manifest 寫入
manifest {
// 指定自定義的 Lint 檢查類
// 為了保險起見,其實兩個都可以加上。
// AGP 3.X
attributes("Lint-Registry-v2": "com.example.lint.MyIssueRegistry")
// AGP 2.X attributes("Lint-Registry": "com.example.lint.MyIssueRegistry") } }
通過檢視lint-checks
原始碼,可以從BuiltinIssueRegistry
找到負責 Api 相關檢查的類ApiDetector
。
但是通過 gradle 依賴的lint-checks
中沒有提供 .java 原始碼。所以需要去googlesource 找 java 原始碼,然後複製相關的原始檔即可。
/** * Looks for usages of APIs that are not supported in all the versions targeted by this application * (according to its minimum API requirement in the manifest). */ public class ApiDetector extends ResourceXmlDetector implements SourceCodeScanner, ResourceFolderScanner { public static final AndroidxName REQUIRES_API_ANNOTATION = AndroidxName.of(SUPPORT_ANNOTATIONS_PREFIX, "RequiresApi"); public static final String SDK_SUPPRESS_ANNOTATION = "android.support.test.filters.SdkSuppress"; /** * Accessing an unsupported API */ @SuppressWarnings("unchecked") public static final Issue UNSUPPORTED = Issue.create( "NewApi_Mock", // ① "Calling new methods on older versions", "This check scans through all the Android API calls in the application and " + "warns about any calls that are not available on **all** versions targeted " + "by this application (according to its minimum SDK attribute in the manifest).\n" + "\n" + "If you really want to use this API and don't need to support older devices just " + "set the `minSdkVersion` in your `build.gradle` or `AndroidManifest.xml` files.\n" + "\n" + "If your code is **deliberately** accessing newer APIs, and you have ensured " + "(e.g. with conditional execution) that this code will only ever be called on a " + "supported platform, then you can annotate your class or method with the " + "`@TargetApi` annotation specifying the local minimum SDK to apply, such as " + "`@TargetApi(11)`, such that this check considers 11 rather than your manifest " + "file's minimum SDK as the required API level.\n" + "\n" + "If you are deliberately setting `android:` attributes in style definitions, " + "make sure you place this in a `values-v`*NN* folder in order to avoid running " + "into runtime conflicts on certain devices where manufacturers have added " + "custom attributes whose ids conflict with the new ones on later platforms.\n" + "\n" + "Similarly, you can use tools:targetApi=\"11\" in an XML file to indicate that " + "the element will only be inflated in an adequate context.", Category.CORRECTNESS, 6, Severity.ERROR, new Implementation( ApiDetector.class, EnumSet.of(Scope.JAVA_FILE, Scope.RESOURCE_FILE, Scope.MANIFEST), Scope.JAVA_FILE_SCOPE, Scope.RESOURCE_FILE_SCOPE, Scope.MANIFEST_SCOPE)); /** * Accessing an inlined API on older platforms */ public static final Issue INLINED = //...省略 /** * Method conflicts with new inherited method */ public static final Issue OVERRIDE = //...省略 /** * Attribute unused on older versions */ public static final Issue UNUSED = //...省略 /** * Obsolete SDK_INT version check */ public static final Issue OBSOLETE_SDK = //...省略 }
-
① 處,是為了防止與自帶 lint 中的
NewApi
區別。 -
建立一個常量
private static final AndroidVersion MIN_SDK_VERSION = new AndroidVersion(15, (String)null);
,然後搜尋ApiDetector
中minSdkVersion
變數,將MIN_SDK_VERSION
賦值給minSdkVersion
,這樣檢查時獲取的 minSdkVersion 就是固定的 15 了,當然這裡的 15 你可以任意修改。 -
在
VersionChecks.java
中,內部類ApiCheckGraph
會繼承ControlFlowGraph
,但是ControlFlowGraph
在lint-api
中不存在,後面發現只有一個方法使用此類,而這個方法也沒有呼叫,遂註釋之,以成功編譯。
最後按照前面提到的自定義 lint 方式編譯即可。
參考資料: