安卓类加载机制


一、ClassLoader介绍

任何一个 Java 程序都是由一个或多个 class 文件组成,在程序运行时,需要将 class 文件加载到 JVM 中才可以使用,负责加载这些 class 文件的就是 Java 的类加载机制。ClassLoader 的作用简单来说就是加载 class 文件,提供给程序运行时使用。每个 Class 对象的内部都有一个 classLoader 字段来标识自己是由哪个 ClassLoader 加载的。

BootClassLoader
用于加载Android Frameworkclass文件。
class Class<T> {
...
private transient ClassLoader classLoader;
...
}

ClassLoader是一个抽象类,而它的具体实现类主要有:
在这里插入图片描述

  • BootClassLoader
    用于加载Android Framework层class文件。
  • PathClassLoader
    用于Android应用程序类加载器。可以加载指定的dex,以及jar、zip、apk中的classes.dex
  • DexClassLoader
    用于加载指定的dex,以及jar、zip、apk中的classes.dex

很多博客里说PathClassLoader只能加载已安装的apk的dex,其实这说的应该是在dalvik虚拟机上。但现在一般不用关心dalvik了。

示例

Log.e(TAG, "Activity.class 由:" + Activity.class.getClassLoader() +" 加载");
Log.e(TAG, "MainActivity.class 由:" + getClassLoader() +" 加载");

输出:

com.MyStudy.HomeScreen E Activity.class 由:java.lang.BootClassLoader@d7628d5 加载
com.MyStudy.HomeScreen E MainActivity.class 由:dalvik.system.PathClassLoader[DexPathList[[zip file “/data/app/com.MyStudy.HomeScreen-GRRgDXx9OKqZObuWM0RBgg==/base.apk”],nativeLibraryDirectories=[/data/app/com.MyStudy.HomeScreen-GRRgDXx9OKqZObuWM0RBgg==/lib/x86, /system/lib, /system/product/lib]]] 加载

它们之间的关系如下:

在这里插入图片描述

PathClassLoader 与 DexClassLoader 的共同父类是 BaseDexClassLoader 。

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
    }
}

public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
           super(dexPath, null, librarySearchPath, parent);
    }
}

可以看到两者唯一的区别在于:创建 DexClassLoader 需要传递一个 optimizedDirectory 参数,并且会将其创建为 File 对象传给 super ,而 PathClassLoader 则直接给到null。因此两者都可以加载指定的dex,以及jar、zip、apk中的classes.dex

PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/xx.dex", getClassLoader());
File dexOutputDir = context.getCodeCacheDir();
DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/xx.dex",dexOutputDir.getAbsolutePath(),null,getClassLoader());

其实, optimizedDirectory 参数就是dexopt的产出目录(odex)。那 PathClassLoader 创建时,这个目录为null,就意味着不进行dexopt?并不是, optimizedDirectory 为null时的默认路径为:/data/dalvik-cache。

在API 26源码中,将DexClassLoader的optimizedDirectory标记为了 deprecated 弃用,实现也变为了:

public DexClassLoader(String dexPath, String optimizedDirectory,String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
......PathClassLoader一模一样了!

二、双亲委托机制

可以看到创建 ClassLoader 需要接收一个 ClassLoader parent 参数。这个 parent 的目的就在于实现类加载的双亲委托。即:

某个类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

优点:

  • 1、 避免重复加载, 当父加载器已经加载了该类的时候, 就没有必要子ClassLoader再加载一次。
  • 2、 安全性考虑, 防止核心API库被随意篡改。
  protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized(this.getClassLoadingLock(name)) {
        	// 检查class是否有被加载
            Class<?> c = this.findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();

                try {
                    if (this.parent != null) {
                    //如果parent不为null,则调用parent的loadClass进行加载
                        c = this.parent.loadClass(name, false);
                    } else {
                    //parent为null,则调用BootClassLoader进行加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException var10) {
                }

                if (c == null) {
                // 如果都找不到就自己查找
                    long t1 = System.nanoTime();
                    c = this.findClass(name);
                    PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    PerfCounter.getFindClasses().increment();
                }
            }

            if (resolve) {
                this.resolveClass(c);
            }
            return c;
        }
    }

