Android PathClassLoader 和 DexClassLoader 区别之源码分析

前言

一般说起 PathClassLoader 和 DexClassLoader ,大家都会说,前者只能加载内存中已经安装的apk中的dex,而后者可以加载sd卡中的apk/jar ,因此 DexClassLoader 是热修复和插件化的基础。但是具体为什么DexClassLoader能加载sd卡中的类,很多文章都只是一笔带过 ,于是研究了下源码,做个记录。

【注意】本文所参考的源码基于Android 7.1.2_r36 及以前版本,在Android 8.0.0_r4 之后,BaseDexClassLoader 、DexClassLoader 源码有所变动

PathClassLoader

//类路径:/libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
    super(dexPath, null, librarySearchPath, parent);
}

DexClassLoader

//类路径:/libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java
 public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
    super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}

可以看到,两者都是继承自BaseDexClassLoader ,构造方法的具体逻辑在父类中实现,唯一不同的是一个参数:optimizedDirectory

BaseDexClassLoader 构造方法

//类路径: /libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
    super(parent);
    this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}

BaseDexClassLoader的构造方法,只是用这些参数,创建了一个DexPathList的实例,DexPathList 使用 Element 数组存储了所有的 dex 信息,在 dex 被存到 Element 数组后,所有的类都会在 Element 数组中寻找,不会再次从文件中加载

//类路径:/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
 @Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    //从pathList的Element数组中找类,找不到就报ClassNotFoundException
    Class c = pathList.findClass(name, suppressedExceptions);
    if (c == null) {
        ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
        for (Throwable t : suppressedExceptions) {
            cnfe.addSuppressed(t);
        }
        throw cnfe;
    }
    return c;
}

DexPathList

DexPathList 通过 makeDexElements 得到了一个 Element[]类型的 dexElements对象数组,里面存放了app的所有dex相关信息。这里仍然看不出 PathClassLoader 和 DexClassLoader 的具体差别,只知道前者参数中的 optimizedDirectory 传的是 null 。

//类路径:/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
public DexPathList(ClassLoader definingContext, String dexPath,
            String librarySearchPath, File optimizedDirectory) {
    //...略
    this.definingContext = definingContext;
    ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
    // save dexPath for BaseDexClassLoader
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions,definingContext);
    //...略
}

makeElements 中,使用for,循环调用了 loadDexFile 来加载 dexPath 中每个目录中的 dex

 dex = loadDexFile(file, optimizedDirectory, loader, elements);

再看 loadDexFile 函数:

//类路径:/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
                                       Element[] elements)
            throws IOException {
        if (optimizedDirectory == null) {
            return new DexFile(file, loader, elements);
        } else {
            String optimizedPath = optimizedPathFor(file, optimizedDirectory);
            return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
        }
    }

根据 optimizedDirectory 参数是否为空,执行的方法不同,PathClassLoader 执行的是 new DexFile(), 而 DexClassLoader 执行的是 DexFile.loadDex() , 然而 loadDex 最终也还是会调用 new DexFile() 来创建实例。

DexFile

//类路径:/libcore/dalvik/src/main/java/dalvik/system/DexFile.java
static DexFile loadDex(String sourcePathName, String outputPathName,
        int flags, ClassLoader loader, DexPathList.Element[] elements) throws IOException {
    return new DexFile(sourcePathName, outputPathName, flags, loader, elements);
}
private DexFile(String sourceName, String outputName, int flags, ClassLoader loader,DexPathList.Element[] elements) throws IOException {
        mCookie = openDexFile(sourceName, outputName, flags, loader, elements);
        mFileName = sourceName;
}
private static Object openDexFile(String sourceName, String outputName, int flags,
            ClassLoader loader, DexPathList.Element[] elements) throws IOException {
    return openDexFileNative(new File(sourceName).getAbsolutePath(),
                                 (outputName == null)
                                     ? null
                                     : new File(outputName).getAbsolutePath(),
                                 flags,
                                 loader,
                                 elements);
}
private static native Object openDexFileNative(String sourceName, String outputName, int flags,
            ClassLoader loader, DexPathList.Element[] elements);

在 DexFile 的构造方法中,调用了 openDexFile 去生成一个 mCookie,可以看到,不管是哪个类型的ClassLoader,最终都会调用 native 方法 openDexFileNative 来实现具体的加载逻辑。

