时间:2021-05-19
前言
Android中类加载器有BootClassLoader,URLClassLoader,
PathClassLoader,DexClassLoader,BaseDexClassLoader,等都最终继承自java.lang.ClassLoader
最近在优化西瓜视频客户端冷启动速度时,发现在关闭插件 ClassLoader 注入的情况下,启动速度提升了300ms左右,但是西瓜在启动阶段并没有使用到插件,那么这么大的耗时是怎么来的呢?下面话不多说了,来一起看看详细的介绍吧。
猜原因
首先看下西瓜目前使用的插件 ClassLoader 是怎么注入的,大致代码如下:
代码大致意思是在 PathClassLoader 和 BootClassLoader 之间插入了一个 DelegateClassLoader,而在 DelegateClassLoader 的 findClass 方法中去执行插件 Class 的加载。
为了方便验证,写一个简单的测试Demo,测试加载一个类的耗时:
以小米Max2,Android7.1.1机型为例,测试不注入和注入 DelegateClassLoader 加载一个类的耗时:
不注入:60μs
注入后:472μs
差不多慢了8倍,测试了几款手机基本数据都差不多,但是4.x手机上这两种情况下耗时差别却很小。
DelegateClassLoader.findClass耗时?
因为双亲委托机制,所以宿主中所有类的加载都会走到 DelegateClassLoader.findClass 中,但是 DelegateClassLoader 中因为不存在宿主类,所以必然找不到,因此一个宿主类的加载会多调用了一次无用的 findClass 方法,一次findClass的调用会带来如此大的耗时?于是将 DelegateClassLoader 代码精简成下面这样的:
这样,DelegateClassLoader 中没有做任何插件类加载的逻辑,只是做了一个中转到父 ClassLoader 的 loadClass 的操作。
结果依然是8倍左右的耗时差距。
java方法调用耗时?
上面方案里只是比不注入自定义 ClassLoader 多了一次 DelegateClassLoader.loadClass 方法的调用,理论上不可能存在这么大的耗时。如果说多调用一次 java 方法 DelegateClassLoader.loadClass 会有8倍的耗时差异的话,那么多调用两次是不是就是16倍的差异?
于是尝试注入两个 DelegateClassLoader,类似这样:
但是结果还是8倍左右的耗时差异,并非16倍,这么说不是方法调用带来的性能损耗。
自定义ClassLoader耗时?
所以猜测可能是系统对 PathClassLoader 有什么优化?然后直接构造一个空的 PathClassLoader 注入到 PathClassLoader 和 BootClassLoader 中间,类似这样:
神奇的8倍耗时差异没了!所以真的是系统对 PathClassLoader 有优化?
带着这个疑问我们来看下 ClassLoader 的源码,以 Android 7.1.1 源码为例。
ClassLoader#loadClass
首先来看下源头,ClassLoader 的 loadClass 源码,核心代码如下:
大致流程是先调用 findLoadedClass 尝试从已加载的 class 中查找,然后再调用父 ClassLoader 的 loadClass 查找,如果依然没有找到的话,最后再调用自己的 findClass 加载。
在 JVM 中,类第一次加载时,肯定之前是没有加载过的,因此 findLoadedClass 应该是返回 null 的,而 BootClassLoader 中只有系统类,因此宿主类的加载应该是调用了 PathClassLoader#findClass 加载的。
PathClassLoader#findClass
那么我们再来看看 PathClassLoader#findClass 的源码,调用链大致如下:
如果说系统对 ClassLoader 有某些优化,那么应该只要重点关注在调用链中有用到 ClassLoader 的地方即可。
整个 findClass 流程中使用到 ClassLoader 的地方并不多,只有 ClassLinker::RegisterDexFile 和 ClassLinker::SetupClass 中使用到了。
如果不是 findClass 里有优化,难道在 ClassLoader#findLoadedClass 里?
ClassLoader#findLoadedClass
再来看看 ClassLoader#findLoadedClass 的源码,调用链大致如下:
首先来看下c层调用的第一个方法 VMClassLoader_findLoadedClass :
这里主要有两个分支,第一个分支,第12行调用 ClassLinker#LookupClass :
这里大致意思是从 ClassLoader 中找到 ClassTable ,然后调用 ClassTable#Lookup 而这个 ClassTable 里面就保存了已经加载过的类以及启动时从 app image 中加载的类(app image的作用是记录已经编译好的“热代码”,并且在启动时一次性把它们加载到缓存,参考Tinker博客)。如果一个类是首次加载且不在 app image 中,那么这里会返回 null。
这样就会走到第二个分支(第25行) ClassLinker::FindClassInPathClassLoader 中
这里主要分为两个部分:
重点来了!因为上面使用到了反射机制取 PathClassLoader 中的字段,为了保证这套机制不出问题,这里面加了个校验:
如果 ClassLoader 链中存在不认识的 ClassLoader,也就是说 ClassLoader 的类不是 BootClassLoader 和 PathClassLoader,那么就认为加载类失败。当然这里加载失败的话,并不会影响最终类加载结果,因为在 Java 层 findLoadedClass 失败后,会走到 findClass 中的。
结论
在 Android ART 中默认的 ClassLoader 机制,在 ClassLoader#findLoadedClass 时就把 JVM 中的 findLoadedClass 和 findClass 两件事情都做了。但是如果在 class loader 链中存在自定义 ClassLoader,那么这个机制就会失效,会回退到 JVM 默认的 ClassLoader 机制。
回到上面的问题,由于我们自定义了 ClassLoader,导致 Art 的 ClassLoader 机制回退到了 JVM 的默认类加载机制,而 JVM 默认的类加载机制存在多次 JNI 调用,JNI 调用本身性能是比直接方法调用耗时高几倍的,这里不再详细展开,因此也就能解释前面所说的几倍的耗时差异了。
参考
Android N混合编译与对热补丁影响解析
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。
声明:本页内容来源网络,仅供用户参考;我单位不保证亦不表示资料全面及准确无误,也不保证亦不表示这些资料为最新信息,如因任何原因,本网内容或者用户因倚赖本网内容造成任何损失或损害,我单位将不会负任何法律责任。如涉及版权问题,请提交至online#300.cn邮箱联系删除。
背景在自定义Egg.js的请求级别日志这篇文章中,我们实现了自定义请求级别的日志模块。看上去功能是完整了,但好像还缺点什么。大家在根据日志追查问题的过程中,很多
前言:前面几篇讲了自定义控件绘制原理Android自定义控件基本原理详解(一),Android自定义控件之自定义属性(二),Android自定义控件之自定义组合
笔者有一段时间没有发表关于Android的文章了,关于Android自定义组件笔者有好几篇想跟大家分享的,后期会记录在博客中。本篇博客给大家分享的是自定义一个日
Android中ScrollView与ListView冲突问题的解决办法自定义MyListViewpublicclassMyListViewextendsLis
如果要使用自定义类加载器加载class文件,就需要继承java.lang.ClassLoader类。ClassLoader有几个重要的方法:protectedC