Android热修复入门:Android中的ClassLoader
ClassLoader简介
对于Java程序来说,编写程序就是编写类,运行程序也就是运行类(编译得到的class文件),其中起到关键作用的就是类加载器ClassLoader。
任何一个Java程序都是若干个class文件组成的一个完整的Java程序,在程序运行的时候,需要将class文件加载到JVM中才可以使用后,负责加载这些class文件的就是Java的类加载(ClassLoader)机制。
因此ClassLoader的作用简单来说就是加载class文件,提供给程序运行时使用。
ClassLoader的双亲委托模型
先看jdk中的ClassLoader类的构造方法,其需要传入一个父类加载器,并持有该引用:
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}
private static Void checkCreateClassLoader() {
return null;
}
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
}
当类加载器收到加载类或资源的请求时,通常都是先委托给父类加载器加载,也就是说只有当父类加载器找不到指定类或资源的时候,自身才会执行实际类加载过程:
- 源ClassLoader先判断该Class是否已加载,如果已加载,则直接返回Class,如果没有则委托给父类加载器
- 父类加载器判断是否加载过该Class,如果已加载,则直接返回Class,如果没有则委托给祖父类加载器。
- 依次类推,直到始祖类加载器(引用类加载器)。
- 始祖类加载器判断是否加载过该Class,如果已加载,则直接返回Class,如果没有则尝试从其对应的类路径下寻找class字节码文件并载入。如果加入成功,则直接返回class,如果载入失败,则委托给始祖类加载器的子类加载器。
- 始祖类加载器的子类加载器尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则直接返回Class,如果载入失败,则委托给始祖类加载器的孙类加载器。
- 依次类推,知道源ClassLoader。
- 源ClassLoader尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则直接返回Class,如果载入失败,源ClassLoader不会再委托其子类加载器,而是抛出异常。
Android中的ClassLoader
Android的Dalvik/ART虚拟机图通标准Java的虚拟机一样,也是同样需要加载class文件到内存中来使用,但是在ClassLoader的加载细节上会有略微的差别。
Android应用打包成apk文件时,class文件会被打包成一个或者多个dex文件,将一个apk文件后缀改成.zip格式解压后(也可以直接解压,apk文件本质是个zip文件),里面就有class.dex文件,由于Android的65K问题,使用MultiDex就会生成多个dex文件。
当Android系统安装一个应用的时候,会针对不同平台对Dex进行优化,这个过程由一个专门的工具来处理,叫DexOpt。DexOpt是在第一次加载Dex文件的时候执行的,该过程会生成一个ODEX文件,即Optimised Dex。执行ODEX的效率会比直接执行Dex文件的效率高很多,加快App的启动和响应。
更多可以参考:
http://www.mywiki.cn/hovercool/index.php/ART和Dalvik
https://www.jianshu.com/p/242abfb7eb7f
总之,Android的Dalvik/ART无法向JVM那样直接加载class文件的jar文件中的class,需要通过dx工具来优化转换成Dalvik byte code才行,只能那个通过dex或者包含dex的jar,apk文件来加载(注意odex文件后缀肯呢个会是.dex或.odex,也属于dex文件),因此Android中的ClassLoader工作就交给了BaseDexClassLoader来处理。
注:如果 jar 文件包含有 dex 文件,此时 jar 文件也是可以用来加载的,不过实际加载的还是其中的 dex 文件,不要弄混淆了。
BaseDexClassLoader及其子类
ClassLoader是一个抽象类,其具体实现的子类有BaseDexClassLoader和SecureClassLoader。
SercureClassLoader的子类是URLClassLoader,其只能用来加载jar文件,在Android的Dalvik/ART上没发使用。
BaseDexClassLoader的子类是PathClassLoader和DexClassLoader。
PathDexClassLoader
PathClassLoader在应用启动时创建,从data/app/…安装目录下加载apk文件。
其有两个构造函数:遵循双亲委托模型
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
- dexPath:包含dex文件的jar文件或者apk文件的路径集,多个以文件分隔符分隔,默认是“:”
- libraryPath:包含C/C++库的路径集,多个同样以文件分隔符分隔,可以为空。
PathClassLoader里面除了这两个构造方法以外就没有其他的代码了,具体的实现都是在BaseClassLoader里面,其dexParh比较受限制,一般是已经安装应用的apk文件路径。
在Android中,App安装到手机后,apk里面的class.dex中的class均是PathClassLoader来加载的。
我们可以来看一下:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ClassLoader loader = MainActivity.class.getClassLoader();
while (loader != null) {
System.out.println(loader.toString());
loader = loader.getParent();
}
}
}
输出:
I/System.out: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.asus1.rexiufu-aF8D1nfuAzND1R3FxXGMDA==/base.apk"],nativeLibraryDirectories=[/data/app/com.example.asus1.rexiufu-aF8D1nfuAzND1R3FxXGMDA==/lib/arm64, /system/lib64, /vendor/lib64, /product/lib64]]]
I/System.out: java.lang.BootClassLoader@cfe4423
/data/app/com.example.asus1.rexiufu-aF8D1nfuAzND1R3FxXGMDA==/base.apk就是示例应用安装在上机上的位置。
BootClassLoader是PathClassLoader的父加载器,其在系统启动时创建,在App启动时会将该对象传进来,具体的调用在com.android.internal.os.ZygoteInit
的 main()
方法中调用了 preload()
, 然后调用 preloadClasses()
方法,在该方法内部调用了 Class 的 forName() 方法:
Class.forName(line, true, null);
在forName方法内部获取到BootClassLoader实例:
public static Class<?> forName(String className, boolean shouldInitialize,
ClassLoader classLoader) throws ClassNotFoundException {
if (classLoader == null) {
classLoader = BootClassLoader.getInstance();
}
// Catch an Exception thrown by the underlying native code. It wraps
// up everything inside a ClassNotFoundException, even if e.g. an
// Error occurred during initialization. This as a workaround for
// an ExceptionInInitializerError that's also wrapped. It is actually
// expected to be thrown. Maybe the same goes for other errors.
// Not wrapping up all the errors will break android though.
Class<?> result;
try {
result = classForName(className, shouldInitialize, classLoader);
} catch (ClassNotFoundException e) {
Throwable cause = e.getCause();
if (cause instanceof LinkageError) {
throw (LinkageError) cause;
}
throw e;
}
return result;
}
而PathClassLoader的实例化又是在哪里进行呢?
其中:
- 在Zygotelnit中调用的是用来启动相关的系统服务
- 在ApplicationLoaders中用来加载系统安装过的apk,用来加载apk内的class,其调用时在LoadApk类中的
getClassLoader()
方法中调用的,得到的就是PathClassLoader:
mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip, lib,
mBaseClassLoader);
DexClassLoader
A class loader that loads classes from .jar and .apk files containing a classes.dex entry. This can be used to execute code not installed as part of an application.
对比PahtClassLoader只能加载已经安装应用的dex或apl文件,DexClassLoader则没有此限制,可以从SD卡上加载包含class.dex的jar和apk文件,这也就是插件化和热修复的基础,在不需要安装应用的情况下,完成需要使用的dex的加载。
DexClassLoader的源码里面只有一个构造方法,这里也是遵循双亲委托模型:
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
参数说明:
-
String dexPath : 包含 class.dex 的 apk、jar 文件路径 ,多个用文件分隔符(默认是 :)分隔
-
String optimizedDirectory : 用来缓存优化的 dex 文件的路径,即从 apk 或 jar 文件中提取出来的 dex 文件。该路径不可以为空,且应该是应用私有的,有读写权限的路径(实际上也可以使用外部存储空间,但是这样的话就存在代码注入的风险),可以通过以下方式来创建一个这样的路径:
File dexOutputDir = context.getCodeCacheDir();
-
String libraryPath : 存储 C/C++ 库文件的路径集
-
ClassLoader parent : 父类加载器,遵从双亲委托模型
其实PathClassLoader和DexClassLoader都是只是对BaseClassLoader的一层简单的封装,真正的实现都在BaseClassLoader。
BaseClassLoader源码分析
先看一下它的结构:
其中有个重要的字段,pathList,其继承ClassLoader实现的findClass(),findResource()均是基于pathList来实现的:
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
...
return c;
}
@Override
protected URL findResource(String name) {
return pathList.findResource(name);
}
@Override
protected Enumeration<URL> findResources(String name) {
return pathList.findResources(name);
}
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
具体实现就在DexPathList里面了,DexPathList的构造方法也比较简单:
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
...
}
接手之前传进来的包含dex的apk/jar/dex的路径集、native库的路径集和缓存优化的dex文件的路径,然后调用makePathElements()方法生成一个Element[ ] dexElements数组,Element是DexPathList的一个嵌套类:
static class Element {
private final File dir;
private final boolean isDirectory;
private final File zip;
private final DexFile dexFile;
private ZipFile zipFile;
private boolean initialized;
}
那么makePathElements()是如何生成Element数组呢?
private static Element[] makePathElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions) {
List<Element> elements = new ArrayList<>();
// 遍历所有的包含 dex 的文件
for (File file : files) {
File zip = null;
File dir = new File("");
DexFile dex = null;
String path = file.getPath();
String name = file.getName();
// 判断是不是 zip 类型
if (path.contains(zipSeparator)) {
String split[] = path.split(zipSeparator, 2);
zip = new File(split[0]);
dir = new File(split[1]);
} else if (file.isDirectory()) {
// 如果是文件夹,则直接添加 Element,这个一般是用来处理 native 库和资源文件
elements.add(new Element(file, true, null, null));
} else if (file.isFile()) {
// 直接是 .dex 文件,而不是 zip/jar 文件(apk 归为 zip),则直接加载 dex 文件
if (name.endsWith(DEX_SUFFIX)) {
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
} else {
// 如果是 zip/jar 文件(apk 归为 zip),则将 file 值赋给 zip 字段,再加载 dex 文件
zip = file;
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException suppressed) {
suppressedExceptions.add(suppressed);
}
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(dir, false, zip, dex));
}
}
// list 转为数组
return elements.toArray(new Element[elements.size()]);
}
loadDexFile() 方法最终会调用 JNI 层的方法来读取 dex 文件 ,有兴趣的可以阅读 https://blog.****.net/nanzhiwen666/article/details/50515895 这篇文章深入了解。
接下来看findClass方法:其根据传入的完整的类名来加载对应的class:
public Class findClass(String name, List<Throwable> suppressed) {
// 遍历 dexElements 数组,依次寻找对应的 class,一旦找到就终止遍历
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
// 抛出异常
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
这里有关于修复实现的一个点,就是将补丁dex文件放到dexElements数组前面,这样在加载class的时候,优先找到补丁包中的dex文件,加载到class之后就不再寻找了,从而原来的apk文件中同名的类就不会再使用,从而达到修复的目的。虽然说起来比较简单,但是实现起来还是有很多细节需要注意。
至此,BaseDexClassLader 寻找 class 的路线就清晰了:
- 当传入一个完整的类名,调用 BaseDexClassLader 的 findClass(String name) 方法
- BaseDexClassLader 的 findClass 方法会交给 DexPathList 的 findClass(String name, List suppressed 方法处理
- 在 DexPathList 方法的内部,会遍历 dexFile ,通过 DexFile 的 dex.loadClassBinaryName(name, definingContext, suppressed) 来完成类的加载
需要注意的是,在项目中使用BaseDexLoader或者DexClassLoader去加载某个dex或者apk中的class的时候,是无法调用findClass()方法的,因为该方法是包访问权限,我们需要调用loadClass(),该方法其实是BaseDexClassLoader的父类ClassLoader内实现的:
public Class<?> loadClass(String className) throws ClassNotFoundException {
return loadClass(className, false);
}
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
ClassNotFoundException suppressed = null;
try {
clazz = parent.loadClass(className, false);
} catch (ClassNotFoundException e) {
suppressed = e;
}
if (clazz == null) {
try {
clazz = findClass(className);
} catch (ClassNotFoundException e) {
e.addSuppressed(suppressed);
throw e;
}
}
}
return clazz;
}
上面这段代码结合之前提到的双亲委托模型就很好理解了,先查找当前的 ClassLoader 是否已经加载过,如果没有就交给父 ClassLoader 去加载,如果父 ClassLoader 没有找到,才调用当前 ClassLoader 来加载,此时就是调用上面分析的 findClass() 方法了。
ClassLoader使用
使用dx命令创建一个dex文件,然后放到你的手机里面,然后执行下面的代码:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
File dexFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
.getPath()+File.separator+"TestClass.dex");
if(!dexFile.exists()){
return;
}
DexClassLoader dexClassLoader = new DexClassLoader(dexFile.getAbsolutePath()
,getExternalCacheDir().getAbsolutePath(),null,getClassLoader());
try {
Class clazz = dexClassLoader.
loadClass("com.example.asus1.rexiufu.TestClass");
TestClass testClass = (TestClass)clazz.newInstance();
System.out.println(testClass.showToast());
}catch (ClassNotFoundException e){
e.printStackTrace();
}catch (IllegalAccessException e){
e.printStackTrace();
}catch (InstantiationException e){
e.printStackTrace();
}
}
});
输出
I/System.out: Hello,Android!
转载自:
https://jaeger.itscoder.com/android/2016/08/27/android-classloader.html