Native 层的源码追踪

DexFile_openDexFileNative
static jint DexFile_openDexFileNative(JNIEnv* env, jclass, jstring javaSourceName, jstring javaOutputName, jint) {    
    //...略
    const DexFile* dex_file;    
    if (outputName.c_str() == NULL) {// 如果outputName为空,则dex_file由sourceName确定
        dex_file = linker->FindDexFileInOatFileFromDexLocation(dex_location, dex_location_checksum); 
    } else {// 如果outputName不为空,则在outputName目录中去寻找dex_file
        std::string oat_location(outputName.c_str());    
        dex_file = linker->FindOrCreateOatFileForDexLocation(dex_location, dex_location_checksum, oat_location);  
    }    
    //...略
    return static_cast<jint>(reinterpret_cast<uintptr_t>(dex_file));    
}

判断传入的 outputName 是否为空,分别执行不同的方法,这个 outputName 就是 BaseDexClassLoader 构造方法中传入的 optimizedDirectory 参数,辗转来到这里。

  • 当 outputName 不为空时【DexClassLoader】
    执行FindOrCreateOatFileForDexLocation函数,通过 outputName拿到 oat_location ,然后尝试调用 FindDexFileInOatLocation 从 oat_location 中寻找到 dex ,这就是我们经常用到到热修复的原理了,通过在sd卡中存放新的补丁dex/jar/apk替代旧的,来实现更新。
const DexFile* ClassLinker::FindOrCreateOatFileForDexLocation(const std::string& dex_location,uint32_t dex_location_checksum,const std::string& oat_location) {
    WriterMutexLock mu(Thread::Current(), dex_lock_);   // 互锁
    return FindOrCreateOatFileForDexLocationLocked(dex_location, dex_location_checksum, oat_location);
}

const DexFile* ClassLinker::FindOrCreateOatFileForDexLocationLocked(const std::string& dex_location,uint32_t dex_location_checksum,const std::string& oat_location) {
    const DexFile* dex_file = FindDexFileInOatLocation(dex_location,dex_location_checksum,oat_location);
    if (dex_file != NULL) {
        // 如果顺利打开,则返回
        return dex_file;
    }
    const OatFile* oat_file = OatFile::Open(oat_location, oat_location, NULL,!Runtime::Current()->IsCompiler());
    if (oat_file == NULL) {
        return NULL;
    }
    const OatFile::OatDexFile* oat_dex_file = oat_file->GetOatDexFile(dex_location, &dex_location_checksum);
    if (oat_dex_file == NULL) {
        return NULL;
    }
    const DexFile* result = oat_dex_file->OpenDexFile();
    return result;
}
  • 当 outputName 为空时【PathClassLoader】
    执行 FindDexFileInOatFileFromDexLocation 函数,从 dex_location 中拿到 dex 文件,这个 dex_location 也就是 BaseDexClassLoader 的 dexPath 参数中分割出来的某个存放文件的路径。在 Android 中,系统使用 PathClassLoader 来加载apk中的dex存放到Element数组中,因此apk中的classes.dex都是通过它来加载的。

总结

Android 中,apk 安装时,系统会使用 PathClassLoader 来加载apk文件中的dex,PathClassLoader的构造方法中,调用父类的构造方法,实例化出一个 DexPathList ,DexPathList 通过 makePathElements 在所有传入的dexPath 路径中,找到DexFile,存入 Element 数组,在应用启动后,所有的类都在 Element 数组中寻找,不会再次加载。

在热更新时,实现 DexClassLoader 子类,传入要更新的dex/apk/jar补丁文件路径(如sd卡路径中存放的patch.jar),通过反射拿到 DexPathList,得到补丁 Element 数组,再从Apk原本安装时使用的 PathClassLoader 中拿到旧版本的 Element 数组,合并新旧数组,将补丁放在数组最前面,这样一个类一旦在补丁 Element 中找到,就不会再次加载,这样就能替换旧 Element 中的旧类,实现热更新。

参考资料

http://androidxref.com/7.1.2_r36/
https://jaeger.itscoder.com/android/2016/08/27/android-classloader.html
https://blog.csdn.net/zhoushishang/article/details/38703623

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值