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;
    }

当类加载器收到加载类或资源的请求时,通常都是先委托给父类加载器加载,也就是说只有当父类加载器找不到指定类或资源的时候,自身才会执行实际类加载过程:

  1. 源ClassLoader先判断该Class是否已加载,如果已加载,则直接返回Class,如果没有则委托给父类加载器
  2. 父类加载器判断是否加载过该Class,如果已加载,则直接返回Class,如果没有则委托给祖父类加载器。
  3. 依次类推,直到始祖类加载器(引用类加载器)。
  4. 始祖类加载器判断是否加载过该Class,如果已加载,则直接返回Class,如果没有则尝试从其对应的类路径下寻找class字节码文件并载入。如果加入成功,则直接返回class,如果载入失败,则委托给始祖类加载器的子类加载器。
  5. 始祖类加载器的子类加载器尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则直接返回Class,如果载入失败,则委托给始祖类加载器的孙类加载器。
  6. 依次类推,知道源ClassLoader。
  7. 源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.ZygoteInitmain() 方法中调用了 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.csdn.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

猜你喜欢

转载自blog.csdn.net/qq_36391075/article/details/83042904