关于Android中自定义ClassLoader耗时问题的追查

前言

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 中使用到了。

  • 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/native/javalangVMClassLoader.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混合编译与对热补丁影响解析

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • 详解Android类加载ClassLoader

    基本知识 Java的类加载设计了一套双亲代理的模式,使得用户没法替换系统的核心类,从而让应用更安全.所谓双亲代理就是指,当加载类的时候首先去Bootstrap中加载类,如果没有则去Extension中加载,如果再没有才去AppClassLoader中去加载.从而实现安全和稳定. Java ClassLoader BootstrapClassLoader 引导类加载器 ,用来加载Java的核心库.通过底层代码来实现的,基本上只要parent为null,那就表示引导类加载器. 比如:charsets

  • 浅谈Android Classloader动态加载分析

    ClassLoader概念 我们知道,Java源文件(.java)经过编译器编译之后,会转换成Java字节码(.class),然而程序是如何加载这些字节码文件到内存中呢?这就用到了ClassLoader,即类加载器.ClassLoader类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例.从而只有class文件被载入到了内存之后,才能被其程序所引用.所以ClassLoader就是用来动态加载class文件到内存当中用的. ClassLoader的分类 An

  • Android开发中类加载器DexClassLoader的简单使用讲解

    简介 "类装载器"(ClassLoader),顾名思义,就是用来动态装载class文件的.标准的Java SDK中有个ClassLoader类,借助此类可以装载需要的class文件,前提是ClassLoader类初始化必须制定class文件的路径. import关键字引用的类文件和ClassLoader动态加载类的区别: import引用类的两个特点: 1.必须存在于本地,当程序运行该类时,内部类装载器会自动装载该类. 2.编译时必须在现场,否则编译过程会因找不到引用文件而不能正常编译

  • 关于Android中自定义ClassLoader耗时问题的追查

    前言 Android中类加载器有BootClassLoader,URLClassLoader, PathClassLoader,DexClassLoader,BaseDexClassLoader,等都最终继承自java.lang.ClassLoader 最近在优化西瓜视频客户端冷启动速度时,发现在关闭插件 ClassLoader 注入的情况下,启动速度提升了300ms左右,但是西瓜在启动阶段并没有使用到插件,那么这么大的耗时是怎么来的呢?下面话不多说了,来一起看看详细的介绍吧. 猜原因 首先看下

  • Android中自定义水平进度条样式之黑色虚线

    以下内容给大家介绍Android中自定义水平进度条样式之黑色虚线,对代码实现方法感兴趣的朋友一起学习吧. 布局layout中使用: <ProgressBar android:id="@+id/progress_bar" style="?android:attr/progressBarStyleHorizontal" <!--必须设置为水平--> android:progressDrawable="@drawable/myprogress&

  • Android中自定义PopupWindow实现弹出框并带有动画效果

    使用PopupWindow来实现弹出框,并且带有动画效果 首先自定义PopupWindow public class LostPopupWindow extends PopupWindow { public Lost lost; public void onLost(Lost lost){ this.lost = lost; } private View conentView; public View getConentView() { return conentView; } public L

  • Android 中自定义ContentProvider与ContentObserver的使用简单实例

    Android 中自定义ContentProvider与ContentObserver的使用简单实例 示例说明: 该示例中一共包含两个工程.其中一个工程完成了自定义ContentProvider,另外一个工程用于测试该自定义ContentProvider且在该工程中使用了ContentObserver监听自定义ContentProvider的数据变化 以下代码为工程TestContentProvider ContentProviderTest如下: package cn.testcontentp

  • Android中自定义的dialog中的EditText无法弹出输入法解决方案

    1.解决无法弹出输入法: 在show()方法调用之前,用dialog.setView(new EditText(context))添加一个空的EditText,由于是自定义的AlertDialog,有我们指定的布局,所以设置这个不会影响我们的功能,这样就可以弹出输入法了-- 2.可以弹出输入法了,但了为了增强用户体验性,当dialog中含有editText时应该,在显示dialog的同时自动弹出键盘: (1) 可以在自定义的dialog中增加如下方法: public void showKeybo

  • Android中自定义view实现侧滑效果

    效果图: 看网上的都是两个view拼接,默认右侧的不显示,水平移动的时候把右侧的view显示出来.但是看最新版QQ上的效果不是这样的,但给人的感觉却很好,所以献丑来一发比较高仿的. 知识点: 1.ViewDragHelper 的用法: 2.滑动冲突的解决: 3.自定义viewgroup. ViewDragHelper 出来已经比较久了 相信大家都比较熟悉,不熟悉的话google一大把这里主要简单用一下它的几个方法 1.tryCaptureView(View child, int pointerI

  • Android中自定义一个View的方法详解

    本文实例讲述了Android中自定义一个View的方法.分享给大家供大家参考,具体如下: Android中自定义View的实现比较简单,无非就是继承父类,然后重载方法,即便如此,在实际编码中难免会遇到一些坑,我把自己遇到的一些问题和解决方法总结一下,希望对广大码友们有所帮助. 注意点① 用xml定义Layout时,Root element 最好使用merge 当我们需要继承一个布局比较复杂的ViewGroup(比较多的是LinearLayout.RelativeLayout)时,通常会用xml来

  • Android 中自定义Dialog样式的Activity点击空白处隐藏软键盘功能(dialog不消失)

    一.需求触发场景: 项目中需要开发带有EditText的Dialog显示,要求在编辑完EditText时,点击Dilog的空白处隐藏软键盘.但是Dialog不会消失.示例如下: 二.实现方法: 发布需求时,我个人曾想过直接通过new的方式直接创建Dialog,经过多次尝试,无法实现要求,所以采用将Activity设置为Dialog样式进行展示,调用方法实现需求.具体实现如下: 本次演示示例的工程结构: 2.1AndroidMainfest.xml配置文件 需要在配置文件中将需要显示为dialog

  • 解决Android中自定义DialogFragment解决宽度和高度问题

    关于详解Android应用中DialogFragment的基本用法,大家可以参考下. 1. 概述 DialogFragment在android 3.0时被引入.是一种特殊的Fragment,用于在Activity的内容之上展示一个模态的对话框.典型的用于:展示警告框,输入框,确认框等等. 在DialogFragment产生之前,我们创建对话框:一般采用AlertDialog和Dialog.注:官方不推荐直接使用Dialog创建对话框. 2. 好处与用法 使用DialogFragment来管理对话

  • Android中自定义Checkbox组件实例

    在Android中,Checkbox是一个很重要的UI组件,而且在Android中,它展现的形式越来越好看,这就说明有些系统,比如4.0以下,checkbox还是比较不好看,或者跟软件的风格不协调,就需要我们自定义这个组件. 自定义这个组件很简单,简单的增加修改xml文件即可. 准备工作 准备好两张图片,一个是选中的图片,另一个是未选中的图片.本文以checked.png和unchecked.png为例. 设置选择框 在drawable下新建文件custom_checkbox.xml 复制代码

随机推荐