我们在写代码的时候,有时候很容易使用一个高版本的 API,如果不注意,可能会导致在一些低版本的设备崩溃。
因此,我们可以选择引入 lint 在编译时进行检查。
今天的问题是?
- 应用:如何在打包时,强制开启 New API检查?
- 原理:lint 怎么能知道某个 方法是哪个版本加入的?是有一个汇总的地方维护着这样的方法列表吗?
- 原理:即使有这样的一个列表,lint是怎么扫描每一个方法调用的?
补一个问题:问题4:低版本存在,高版本已经删除的api(是 compileSdkVersion对应版本没有,直接调用报错的),除了反射用什么较为优雅的方法来代码调用呢。
更多问答 >>
-
每日一问 | View invalidate() 相关的一些细节探究~
2020-12-27 22:38 -
每日一问 | RxJava中Observable、Flowable、Single、Maybe 有何区别?
2021-01-03 20:34 -
每日一问 | Java中匿名内部类写成 lambda,真的只是语法糖吗?
2021-01-11 00:00 -
每日一问 | Java 中的 lambda 与 Android 中的 lambda 有什么不同?
2021-01-31 17:20 -
每日一问 | Android 中两种设置线程优先级的方式,有何区别?
2021-01-27 23:59 -
每日一问 | 当Unsafe遇上final,超神奇的事情发生了?
2020-11-02 00:16 -
每日一问 | 属性动画与硬件加速的相遇,不是你想的那么简单?
2020-10-26 23:45 -
每日一问 | 关于 RecyclerView$Adapter setHasStableIds(boolean)的一切
2020-10-26 23:44 -
每日一问 | 玩转 Gradle,可不能不熟悉 Transform,那么,我要开始问了。
2020-10-26 23:45 -
每日一问 | 启动了Activity 的 app 至少有几个线程?
2020-10-12 00:47
Android Studio检测New API的原理?
先简单概况一下: Android Studio的Editor会在每一次内容变更完毕后,给一个叫alarm pool
的线程池提交一个MergingUpdateQueue(实现了Runnable),这个MergingUpdateQueue最终会执行到LintDriver的analyze
方法,也算是Lint的入口了。在检查文件之前,首先会初始化一个ApiLookup对象,这个对象在初始化时也会加载项目compileSdkVersion所对应的api-versions.xml
文件,以byte[]的形式保存在内存中(不是文件流的byte[]哦,是处理过后的数据,可以通过索引来快速定位到某个类和某个方法的),同时还会写出到 /root/.AndroidStudio4.0/system/lint/ 目录下(我的是Linux系统,其他系统的目录应该会有变更),以bin作为后缀保存(下次会优先读取这个bin文件,如果那个api-versions.xml
没有被修改过的话)。当ApiVisitor开始visitCallExpression
,调用ApiLookup的getMethodVersion
方法获取目标方法【首次出现的版本号】时,就能通过索引从那个byte[]中快速取出,然后对比minSdk
,如果比minSdk
大的话,证明目标方法是New API,这时候就会反馈错误了。详细分析:
咋一看这个问题感觉有点无从下手,就算知道它在哪里开启,知道它关联的是哪个文件,那又怎样?在没有AS源码的情况下,怎么能定位到具体代码位置呢?其实我们可以用做逆向的方法:根据界面显示的一些相关的词来搜索。在哪里搜?当然是Android Studio程序目录里的plugins
文件夹了,了解Android Studio的同学会知道,Android Studio只不过是IntelliJ IDEA中一个比较大型的插件,再加上对IntelliJ IDEA做了点修改而已,所以Android Studio特有的功能,基本上都会以jar的形式存放在plugins
目录下。我们可以用grep(Windows的同学可以去开那个Linux子系统)先定位到对应jar文件,再找到对应的class。关键词,就试试 @Reginer 同学说的设置里面的那个开关吧:grep -rn "Calling new methods on older versions"
里面有个implementationClass属性,指向了一个叫AndroidLintNewApiInspection的类,对照着包名从解压出来的目录中找到这个class,拖进AS里(反编译)看看:
咦?这个AndroidLintNewApiInspection导了刚刚在sdk-tools那边搜到的包含 "Calling new methods on older versions" 关键词的ApiDetector,还把ApiDetector的
那现在来看看ApiDetector这个类以及它里面的UNSUPPORTED
传给了父类构造方法!UNSUPPORTED
是什么:直接拖进AS里会发现,这个类是用Kotlin写的,只能列出一些方法还有变量的声明,不能完全反编译。用jadx打开看下吧:原来这个UNSUPPORTED是一个Issue!
接着往下看,Implementation的detectorClass
它填的就是ApiDetector。还可以看到ApiDetector实现了SourceCodeScanner(熟悉Lint API的同学会知道UastScanner接口的方法都是来自于SourceCodeScanner的),SourceCodeScanner中有个createUastHandler
方法,需要返回一个UElementHandler(检查代码的主要逻辑都在这个UElementHandler里面)看看ApiDetector它是怎么实现的:emmmm,它返回的是内部类ApiVisitor的实例。想一下,会出现题目中所说的 “Call requires API level 23 (current min is 14)“ ,都是在调用某个方法时出现的,也就是说,这个提示针对的是方法调用,而在UElementHandler中,【分析方法调用】的方法就是
visitCallExpression
,看看ApiVisitor是怎么重写的:逻辑很清晰,检查是否New API的逻辑在后面的
visitCall
方法中:它首先会判断目标class的包名是不是android.support
开头的(support包的代码不用检查),然后对比【目标方法首次出现的版本】和【项目中的minSdk版本】,如果前者更大,那么在minSdk的设备上运行,就很有可能会出问题了,所以接下来会调用report
方法(如果没有加Suppress注解的话),看下它里面做了什么:刚刚的
在拼接完Error Message之后,会通过Context的visitCall
调用的是第一个report
方法,我们主要看它在调用下面的report
方法时,传的第4个参数,它传的是getApiErrorMessage
方法的返回值,看到这里大家应该心中有数了,getApiErrorMessage
方法返回值的格式,就是题目中那个提示的格式了,拼接的第一个参数type,在上面的visitCall
方法里传的是 "Call" ,可以翻回去看看。report
反馈错误。emmmm,【如何判断】的逻辑是分析完了,但它究竟是怎么获取到目标方法【第一次出现的版本】的呢?难道每次都读取一次xml吗?那样不仅效率低而且也太浪费资源了吧。它到底是怎么做的?
从上面的visitCall
方法中可以看到,它是通过ApiDatabase的getMethodVersion
方法来获取的,看看里面做了什么:最主要的
那只好把Android Studio的源码下载下来了:findMember
方法居然看不到。。。反编译不了。下载Android Studio源码
考虑到网络不畅通的同学,我在半个月前就已经同步好了4.1.0的源码,并上传到了百度云上,哈哈哈哈,因为太大了,国内的代码仓库都放不下,只能打包放网盘上了。网盘链接: https://pan.baidu.com/s/14O9aD9GJCLTbTHm6KjpUdA 提取码: z96p网盘上的是完整代码,解压出来15G左右。不过,里面很多平台相关的东西,还有jdk源码部分,对这次源码分析根本没用,所以我又专门把部分基础代码上传到了gitee上,地址:https://gitee.com/wuyr_/android-studio-4.1.0,同步下来之后,用 IntelliJ IDEA Community 直接打开就行。当然了,如果你想直接从googlesource上同步源码,也是可以的:https://android.googlesource.com/platform/tools/base/+/studio-master-dev/source.md 这里是同步源码的官方教程。好,打开源码之后,我们来看看刚刚那个ApiLookup的
findMember
方法,位置就在 /base/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks 目录下:可以看出这是一个二分查找,在找到正确的
offset
之后调用了getApiLevel
方法,并把offset
传了进去:在这里会直接从
说明数据都保存在mData
里面取出对应offset
的值,转正数int之后去掉API_MASK标记,然后return。mData
里面,看看它是怎么来的:它是ApiDatabase(刚刚的ApiLookup的父类)里的成员变量,在
emmmm,为了提高效率,我们还是借助Debug来分析吧(先给这个readData
方法中初始化。readData
方法打下断点)。Debug Android Studio
在开头就已经介绍过,AS其实是IntelliJ IDEA的一个大型插件,所以我们可以用debug IntelliJ IDEA插件的方式来debug AS。不过,平时debug插件的时候,新的IDEA进程都是在IDEA中启动的,那要怎样才能从IDEA中启动一个可debug的AS呢?细心的同学会看到过,当IDEA启动debug进程时,在下面的run窗口中会看到这么一串东西:debug
吧:java
的后面,记得前后要有空格隔开。好啦,按下enter,可debug的Adnroid Studio就启动了~趁还没启动完成,赶紧回到IDEA窗口中,点那个绿色的小虫子,attach到AS进程。等待AS初始化完毕,就能看到刚刚在readData
方法打的断点已经生效啦:cacheCreator
,还能看到项目compileSdkVersion对应的那个api-versions.xml
的File对象!翻一下左边的栈帧,发现readData
方法是在ApiDetector的beforeCheckRootProject
方法里调用的:visitCall
方法,它里面使用的ApiDatabase对象(调用getMethodVersion
方法获取方法版本)是在这时候初始化的。看下它初始化的时候都做了些什么:这里有个缓存机制,当然了,第一次进入的时候肯定是null,所以会进入到第一个if里面。
获取到api-versions.xml
的File对象之后,会调用另一个get
方法:很明显,第一个if的条件现在写死是false的(看它的注解也能知道是测试时才用的),我们重点看下面的else if,它有三个条件,分别是:
binaryData
不存在;xmlFile
(也就是那个api-versions.xml)的最后修改日期比binaryData
要大;binaryData
文件为空;满足其中任何一个条件,就会调用
cacheCreator
返回对象的create
方法:这个
cacheCreator
返回的是接口CacheCreator的对象,可以看到这个接口用了FunctionalInterface修饰,说明它的create
方法可以写成lambda形式的,看它的实现,主要做了两件事:parseApi
方法获取Api对象;writeDatabase
方法并把刚刚获取到的Api对象传了进去;看下
parseApi
方法:额,好像没什么好说的,就是解析xml(那个api-versions.xml)。
看看writeDatabase
方法吧:。。。。。。。
好长啊,200多行,就不贴出来了,主要逻辑就是把刚刚解析出来的xml数据,写到一个bin文件里(关联索引),感兴趣的同学可以借助debug来充分理解。
好了,回到刚刚的
get
方法,在写完数据后,最后会new一个ApiLookup对象:咦?它里面调用了前面分析过的
readData
方法,这样整个流程就打通了,readData
方法里,在读取binaryFile
文件后是直接赋值给mData
,没有做任何修改的:好啦,剩下的就留给同学们自己去探索啦,你可以尝试:
在ApiLookup.
get
方法里打断点,手动删除对应路径的bin文件之后,debug看看创建bin文件的整个流程;debug
writeDatabase
方法,看看bin文件里面的数据是如何存放的;思考为什么要有这个bin文件? 像它这种做法,能不能在自己日常开发中用上?
有了Android Studio的源码,其实不止这个Lint检测功能,还有其他比如 自定义控件是如何做到预览的,AS 中我们的 View 的代码执行了? 这个LayoutEditor的原理,也能通过debug去快速了解了(后面会更新这个问题的回答)。
赞
距离很快已经过去一天了
求更新0.0
更新啦~
我是谁?我在哪儿?我在干什么?。。。。
1.检查 -> Android->Lint->Correctness->Calling new methods on older versions
2.sdk\platforms\android-api\data\api-versions.xml
3.com.android.tools.lint.checks ApiDetector
4.现在我是更改系统源码重新编译出来android.jar,之后将对应的方法标记为removed,但是感觉太麻烦了,又不想用反射,现在陷入了纠结阶段
4呢
methodhandle
4.版本存在,高版本已经删除的api。创建一个同包同类名的文件,使用哪个方法就再创建对应的空方法,就可以直接在项目中使用了。由于类遵循双亲委派模式,运行时会使用系统的类而不会使用自己写的类。
这样编译不了,文件同名了,编译会报错的
那是你使用姿势不对,换个姿势就好。
如果我没猜错的话,是你搞错了,adk里有同名类,没有对应方法。 你这样说容易误导没搞过的人,以为你的方法可行然后浪费时间去尝试。