1、 首先调用findLoadedClass检查传入的类是否已经被加载,如果已经加载那么就直接返回。
2、 如果第一步中类没有被加载(c == null),那么就会判断parent是否等于null,也就是判断父加载器是否存在,如果父加载器存在,就调用父加载器的loadClass方法。
3、 如果父加载器不存在就会调用findBootstrapClassOrNull,这个方法会直接返回null。
4、 如果到了第4步依然c == null,那么表示在向上委托的过程中,没有加载该类,会调用findClass继续向下进行查找。

三、类的加载过程

可以看到在所有父ClassLoader无法加载Class时,则会调用自己的findClass()方法。其实任何ClassLoader子类,都可以重写loadClass()与findClass() 。一般如果你不想使用双亲委托,则重写loadClass()修改其实现。而重写findClass()则表示在双亲委托下,父ClassLoader都找不到Class的情况下,定义自己如何去查找一个Class。如果没有重写的话,那findClass()是在BaseDexClassLoader中实现的。我们来看一下findClass()中的实现逻辑。
BaseDexClassLoader.java

    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
		//查找指定的class
        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;
    }

这里省略了部分代码,可以看到调用了pathList的findClass()方法,pathList就是DexPathList对象,在BaseDexClassLoader初始化的时候被创建。接下来进入DexPathList。

public DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory) {
//.........
// splitDexPath 实现为返回 List<File>.add(dexPath)
// makeDexElements 会去 List<File>.add(dexPath) 中使用DexFile加载dex文件返回 Element数组
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext);
//.........
}

可以看到DexPathList在创建的时候调用了makeDexElements()方法来创建出了dexElements数组,在makeDexElements之前我们先来看一下splitDexPath()方法,在这个方法中将dexPath目录下的所有程序文件转变成一个File集合,而且dexPath是一个用冒号作为分隔符把多个程序文件目录拼接起来的字符串,如 /data/dexdir1:/data/dexdir2:…。makePathElements方法核心作用就是将指定路径中的所有文件转化成DexFile同时存储到到Element[]这个数组中。接下来进入DexPathList的findClass()方法中。

    public Class<?> findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            Class<?> clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }

        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

在findClass()方法中会通过for循环不断的遍历dexElement数组,拿到element,然后调用element的findClass(),Element是DexPathList的内部类,dexElements是维护dex文件的数组, 每一个Element对应一个dex文件。DexPathList遍历dexElements,从每一个dex文件中查找目标类,在找到后即返回并停止遍历。继续往下看,进入element的findClass()方法。

        public Class<?> findClass(String name, ClassLoader definingContext,
                List<Throwable> suppressed) {
            return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
                    : null;
        }

如果dexFile不等于空,就去查找类名与name相同的类,否则返回null,dexFile就是用来描述dex文件的,Dex的加载以及Class的查找,都是由该类调用它的native方法完成的。

在这里插入图片描述

四、总结

安卓应用程序的类加载机制在很大程度上遵循了Java的类加载机制,包括双亲委托机制。双亲委托机制是一种类加载器的工作方式,它要求除了顶层的启动类加载器外,每个类加载器在尝试加载某个类时,都会先委托给它的父类加载器去尝试加载,只有在父类加载器无法加载的情况下,才由当前类加载器自行加载。

Android应用程序中的类加载器主要包括以下几种:

PathClassLoader:用于加载APK中的dex文件,是Android应用程序默认的类加载器。
DexClassLoader:用于动态加载dex文件,允许在程序运行时加载dex文件中的类。

为什么使用双亲委托机制:

  • 避免类重复加载:通过双亲委托机制,父类加载器在加载类时会先查看是否已经被加载过,如果已经加载过就不会重新加载,避免了类的重复加载。
  • 保证类加载的一致性:通过双亲委托机制,每个类加载器只加载自己负责的范围内的类,而不会影响其他类加载器,从而保证了类加载的一致性。
  • 安全性:双亲委托机制可以保证核心类库不会被恶意篡改,提高了系统的安全性。
  • 模块化设计:通过双亲委托机制,不同的类加载器可以按照父子关系构建成层级结构,实现了模块化的设计,提高了代码的可维护性和扩展性。

双亲委托机制能够有效地提高类加载的效率、保证类加载的一致性和安全性、以及实现模块化的设计。在Android应用程序中也采用双亲委托机制,这样设计有助于提高应用的性能、安全性和可维护性。

参考链接:
android中的ClassLoader

  • 24
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值