前言
公司项目近期正在将 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 请查看下列文章(别人写得好,也更详细):
【我的Android进阶之旅】Android自定义Lint实践
美团外卖Android Lint代码检查实践
注意: 为了能够让自定义 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 方式编译即可。
参考资料:
Android Lint工作原理剖析
从Android Plugin源码开始彻底理解gradle构建:初识AndroidDSL(一)
Android Gradle Plugin源码分析
【我的Android进阶之旅】Android自定义Lint实践
美团外卖Android Lint代码检查实践
googlesamples/android-custom-lint-rules/android-studio-3
googlesource
Writing Custom Lint Rules
https://engineering.linkedin.com/android/writing-custom-lint-checks-gradle