介绍 Android 类加载机制,双亲委派模型,BaseDexClassLoader, DexClassLoader, PathClassLoader 加载 dex, jar, apk 等文件。
JVM 虚拟机
执行方式
Java 的口号是“一次编译,到处运行”;也就是说 Java 程序的可执行文件(class 文件,字节码),是与机器硬件平台无关的;在程序运行时,由 JVM 将字节码翻译成当前运行环境处理器 CPU 的机器指令来执行。JVM 中通常有两种执行方式:
- 解释执行
字节码中每一行代码,由JVM解释器解释翻译成机器码再运行,依此循环从字节码中取出下一行解释并执行,因此解释执行的速度比较慢。 - 编译执行
JVM将字节码编译成机器码后,运行机器码的执行方式,执行效率很高。
通常情况下,JVM 采用混合执行的方式来运行程序的:将高频代码编译成机器码编译执行,其他代码直接解释执行。
编译方式
编译执行的过程,有两种常见的编译方式:
JIT:Just In Time
指在运行时编译,边运行边编译,属于动态编译。在程序启动时,将字节码编译成机器码,然后运行。优点是能更好的利用Java动态行为的特性(特别是多态,只有在运行时才能确定执行哪个方法);缺点是动态编译时间会计算在程序运行时间中,特别是会导致程序启动时间变长,以及程序运行期间编译带来的性能消耗。JIT通常只会将执行频率高的热方法动态编译,获取更好的效率平衡。AOT:Ahead Of Time
指在运行前编译,编译完后再运行,属于静态编译。在程序运行前,将字节码编译成机器码存储到本地,程序启动时,直接启动对应的机器码。优点是不存在动态编译的内存和性能消耗,直接运行本地机器码启动速度块;缺点是Java动态特性带来的复杂性,影响了静态编译的代码质量。
编译方式对比:
动态 JIT |
静态 AOT |
|
|---|---|---|
| 平台无关性 | 有 | 无 |
| 代码质量 | 优秀 | 良好 |
| 利用动态行为 | 是 | 否 |
| 类和层次结构的知识 | 有 | 无 |
| 编译时间 | 有限制,有运行时成本 | 限制很少,无运行时成本 |
| 运行时性能影响 | 有 | 无 |
| 编译方式 | 需要谨慎编译,由 JIT 处理 |
需要谨慎编译,由开发人员处理 |
总的来说,动态编译 JIT 能提供最好的系统稳定性能;而静态编译 AOT 能提供最好的交互性能。
Android 虚拟机
概念
Android 虚拟机实现有两个阶段:
DalvikAndroid K 4.4之前的版本都是使用的Dalvik虚拟机。Java生成的class文件由dx工具转换为Dalvik字节码即dex文件,之后进行签名对齐等操作变成APK文件。而Dalvik虚拟机负责将dex字节码解释为机器码,解释执行的速度慢效率低;从Android 2.2 froyo开始引入JIT动态编译执行方式,APP运行时,JIT将执行次数较多的dex文件动态编译为机器码缓存起来,后续再次执行时大大提高运行速度。JIT动态编译的特点是:每次打开APP运行时,都需要重新编译。ART: Android RuntimeAndroid K 4.4采用了Dalvik和ART共存方式,两者可以相互切换;从Android L 5.0开始彻底丢弃Dalvik全面转换为ART方式。ART虚拟机采用的是AOT静态编译执行方式,APP在第一次安装时,dex字节码会被预先编译成机器码;之后打开APP运行时,不需要额外的解释翻译工作,直接使用本地机器码执行,提高运行速度。
两者的区别
Dalvik是运行时编译;ART是运行前编译(安装时编译)Dalvik每次运行时都会将执行频繁的dex文件编译为机器码,运行启动时间加长,边运行边编译会额外销毁内存和系统性能ART在安装时编译,安装时间会延长;把程序代码转换成机器语言存储在本地,会消耗掉更多的存储空间,增幅通常不会超过应用代码包大小的 20%
文件格式
dex文件格式
魔数:dex,Android平台可执行文件,字节码;每个APK中包含一个或多个dex文件格式的可执行文件。odex文件格式
魔数:dey,odex: Optimize Dex即优化后的dex文件。Dalvik虚拟机从APK中将dex文件提取出来优化后生成odex文件,并存储在本地,后期运行时直接编译解释odex文件(仍然是字节码,dey字节码)。而APK被提取后可以有也可以没有dex文件;在多dex文件的APK中,提取优化后只会生成一个odex文件。oat文件格式
魔数:.elf,是ELF格式的可执行文件。ART虚拟机在APK安装时,将dex编译为本地机器码,并生成对应的oat文件格式的文件,可以直接执行。vdex文件格式
魔数:vdex,是APK中dex文件的一份拷贝,同时会将多个dex合并为一个文件。在Android O 8.0之前,oat格式文件除了机器码还包含一份dex的拷贝;但在Android O之后,dex2oat静态编译时会产生两个文件:odex, vdex,其中odex为机器码,通常很小;而vdex则是原始dex的一份拷贝,它是一个全新格式的文件(不是ELF格式)。art文件格式
魔数:art,是一个img文件,表示类对象映像;这个img文件直接被映射到ART虚拟机的堆空间中,包含了oat中的某些对象实例以及函数地址。
为保证
Dalvik和ART的兼容性,以及历史遗留问题,可能会使用相同文件后缀表示不同的文件格式,非常容易混淆。比如.dex后缀可以是dex, oat文件,odex后缀可以是odex, oat文件等。
转换流程图
Java 源文件转换为 oat 格式文件的流程图:

