介绍 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
虚拟机实现有两个阶段:
Dalvik
Android K 4.4
之前的版本都是使用的Dalvik
虚拟机。Java
生成的class
文件由dx
工具转换为Dalvik
字节码即dex
文件,之后进行签名对齐等操作变成APK
文件。而Dalvik
虚拟机负责将dex
字节码解释为机器码,解释执行的速度慢效率低;从Android 2.2 froyo
开始引入JIT
动态编译执行方式,APP
运行时,JIT
将执行次数较多的dex
文件动态编译为机器码缓存起来,后续再次执行时大大提高运行速度。JIT
动态编译的特点是:每次打开APP
运行时,都需要重新编译。ART: Android Runtime
Android 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
;即:自定义类加载器,如果不指定父加载器,则其父加载器默认为PathClassLoader
loadClass
实现了双亲委派模型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
文件,热补丁技术通常是从这里inject
findClass
最终通过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 -> openDexFileNative
DexFile.defineClass -> defineClassNative
Class.classForName
ClassLoader.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的使用