Android自定义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 中使用到了。
ClassLinker::RegisterDexFile 中是对 ClassLoader 取 class_table 的简单操作;
ClassLinker::SetupClass 中是给加载好的 class 设置 ClassLoader,两个方法对 ClassLoader 的操作看上去是不存在任何优化的,理论上不会导致性能损耗,这里不再贴代码。
如果不是 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 中
这里主要分为两个部分:
第一部分:从37行开始,反射从 Java 层的 PathClassLoader 取得 DexPathList,然后再反射从 DexPathList 中取得 dexElements,然后再遍历 dexElements,从每个 Element 中取得 dexFile,然后再从 DexFile 中取得 mCookie,然后通过 mCookie 得到 c 层的 DexFile,最后调用 c 层 DexFile#FindClassDef 来真正的执行类的加载,整个流程其实就是在 c 层把 Java 层的 PathClassLoader#findClass 逻辑走了一遍;
第二部分:采用递归的方式,从 BootClassLoader 开始依次到 PathClassLoader 逐个调用 FindClassInPathClassLoader,直到找到 class 为止,相当于把 Java 层 ClassLoader 的双亲委托加载 class 的机制在 c 层做了一遍,这个其实是 ART 上对 class 加载做的一个优化,但是在 Dalvik 中是没有这段逻辑的,可以参考/dalvik/vm/native/java_lang_VMClassLoader.cpp。
重点来了!因为上面使用到了反射机制取 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混合编译与对热补丁影响解析
长按识别二维码,关注今日头条技术团队
Android自定义ClassLoader耗时问题追查相关推荐
- 抽取样本java实验报告_一个自定义classloader的函数抽取壳样本
原标题:一个自定义classloader的函数抽取壳样本 本文为看雪论坛文章 看雪论坛作者ID:lemn 本文为 看雪安卓高研2w班(7月班)优秀学员作品. 下面先让我们来看看学员的学习心得吧! 学员 ...
- android classloader异常,Android中ClassLoader类加载机制
Android中apk的构建过程 构建apk 如图 所示,典型 Android 应用模块的构建流程通常依循下列步骤: 编译器将您的源代码转换成 DEX(Dalvik Executable) 文件(其中 ...
- android 自定义字体_Android自定义字体教程
android 自定义字体 In this tutorial, we'll explain how to set up android custom fonts in TextViews and Bu ...
- springboot自定义ClassLoader实现同一个jar支持多版本的使用场景【附源码】
springboot自定义ClassLoader实现同一个jar支持多版本的使用场景 背景 最近业务提出一个业务场景:系统目前支持hive3.1.0版本的数据源适配,但是有个别部门使用的数据源是hiv ...
- Android 自定义viewpage + videoview 实现竖屏视频播放效果
Android 自定义viewpage + videoview 实现竖屏视频播放效果 效果图 实现步骤 前提概要 自定义 viewpage 自定义 videoview 主逻辑代码 效果图 由于用的是v ...
- Android自定义权限CVE漏洞分析 (IEEE论文)
文章目录 前言 自定义权限早期漏洞 1.1 Android权限机制 1.2 自定义权限升级漏洞 1.3 confused deputy attack 自定义权限近期漏洞 2.1 黑盒Fuzz工具原理 ...
- java 自定义classloader_编写自定义classloader实现类的动态加载
目标:实现类的动态加载 原理:使用java的自定义classloader机制实现类的动态加载. 代码实现://自定义classloader public class StrategyClassLoad ...
- Android自定义view摇杆,Android自定义View实现游戏摇杆键盘的方法示例
前言 本文主要给大家介绍的是关于Android自定义View实现游戏摇杆键盘的相关内容,为什么会有这篇文章呢?因为在之前的一个项目,操作方向的方式为上下左右,左上需要同时按住左键和右键的方式进行操作. ...
- Android自定义实现刮刮卡效果
转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/40162163 , 本文出自:[张鸿洋的博客] 很久以前也过一个html5的刮刮卡 ...
最新文章
- 安装meme_通过构建Meme生成器学习React
- ubuntu dpkg initramfs-tools错误的解决方法
- 虚拟机开启mysql密码报错_Linux虚拟机下mysql 5.7安装配置方法图文教程
- hdu 1546(最短路)
- cannot load SAP UI5 application view S2 useful method for resource load
- MongoDB C#:如何将包含DateTime的JSON反序列化为正确的BsonDocument DateTime值
- xxs漏洞危害_PHP开发中经常遇到的Web安全漏洞防御详解
- mysql文件_mysql 的各种文件详细说明
- 已解决:fastclick插件在IOS系统上点击input需要双击或长按才有效
- 11.CSS border边框
- 二进制文件和文本文件的区别
- 这个小众副业,一次200,有人月入3万!
- list()与tolist()区别
- 51单片机自学--PWM波占空比程序解释
- P2P网络与BitTorrent技术简介
- 电缆故障测试仪的基本原理与组成——TFN DG15M电缆故障测试仪
- 【C语言】声明与定义
- Bmob用户功能详解(三)
- 打印机无法打印-常见故障总结
- 软件测试宣传ppt,[软件测试ppt完整版.ppt