文件分析工具
dex文件生成及分析工具d8.bat/dx.bat:生成工具,位置路径为Windows sdk\build-tools\28.0.3。示例:d8.bat --output=test.jar Test.jar test.class;其中output必须是.zip, .jar,input可以是.dex, .apk, .jar, .class, .zip。dexdump/dexdump2:分析工具,在AOSP编译完后out/host$ cd linux-x86/bin/目录下会生成对应工具。oat文件
因为是ELF格式文件,所以通用工具都可以读出,如:readelf,AOSP编译完后生成的otadump也可以分析。vdex文件
github: vdexExtractor ,这款工具可以从vdex中提取出原始的dex文件。
示例
如下信息手机系统为 Android O 8.1 :
系统编译
framework生成文件
按照out目录生成文件路径,拷贝到手机对应路径中;在系统启动时,会拷贝一份到/data/dalvik-cache/arm64目录下。1
2
3
4
5
6
7
86.1M system/framework/arm64/boot-framework.art
25M system/framework/arm64/boot-framework.oat
20M system/framework/arm64/boot-framework.vdex
7.4M system/framework/framework.jar
// 系统启动后在 /data/dalvik-cache/arm64 目录下生成对应文件
6.0M system@framework@boot-framework.art
0 system@framework@boot-framework.oat -> /system/framework/arm64/boot-framework.oat
0 system@framework@boot-framework.vdex -> /system/framework/arm64/boot-framework.vdex系统编译应用
Email生成文件
系统应用在安装时同样会拷贝到/data/dalvik-cache/arm64目录下,但是会多生成一个classes.art文件。注意:在data目录生成的dex后缀的文件,实际上是oat文件,pull出来后魔数是.elf格式的。1
2
3
4
5
6
76.7M system/app/Email/Email.apk
76K system/app/Email/oat/arm64/Email.odex
4.1M system/app/Email/oat/arm64/Email.vdex
// 系统启动后在 /data/dalvik-cache/ 目录下生成对应文件
32K /data/dalvik-cache/arm64/system@app@Email@Email.apk@classes.art
72K /data/dalvik-cache/arm64/system@app@Email@Email.apk@classes.dex
4.0M /data/dalvik-cache/arm64/system@app@Email@Email.apk@classes.vdex安装
qsbk.apk生成的文件,apk中包含多个dex文件
第三方应用在安装时,直接安装到/data/app/目录下,并根据包名随机生成一个字符串做目录区分。注意:生成的base.odex实际上是oat文件,pull出来后魔数是.elf格式的。1
2
332M /data/app/qsbk.app-9P***/base.apk
276K /data/app/qsbk.app-9P***/oat/arm/base.odex
15M /data/app/qsbk.app-9P***/oat/arm/base.vdex小结
系统自带framework, app都会生成art, oat, vdex三种文件格式的文件;而第三方应用安装后,只会生成oat, vdex两种文件格式的文件。其中framework中后缀为.oat、系统app中后缀为dex、第三方app中后缀为odex,这三个oat, dex, odex后缀的文件,实际上都是oat文件,注意别混淆了,仅仅是后缀不同而已。
代码速查表
本文基于 Android O 8.1 源码分析 Android 平台的 ClassLoader :
1 | libcore/ojluni/src/main/java/java/lang/Class.java |
Android 类加载器
类图结构

本文不分析 SecureClassLoader, URLClassLoader 这两个类加载器。
ClassLoader:抽象类,类加载器的最终父类BootClassLoader:启动类加载器;在双亲委托模型中,是第一级父加载器BaseDexClassLoader:Android平台加载dex文件的基类PathClassLoader:系统类加载器,也相当于应用类加载器,是用户自定义类加载器默认的父加载器;APK文件都是该加载器加载的DexClassLoader:主要用于从包含classes.dex的.jar, .apk文件中加载类,而这些文件并没有随着应用一起安装InMemoryDexClassLoader:主要用于加载内存中的dex文件,而这些内存中的文件并不需要存储在本地DelegateLastClassLoader:最近委托查找策略类加载器,并不完全按照双亲委派模型来加载的,会提前执行findClass从加载dex文件中查找类,然后才是双亲委派模型
从各个博客看下来,Classloader 在 Android 不同大版本中不管是代码位置还是子类都在不停的变化。
自定义类加载器时,如果不指定父加载器,则默认其父加载器为
PathClassLoader;通过Class获取加载器时,如果其加载器为空,则指定其加载器为BootClassLoader。
双亲委派模型

类加载器的双亲委派模型:要求除了启动类加载器外,其余的类加载器都应当有自己的父加载器(父子不是继承关系,而是组合关系来复用父类代码)。双亲委派模型并不是强制要求,只是 Java 的推荐方式,可以通过重新加载方法来改变。
双亲委派模型原则:某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。Android 和标准 Java 对比:没有扩展类加载器,启动加载器 BootClassLoader 也是 Java 实现的。
父加载器和父类加载器是两个不同的概念:父加载器是参考双亲委派模型在构造方法中指定的;父类加载器表示当前加载器的父类(类图结构上是继承关系)。
ClassLoader
ClassLoader 是抽象类,其他所有的类加载都是它的继承类;有以下几个特点:
- 标准
Java中系统默认加载器为AppClassLoader;而Android中系统类加载器是PathClassLoader - 系统类加载器
PathClassLoader的父加载器为启动加载器BootClassLoader Android启动加载器BootClassLoader是ClassLoader的内部类,也是其子类;默认为顶层父加载器ClassLoader构造方法中指定父加载器,如果不指定:父加载器默认为系统加载器PathClassLoader;即:自定义类加载器,如果不指定父加载器,则其父加载器默认为PathClassLoaderloadClass实现了双亲委派模型findClass, defineClass必须在子类中实现
1 | public abstract class ClassLoader { |
从源码 loadClass 中再描述下双亲委派模型:
- 先确认类是否已经被加载
- 如果没有被加载,且父加载器不为空;使用父加载器加载。依次递归
- 父加载器为空,使用
Bootstrap来加载,但是Android中并不存在该加载器,直接返回null - 父加载器无法完成加载或者其他原因没有加载成功,则调用当前递归到的类加载器
findClass进行类加载
BootClassLoader
启动类加载器 BootClassLoader,是 ClassLoader 的成员内部类,也是 ClassLoader 的子类;是顶层父加载器。和 JVM 不一样,Android 的启动类加载器是 Java 实现的。有如下特点:
- 启动类加载器
BootClassLoader的父加载器为null,它是最顶层的加载器 - 因为是最顶层的父加载器,在
loadClass中如果发现类没有加载,直接在这一层findClass findClass是通过反射Class.classForName(它是一个native方法,标准Java中并存在) 实现的
1 | class BootClassLoader extends ClassLoader { |
Class
因为 Android 修改了标准 Java 的类加载器,所以在 Class.java 中也做了对应修改。主要有以下几个特点:
- 默认类加载器和调用者为同一个加载器
- 获取类加载器时,如果加载器为空,则指定其加载器为
BootClassLoader native方法classForName,从虚拟机中加载类
1 | public final class Class<T> implements java.io.Serializable, |
PathClassLoader
Android 的系统类加载器,也相当于应用类加载器,是用户自定义类加载器默认的父加载器;类中只有构造方法,它继承了 BaseDexClassLoader 。APK 加载时,默认使用该加载器加载到 ART 中。
1 | public class PathClassLoader extends BaseDexClassLoader { |
DexClassLoader
继承了 BaseDexClassLoader ,只有构造方法。主要用于从包含 classes.dex 的 .jar, .apk 文件中加载类,而这些文件并没有随着应用一起安装;比如放在 assert 目录下等等。
而实际上,从构造方法传递的参数来看:DexClassLoader, PathClassLoader 并没有任何区别!!
1 | public class DexClassLoader extends BaseDexClassLoader { |
InMemoryDexClassLoader
继承了 BaseDexClassLoader ,只有构造方法。主要用于加载内存中的 dex 文件,而这些内存中的文件并不需要存储在本地;方便网络下载并加载。
1 | public final class InMemoryDexClassLoader extends BaseDexClassLoader { |
DelegateLastClassLoader
DelegateLastClassLoader 继承 PathClassLoader,是最近委托查找策略;它加载类和资源的策略如下(代码中也有详细注释):
- 首先查看类是否已经被加载
- 然后使用最顶层启动类加载器
BootClassLoader来加载 - 然后使用当前类加载器
findClass,搜索与此类加载器的dexPath关联的dex文件列表中,是否已经加载 - 最后才是委托父加载器加载
从代码中可以看到,DelegateLastClassLoader 并没有完全遵循双亲委派模型。
1 | public final class DelegateLastClassLoader extends PathClassLoader { |
BaseDexClassLoader
BaseDexClassLoader 是 Android 平台加载 dex 文件的基类,所有从 dex 文件中查找加载的类 findClass 都是在这里实现。
构造方法中各参数的含义:
dexPath:包含dex文件的绝对路径列表;文件可以是apk, jar,文件列表分隔符为:optimizedDirectory:没有任何作用,为了兼容早期的版本librarySearchPath:native库所在文件目录的绝对路径列表;文件目录分隔符默认为:parent:当前类加载器的父加载器dexFiles:包含dex文件的二进制数组
1 | public class BaseDexClassLoader extends ClassLoader { |
findClass 流程图:

dex 文件加载与解析
DexFile
每个 jar, apk, dex 文件对应一个 DexFile 类实例,它用来将对应的文件加载到虚拟机中。
- 所有的
dex, jar ,apk文件,最终都是通过openDexFile加载到虚拟机中的 - 如果是通过
dex来加载类,最终会走到defineClass从虚拟机中加载
1 | public final class DexFile { |
DexPathList
内部类:
Element
可能叫DexElement更合适,但是由于历史原因,可能会存在反射调用此类的情形。每个Element对应一个DexFile文件。
1 | /*package*/ static class Element { |
NativeLibraryElement
库文件元素,每个native lib对应一个NativeLibraryElement,可能会包含系统库。
1 | /** |
DexPathList 是对两个内部类的一个封装,表示每个对象可以包含多个可执行文件 dex, jar, apk 等;同时每个对象可以包含多个库文件等。
Elements[]数组包含了所有的dex, jar, apk文件,热补丁技术通常是从这里injectfindClass最终通过DexFile从虚拟机中加载
1 | /*package*/ final class DexPathList { |
Android 预加载
系统开机启动会在 ZygoteInit 进程创建 Java 环境,预加载系统常用类,并创建系统的类加载器 PathClassLoader 。
启动路径和系统服务路径
启动路径 BOOTCLASSPATH 和系统服务路径 SYSTEMSERVERCLASSPATH 最终都是写入 ./root/init.environ.rc 文件的。
BOOTCLASSPATH
编译系统中PRODUCT_BOOT_JARS中添加的jar包名和路径对应生成的,包含所有framework相关jar包路径。PRODUCT_BOOT_JARS最终被编译添加到BOOTCLASSPATH变量中,组建过程:1
2
3
4
5./device/qcom/common/base.mk
./build/make/core/envsetup.mk
./build/make/target/product/core_minimal.mk
./device/qcom/common/common.mk
./device/qcom/custom/custom.mkSYSTEMSERVERCLASSPATH
在build/make/target/product/core_minimal.mk文件中定义的PRODUCT_SYSTEM_SERVER_JARS变量,会在system/core/rootdir/Android.mk中生成SYSTEMSERVERCLASSPATH变量。
./root/init.environ.rc 文件中这两个变量的内容为:
1 | export BOOTCLASSPATH /system/framework/com.qualcomm.qti.camera.jar:/system/framework/QPerformance.jar:/system/framework/core-oj.jar:/system/framework/core-libart.jar:/system/framework/conscrypt.jar:/system/framework/okhttp.jar:/system/framework/bouncycastle.jar:/system/framework/apache-xml.jar:/system/framework/legacy-test.jar:/system/framework/ext.jar:/system/framework/framework.jar:/system/framework/telephony-common.jar:/system/framework/voip-common.jar:/system/framework/ims-common.jar:/system/framework/org.apache.http.legacy.boot.jar:/system/framework/android.hidl.base-V1.0-java.jar:/system/framework/android.hidl.manager-V1.0-java.jar:/system/framework/qcrilhook.jar:/system/framework/hiqmi.jar:/system/framework/qcnvitems.jar:/system/framework/telephony-qmi.jar:/system/framework/tcmiface.jar:/system/framework/WfdCommon.jar:/system/framework/oem-services.jar:/system/framework/qcom.fmradio.jar:/system/framework/telephony-ext.jar |
预加载类 preloadClasses
预加载的类大概有 4500+ 个系统常用类,采用的是空间换时间的策略:系统开机时就将常用类加载,后续应用使用时不用重复加载,提高应用运行速度。
设置预加载的文件为 preloaded-classes :
AOSP 源码路径为:preloaded-classes: frameworks/base/config/preloaded-classes
编译完后会被拷贝到:./system/etc/preloaded-classes ;文件中指定需要加载的常见系统类:
1 | android.app.Activity$HostCallbacks |
预加载类流程图:

创建系统类加载器
Android 默认的系统类加载器为 PathClassLoader ,也是在系统启动阶段 ZygoteInit 中创建的,并加载系统服务的 jar 。
创建系统类加载器 PathClassLoader ,并加载系统服务 jar 文件流程图:

ZygoteInit 处理系统服务进程中,除了生成系统类加载器 PathClassLoader ,并会通过该加载器反射调用 SystemServer.main 方法,启动系统服务进程 system_server 进程,管理整个系统的所有服务。
APK 及四大组件加载过程
APK 加载过程
LoadedApk 类是整个应用 APK 加载的入口类,最终在类加载器工厂中 ClassLoaderFactory.java 创建具体的加载器 PathClassLoader 。
1 | // ClassLoaderFactory.java |
调用序列图:

部分 Log 打印:
ZygoteInit新建应用进程ActivityThread线程启动进入main入口:AMS.attachApplication启动应用以及进入Looper.loop()循环接受主线程消息AMS.attachApplication会调用ActivityThread.bindApplication启动并绑定该应用ContextImpl第一次加载应用时,调用PathClassLoader加载整个应用APK
1 | // 1. AMS.attachApplication |
Application 类加载过程
在 ActivityThread.handleBindApplication 除了加载 APK 外,还加载了 Application 类并启动这个应用;最终调用 Instrumentation.newApplication 来加载 Application 类的:
1 | // Instrumentation.java |
调用序列图:

部分 Log 打印:
1 | // Application 类加载过程 |
Activity 类加载过程
不管是在启动应用时启动主 Activity ,还是当前 Activity 打开另一个 Activity 时,最终都是通过 ActivityStackSupervisor.realStartActivityLocked 来调用 Activity.scheduleLaunchActivity 来启动指定 Activity ;而每个 Activity 是在 Instrumentation.newActivity 中来加载的:
1 | // Instrumentation.java |
调用序列图:

部分 Log 打印:
- 上半部分为启动主
Activity - 下半部分为打开另一个
Activity
1 | // 1. 启动主 Activity |
Service 类加载过程
不管 Service 是否设置新进程,也不管是 startService 还是 bindService ,系统都是在 ActivityThread.handleCreateService 中加载 Service 类的。
1 | // ActivityThread.java |
bindService 的调用序列图:

部分 Log 打印:
1 | // 1. ComtextImpl.bindService 开始进入 AMS |
静态注册 BroadcastReceiver 类加载过程
静态注册的广播接收器是在 ActivityThread.handleReceiver 中响应广播事件,并加载对应的 BroadcastReceiver 类的:
1 | // ActivityThread.java |
完整版本可以参考 四大组件 – Broadcast ,简化后的调用序列图:

部分 Log 打印:
1 | 06-23 10:47:50.276: E/System(19446): XMT, ClassLoader.loadClass, name = com.staqu.essentials.notifications.NotificationActivationReceiver, resolve = false |
Android 动态类加载
示例:动态加载 assets 目录下的 dex 文件,并反射调用指定类中的某个方法,返回值在当前 Activity 中显示。
生成 dex 文件
在 AS 中新建 Test.java 文件,编译生成对应的 Test.class,使用工具生成对应的 test.jar 文件:d8.bat --output=test.jar Test.class 。
将 Test.java, Test.class, test.jar 三个文件剪切到 assets 目录下,避免将 Test.java 打包到当前 APK 中了。
1 | package com.*.knowledge.classloading; |
当前 Activity 动态加载
- 使用异步任务
AsyncTask实现动态加载 - 避免内存泄露,定义静态内部类
LoaderAsyncTask,并使用弱引用指向当前Activity - 将
assets目录下的dex文件,拷贝到cache目录下,方便动态加载 - 使用
DexClassLoader动态加载cache目录下的dex文件,指定父加载器为null - 使用反射调用
test方法,并在当前Activity中显示结果
1 | package com.*.knowledge.classloading; |
这里需要注意下:DexClassLoader 动态加载时,指定父加载器为 null;依据双亲委派模型,只有父加载器加载失败时,才会使用当前加载器加载。
这里假定: Test.java 在当前 APK 存在,并且父加载器使用的是 PathClassLoader 。依据双亲委派模型会优先使用 PathClassLoader 加载 Test 类,而我们指定的 DexClassLoader 并不能动态加载 assets 目录下的 Test 。
示例 Log
1 | *: I/mmid(518): select timeout: wait for receiving msg |
从 Log 中可以看出,Test.test 方法是 DexClassLoader 加载的,对应路径为 /data/user/0/com.*.knowledge/cache/test.jar 。
Android 热补丁
在 Android 动态加载中,我们把 Test.java 从源码中删除了,也就是说 APK 中并不包含这个类,所以通过反射来访问的。接下来我们介绍 Android 热补丁,也就是 Test.java 在 APK 中存在,我们需要使用 assets 中的补丁替换它。
热补丁原理
Android 的类加载器在加载一个类时,先从当前加载器的 DexPathList 对象中的 Element[] dexElements 数组中,获取对应的类并加载。采用的是数组遍历的方式,遍历每一个 dex 文件;先遍历出来的是 dex 文件,先加载 class,成功加载后就不再遍历后续 dex 文件。
热修复的原理就是:将补丁 class 打包成 dex 文件后,放到 Element 数组的第一个元素,这样就能保证获取到的 class 是最新修复好的 class了。

参考 Java 类加载机制 ,对于
new TestClass()这个语句,会触发类初始化过程。而初始化分为两部分:类初始化过程<cinit>,即类加载过程的初始化阶段;类实例化过程<init>。而类一旦初始化后,后续再new只会执行实例化过程,也就是常说的类只会被加载一次,但是会实例化多次。所以热补丁必须要在类初始化<cinit>之前合入,否则不会生效。
Test 类
1 | package com.*.knowledge.classloading; |
这个 Test.java 跟随其他代码一起编译到 APK 中,而我们 assets 中的 Test.java 的方法中,返回值不一样 return "Test: from other dex file, classLoader: " ... ,后续通过 Log 和界面显示出来。
如果最终热加载成功,将显示 ... from other dex ... 而不是 ... from current APK ... 。
当前 Activity 合入热补丁
因为 DexPathList, Element 等都是包内可见,所以只能通过反射将补丁包插入到 Element[] 数组的第一个元素中。
- 获取系统加载器
PathClassLoader, DexPathList, Elements - 获取补丁包中的
DexClassLoader, DexPathList, Elements - 将补丁包的
Elements插入到当前系统加载器的Elements中 - 后续加载修复类时,会优先从补丁包中获取
1 | public class HotFixActivity extends AppCompatActivity { |
示例 Log
1 | * I/mmid: select timeout: wait for receiving msg |
从结果可以看出 Test 类是从补丁包中加载的:
- 类加载器为系统加载器
PathClassLoader,也就是它的Elements数组已经通过反射合入了热补丁 DexPathList中包含两个文件:补丁包/data/user/0/com.*.knowledge/cache/test.jar和原始的APK: /data/app/com.*.knowledge-0dRhS8t5SzW66qvx7ecm8w==/base.apk
其他
dalvikvm
类似 PC 端的 java 工具,用来指执行 apk,dex,jar 等 Android 可执行文件的,常用参数为:
-cp:指定可执行文件绝对路径-verbose:class:在logcat中输出类加载过程
1 | xmt@server005:~/$ dalvikvm --help |
下面是一个调试过程,可以看到 Android 类加载过程
java文件编写1
2
3
4
5
6public class DalvikvmTest {
public static void main(String[] args) {
System.out.println("This is DalvikvmTest.");
}
}class文件生成javac -bootclasspath C:\Users\xmt\sdk\platforms\android-28\android.jar DalvikvmTest.java,这里可以省略-bootclasspath,只有在调用了android代码时才需要。dex, jar文件生成1
C:\Users\xmt\sdk\build-tools\28.0.3\d8.bat --classpath C:\Users\xmt\sdk\platforms\android-28\android.jar --output=DalvikvmTest.jar DalvikvmTest.class
这里同样可以省略 --classpath 。
push到手机中,dalvikvm执行1
2
3adb push DalvikvmTest.jar /storage/emulated/0/test
adb shell
dalvikvm -verbose:class -cp /storage/emulated/0/test/DalvikvmTest.jar DalvikvmTest对应
log
从Log中可以清晰的看到类的加载和类初始化两个过程,默认使用的是PathClassLoader类加载器。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44/? I/dalvikvm: Adding image space took 153.906us
/? I/dalvikvm: Adding image space took 57.448us
/? I/dalvikvm: Adding image space took 71.718us
...
/? W/ADebug: Failed to get property persist.sys.media.traces
/? I/dalvikvm: Initialized class Landroid/system/OsConstants; from /system/framework/core-libart.jar
/? I/dalvikvm: Initialized class Ljava/io/FileDescriptor; from /system/framework/core-oj.jar
/? I/dalvikvm: Initialized class Ljava/net/Inet4Address; from /system/framework/core-oj.jar
/? I/dalvikvm: Initialized class Ljava/lang/System; from /system/framework/core-oj.jar
/? I/dalvikvm: Initialized class Ljava/net/Inet6Address; from /system/framework/core-oj.jar
/? I/dalvikvm: Initialized class Ljava/io/UnixFileSystem; from /system/framework/core-oj.jar
/? I/dalvikvm: Initialized class Ljava/io/File; from /system/framework/core-oj.jar
/? I/dalvikvm: Initialized class Ljava/util/regex/Pattern; from /system/framework/core-oj.jar
/? I/dalvikvm: Loaded class [Ljava/lang/ThreadLocal$ThreadLocalMap$Entry;
/? E/System: XMT, openDexFile, sourceName = /storage/emulated/0/test/DalvikvmTest.jar, outputName = null, flags = 0, classloader = dalvik.system.PathClassLoader[null]
/? E/System: java.lang.Exception
at dalvik.system.DexFile.openDexFile(DexFile.java:356)
at dalvik.system.DexFile.<init>(DexFile.java:100)
at dalvik.system.DexFile.<init>(DexFile.java:74)
at dalvik.system.DexPathList.loadDexFile(DexPathList.java:374)
at dalvik.system.DexPathList.makeDexElements(DexPathList.java:337)
at dalvik.system.DexPathList.<init>(DexPathList.java:157)
at dalvik.system.BaseDexClassLoader.<init>(BaseDexClassLoader.java:65)
at dalvik.system.PathClassLoader.<init>(PathClassLoader.java:64)
at java.lang.ClassLoader.createSystemClassLoader(ClassLoader.java:224)
at java.lang.ClassLoader.-wrap0(Unknown Source:0)
at java.lang.ClassLoader$SystemClassLoader.<clinit>(ClassLoader.java:183)
at java.lang.ClassLoader.getSystemClassLoader(ClassLoader.java:1110)
/? E/System: XMT, element: null
/? I/dalvikvm: The ClassLoaderContext is a special shared library.
/? I/dalvikvm: Registering /storage/emulated/0/test/oat/arm64/DalvikvmTest.odex
/? I/dalvikvm: Initialized class Ljava/lang/ClassLoader$SystemClassLoader; from /system/framework/core-oj.jar
// 加载 DalvikvmTest 类
/? I/dalvikvm: Loaded class LDalvikvmTest; from /storage/emulated/0/test/DalvikvmTest.jar
/? I/dalvikvm: Beginning verification for class: DalvikvmTest in /storage/emulated/0/test/DalvikvmTest.jar
/? I/dalvikvm: Class preverified status for class DalvikvmTest in /storage/emulated/0/test/DalvikvmTest.jar: 1
// DalvikvmTest 类初始化
/? I/dalvikvm: Initialized class LDalvikvmTest; from /storage/emulated/0/test/DalvikvmTest.jar
/? I/dalvikvm: Initialized class Llibcore/icu/NativeConverter; from /system/framework/core-libart.jar
/? I/dalvikvm: Initialized class Ljava/nio/charset/StandardCharsets; from /system/framework/core-oj.jar
/? I/dalvikvm: Loaded class Ljava/io/BufferedWriter; from /system/framework/core-oj.jar
/? I/dalvikvm: Beginning verification for class: java.io.BufferedWriter in /system/framework/core-oj.jar
/? I/dalvikvm: Class preverified status for class java.io.BufferedWriter in /system/framework/core-oj.jar: 1
/? I/dalvikvm: Initialized class Ljava/io/BufferedWriter; from /system/framework/core-oj.jar
AOSP libcore 编译
在调试时,希望打出类加载堆栈中调用关系,在 DexFile.java 中增加了一个 Log 打印:System.logE("XMT, ***" , new Exception()); 。修改完代码后,在 libcore 目录下编译 mm ,但是总是会出错并打印如下信息:
1 | ninja: error: 'out/host/common/obj/JAVA_LIBRARIES/junit-hostdex_intermediates/classes.jack', needed by 'out/host/common/obj/JAVA_LIBRARIES/core-test-rules-hostdex_intermediates/classes.jack', missing and no known rule to make it |
根据错误信息,需要编译 core-test-rules 模块,其实我们并不需要。在 libcore/JavaLibrary.mk 中看到这个模块是宏 LIBCORE_SKIP_TESTS 控制的,我们在 libcore/Android.mk 中注释掉这个宏,export LIBCORE_SKIP_TESTS = false ,重新编译。Java 代码的改动会更新:
1 | out/***/system/framework/core-libart.jar |
将这几个文件 push 到手机后重启生效。
小结:
- 打印
log方式:使用System.logE() - 编译方式:设置
export LIBCORE_SKIP_TESTS = false,在libcore/目录下mm
常见问题
APK 加载过程
应用中的四大组件和自定义类,编译工具会先将 Java 文件生成 .class ,然后合并成 .dex 文件,最终打包成 APK 。Android 启动 APP 时,会加载对应的 APK 文件,并将整个 APK 中相关类信息加载到虚拟机中,参考 APK 加载过程章节中的序列图。
Android 类加载时机
APK 加载后,并不会将拥有的类全部加载,只有在类初始化时才会加载,类加载时机参考 Java 类加载机制 ,大概有 5 种情况会触发。
APK中的四大组件是主动调用ClassLoader来加载,通过反射来实例化的APK中普通自定义类通过new来加载和实例化;这个过程并没有出现ClassLoader类加载器相关调用关系,怀疑是虚拟机ART中直接实现(没有看虚拟机的实现代码只是猜测)APK中如果遇到了framework中的类,会通过双亲委派模型从系统中ClassLoader.findLoadedClass查找已经加载的类- 虚拟机
ART中的底层代码,在加载应用中的类时,会在Java DexPathList.Elements数组中查找对应的apk, dex, jar等文件,并加载对应类(并没有搞清楚底层代码是怎么实现的,但是从热补丁的测试结果来看是这样的)
打印类加载过程
可以通过 dalvikvm -verbose:class 简单了解类加载过程。
几个重要的 native 方法
DexFile.openDexFile -> openDexFileNativeDexFile.defineClass -> defineClassNativeClass.classForNameClassLoader.findLoadedClass -> VMClassLoader.findLoadedClass
后续
- 热加载,插件化详细分析
ART底层代码分析
参考文档
- JIT和AOT的比较
- JVM解释器
- JIT与JVM的三种执行模式
- ART、JIT、AOT、Dalvik之间的关系
- odex文件在Dalvik和ART中不同的含义
- android 文件格式
- ELF格式的oat文件图解
- ELF文件格式解析
- jack:字节码生成工具
- API Reference: PathClassLoader
- API Reference: DexClassLoader
- API Reference: InMemoryDexClassLoader
- API Reference: DelegateLastClassLoader
- API Reference: BaseDexClassLoader
- Android动态加载Dex过程
- 热修复——深入浅出原理与实现
- ART配置
- Android类加载机制的细枝末节
- ART运行普通java程序
- Android动态加载基础 ClassLoader工作机制
- ART系统类的编译解析加载探究
- 老罗:Android运行时ART简要介绍和学习计划
- 尼古拉斯_赵四:Android中的动态加载机制
- Android自定义ClassLoader耗时问题追查
- Android Dalvikvm的使用