Android类加载器与Java类加载器的对比

5,360 阅读7分钟

目录结构

什么是类加载器?
Java类加载器
    BootstrapClassLoader(启动类加载器)
    ExtensionClassLoader(扩展类加载器)
    ApplicaitonClassLoader(也叫SystemClassLoader,应用程序类加载器)
    Java类加载器---双亲委派模型
    工作原理
    源码
    ClassLoader中几个比较重要的方法
    双亲委派模式作用
    自定义Java类加载器
android类加载器
    JVM与android虚拟机区别
    android类加载机制与源码解析
    android类加载机制总结
    使用DexClassLoader动态加载dex的简单例子
android类加载器与java类加载器异同
参考资料

什么是类加载器?

类加载器(Class Loader):顾名思义,指的是可以加载类的工具。前面JVM类加载机制——类的生命周期已经介绍过类加载的5个过程,即:加载、验证、准备、解析、初始化,而类加载器的任务是根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例,在虚拟机提供了3种类加载器,启动(Bootstrap)类加载器、扩展(Extension)类加载器、系统(System)类加载器(也称应用类加载器),下面分别介绍。

Java类加载器

BootstrapClassLoader(启动类加载器)

启动类加载器主要加载的是JVM自身需要的类,这个类加载使用 C++语言实现 (这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的)的,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。

ExtensionClassLoader(扩展类加载器)

扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类由Java语言实现的,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类,开发者可以直接使用标准扩展类加载器。

ApplicaitonClassLoader(也叫SystemClassLoader,应用程序类加载器)

也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath或-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader() 方法可以获取到该类加载器。

  在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,需要注意的是,Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式即把请求交由父类处理,它一种任务委派模式,下面我们进一步了解它。

Java类加载器---双亲委派模型

双亲委派模式是在Java 1.2后引入的,类加载器间的关系如下

注意:双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码(“父类加载器”不能理解为“父类,加载器”,而应该理解为“父,类加载器”,)

  • 启动类加载器,由C++实现,没有父类。

  • 拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null

  • 系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader

  • 自定义类加载器,父类加载器为AppClassLoader。

下面我们通过程序来验证上述阐述的观点:


public class ClassLoaderTest {

    public static void main(String[] args) throws ClassNotFoundException {
       
			 FileClassLoader loader1 = new FileClassLoader("xxx/path");
			
			  System.out.println("自定义类加载器的父加载器: "+loader1.getParent());
			  System.out.println("系统默认的AppClassLoader: "+ClassLoader.getSystemClassLoader());
			  System.out.println("AppClassLoader的父类加载器: "+ClassLoader.getSystemClassLoader().getParent());
			  System.out.println("ExtClassLoader的父类加载器: "+ClassLoader.getSystemClassLoader().getParent().getParent());
    }
}

class FileClassLoader extends  ClassLoader{
    private String rootDir;
    
    public FileClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }
    // 编写获取类的字节码并创建class对象的逻辑
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //...省略逻辑代码
        return null;
    }
    //编写读取字节流的方法
    private byte[] getClassData(String className) {
        // 读取类文件的字节
        //省略代码....
        return null;
    }
}

输出结果:

自定义类加载器的父加载器: sun.misc.Launcher$AppClassLoader@18b4aac2
系统默认的AppClassLoader: sun.misc.Launcher$AppClassLoader@18b4aac2
AppClassLoader的父类加载器: sun.misc.Launcher$ExtClassLoader@6bc7c054
ExtClassLoader的父类加载器: null

工作原理

如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式(接下来的源码可以看出这个流程),即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成,这不就是传说中的实力坑爹?

源码

  • 相关类结构图

有几个注意点可以帮助你阅读源码:

  1. 拓展类加载器ExtClassLoader和系统类加载器AppClassLoader,这两个类都继承自URLClassLoader,是sun.misc.Launcher的静态内部类。
  2. sun.misc.Launcher主要被系统用于启动主应用程序,ExtClassLoader和AppClassLoader都是由sun.misc.Launcher创建的
  3. 顶层的类加载器是ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器)
ClassLoader中几个比较重要的方法
  • loadClass(String)

该方法加载指定名称(包括包名)的二进制类型,该方法在JDK1.2之后不再建议用户重写但用户可以直接调用该方法,loadClass()方法是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派模式的实现,其源码如下,loadClass(String name, boolean resolve)是一个重载方法,resolve参数代表是否生成class对象的同时进行解析相关操作。

protected Class<?> loadClass(String name, boolean resolve)
      throws ClassNotFoundException
  {
      synchronized (getClassLoadingLock(name)) {
          // 先从缓存查找该class对象,找到就不用重新加载
          Class<?> c = findLoadedClass(name);
          if (c == null) {
              long t0 = System.nanoTime();
              try {
                  if (parent != null) {
                      //如果找不到,则委托给父类加载器去加载
                      c = parent.loadClass(name, false);
                  } else {
                  //如果没有父类,则委托给启动加载器去加载
                      c = findBootstrapClassOrNull(name);
                  }
              } catch (ClassNotFoundException e) {
                  // ClassNotFoundException thrown if class not found
                  // from the non-null parent class loader
              }

              if (c == null) {
                  // If still not found, then invoke findClass in order
                  // 如果都没有找到,则通过自定义实现的findClass去查找并加载
                  c = findClass(name);

                  // this is the defining class loader; record the stats
                  sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                  sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                  sun.misc.PerfCounter.getFindClasses().increment();
              }
          }
          if (resolve) {//是否需要在加载时进行解析
              resolveClass(c);
          }
          return c;
      }
  }

从loadClass实现也可以知道如果不想重新定义加载类的规则,也没有复杂的逻辑,只想在运行时加载自己指定的类,那么我们可以直接使用this.getClass().getClassLoder.loadClass("className"),这样就可以直接调用ClassLoaderloadClass方法获取到class对象

  • findClass(String)

在JDK1.2之前,在自定义类加载时,总会去继承ClassLoader类并重写loadClass方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中,从前面的源码可知,findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。需要注意的是ClassLoader类中并没有实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常,同时应该知道的是findClass方法通常是和defineClass方法一起使用的(稍后会分析),ClassLoader类中findClass()方法源码如下:

//直接抛出异常
protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
}
  • defineClass(String name,byte[] b, int off, int len)
    • name:classname;b:字节码的byte字节流;off:开始解析的索引;len:解析的字符长度

defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑),通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象,defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象,简单例子如下:

protected Class<?> findClass(String name) throws ClassNotFoundException {
	  // 获取类的字节数组
      byte[] classData = getClassData(name);  
      if (classData == null) {
          throw new ClassNotFoundException();
      } else {
	      //使用defineClass生成class对象
          return defineClass(name, classData, 0, classData.length);
      }
  }

需要注意的是,如果直接调用defineClass()方法生成类的Class对象,这个类的Class对象并没有解析(也可以理解为链接阶段,毕竟解析是链接的最后一步),其解析操作需要等待初始化阶段进行。

  • resolveClass(Class≺?≻ c)

解析类。前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。(类的生命周期详解)

双亲委派模式作用

  • 共享功能,一些Framework层级的类一旦被顶层的ClassLoader加载过就缓存在内存里面,以后任何地方用到都不需要重新加载。
  • 隔离功能,保证java/Android核心类库的纯净和安全,防止恶意加载。( 比如string类,避免用户自己写代码冒充核心类库)

自定义Java类加载器

从上面源码的分析,可以知道:实现自定义类加载器需要继承ClassLoader,如果想保证自定义的类加载器符合双亲委派机制,则覆写findClass方法;如果想打破双亲委派机制,则覆写loadClass方法。

编写自定义类加载器的意义何在呢?

  • 当class文件不在ClassPath路径下,默认系统类加载器无法找到该class文件,在这种情况下我们需要实现一个自定义的ClassLoader来加载特定路径下的class文件生成class对象。

  • 当一个class文件是通过网络传输并且可能会进行相应的加密操作时,需要先对class文件进行相应的解密后再加载到JVM内存中,这种情况下也需要编写自定义的ClassLoader并实现相应的逻辑。

  • 当需要实现热部署功能时(一个class文件通过不同的类加载器产生不同class对象从而实现热部署功能),需要实现自定义ClassLoader的逻辑。

这里我们实现一个不破坏双亲委托机制的类加载器,如下步骤:

  1. 自定义一个People.java类做例子,该类写在记事本里,在用javac命令行编译成class文件,放在d盘根目录下

public class People {
	private String name;
 
	public People() {}
 
	public People(String name) {
		this.name = name;
	}
 
	public String getName() {
		return name;
	}
 
	public void setName(String name) {
		this.name = name;
	}
 
	public String toString() {
		return "I am a people, my name is " + name;
	}
 
  1. 自定义类加载器
package cn.eft.llj.jvm.customLoader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
 
public class MyClassLoader extends ClassLoader
{
    public MyClassLoader()
    {
        
    }
    
    public MyClassLoader(ClassLoader parent)
    {
        super(parent);
    }
    
    protected Class<?> findClass(String name) throws ClassNotFoundException
    {
    	File file = new File("D:\\Person.class");//读取我们刚放的class文件
        try{
            byte[] bytes = getClassBytes(file);
            //defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class
            Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
            return c;
        } 
        catch (Exception e)
        {
            e.printStackTrace();
        }
        
        return super.findClass(name);
    }
    
    private byte[] getClassBytes(File file) throws Exception
    {
        // 这里要读入.class的字节,因此要使用字节流
        FileInputStream fis = new FileInputStream(file);
        FileChannel fc = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel wbc = Channels.newChannel(baos);
        ByteBuffer by = ByteBuffer.allocate(1024);
        
        while (true){
            int i = fc.read(by);
            if (i == 0 || i == -1)
            break;
            by.flip();
            wbc.write(by);
            by.clear();
        }
        fis.close();
        return baos.toByteArray();
    }
}
  1. 测试类加载
public class Test
{
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException,
                                                  InstantiationException, MalformedURLException
    {
        MyClassLoader mcl = new MyClassLoader();
        Class<?> clazz = Class.forName("Person", true, mcl);
        Object obj = clazz.newInstance();
    
        System.out.println(obj);
        System.out.println(obj.getClass().getClassLoader());//打印出我们的自定义类加载器
    
    }
}

运行结果:

I am a person, my name is null
cn.eft.llj.jvm.customLoader.MyClassLoader@6bc7c054

注意:实现自定义类加载器也可以通过继承URLClassLoader,但是这种方式不知为何无法实现,网上很多示例代码,要么实现有问题,要么运行会报错,若有知道的同学麻烦评论区跟我说下。

android类加载器

JVM与android虚拟机区别

JVM是Java Virtual Machine,而DVM就是Dalvik Virtual Machine,是安卓中使用的虚拟机,所有安卓程序都运行在安卓系统进程里,每个进程对应着一个Dalvik虚拟机实例。他们都提供了对象生命周期管理、堆栈管理、线程管理、安全和异常管理以及垃圾回收等重要功能,各自拥有一套完整的指令系统,以下简要对比两种虚拟机的不同。 dalvik与jvm的不同:

  1. jvm:执行的是class文件;dalvik:执行的是dex
  2. 类加载系统与jvm区别较大
  3. jvm只能存在一个;dalvik可以同时存在多个dvm
  4. dalvk基于寄存器,jvm基于栈(内存)

Dalvik执行的是dex字节码,运行时动态地将执行频率很高的dex字节码翻译成本地机器码;

android5.0后虚拟机由dalvik替换为ART(Android Runtime),在安装应用的时候,dex中的字节码将被编译成本地机器码,之后每次打开应用,执行的都是本地机器码。

ART对比dalvik:
1、DVM使用JIT将字节码转换成机器码,效率低
2、ART采用AOT预编译技术,执行效率更快
3、ART会占用更多的安装时间和存储空间
4、预编译减少了 CPU 的使用频率,降低了能耗

JIT(Just In Time,即时编译技术)
AOT(Ahead Of Time,预编译技术)

详细了解可参考这儿JAVA虚拟机与Android虚拟机的区别

android类加载机制与源码解析

Android的Dalvik/ART虚拟机虽然与标准Java的JVM虚拟机不一样,ClassLoader具体的加载细节不一样,但是工作机制是类似的, (从android sdk的ClassLoader#loadClass和jdk的ClassLoader#loadClass源码可以看出都使用了双亲委托机制)

下面直接来看看ClassLoader的关系

  • BootClassLoader

BootClassLoader实例在Android系统启动的时候被创建,用于加载一些Android系统框架的类,其中就包括APP用到的一些系统类。(与Java中的BootstrapClassLoader不同,它并不是由C/C++代码实现,而是由Java实现的,BootClassLoader是ClassLoader的内部类)

  • BaseDexClassLoader,我们先来看看基类BaseDexClassLoader的构造方法
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
        String librarySearchPath, ClassLoader parent) {
    super(parent);
    this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}

BaseDexClassLoader构造方法的四个参数的含义如下:

  • dexPath :指目标类所在的apk或jar文件的路径,比如像这样的:"/data/app/com.yeoggc.myapplication-1/base.apk"。如果要包含多个路径,路径之间用冒号分隔。

  • optimizedDirectory:类加载器把dexPath路径上的文件,进行ODEX优化到内部存储路径,该路径就是由optimizedDirectory指定的。如果为null,那么就采用默认的系统路径。(不能是任意目录,它必须是程序所属的目录才行,比如:data/data/包名/xxx)

    dex和odex区别:
    其实一个APK是一个程序压缩包,里面有个执行程序包含dex文件,ODEX优化就是把包里面的执行程序提取出来,就变成ODEX文件。因为你提取出来了,系统第一次启动的时候就不用去解压程序压缩包,少了一个解压的过程。这样的话系统启动就加快了。为什么说是第一次呢?是因为DEX版本的也只有第一次会解压执行程序到 /data/dalvik-cache(针对PathClassLoader)或者optimizedDirectory(针对DexClassLoader)目录,之后也是直接读取目录下的的dex文件,所以第二次启动就和正常的差不多了。当然这只是简单的理解,实际生成的ODEX还有一定的优化作用。ClassLoader只能加载内部存储路径中的dex文件,所以这个路径必须为内部路径。
    
  • libraryPath:指目标类中所使用到的C/C++库存放的路径。

  • parent:是指该类加载器的父类加载器,一般为当前执行类的装载器

它派生出两个子类加载器:

  • PathClassLoader: 主要用于系统和app的类加载器,其中optimizedDirectory为null, 采用默认目录/data/dalvik-cache/
  • DexClassLoader: 可以从包含classes.dex的jar或者apk中,加载类的类加载器, 可用于执行动态加载, 但必须是app私有可写目录来缓存odex文件. 能够加载系统没有安装的apk或者jar文件, 因此很多热修复和插件化方案都是采用DexClassLoader;

DexClassLoader与PathClassLoader都继承于BaseDexClassLoader,这两个类只是提供了自己的构造函数,没有额外的实现,我们对比下它们的构造函数的区别。

  • PathClassLoader
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
public class 
DexClassLoader extends BaseDexClassLoader {
    
   public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
    }
}

可以发现这两个类的构造函数最大的差别就是DexClassLoader提供了optimizedDirectory,而PathClassLoader则没有,optimizedDirectory正是用来存放odex文件的地方,所以可以利用DexClassLoader实现动态加载。

除了这两个子类以外,还有两个类:

  • DexPathList:就跟它的名字那样,该类主要用来查找Dex、SO库的路径,并这些路径整体呈一个数组。
  • DexFile:用来描述Dex文件,Dex的加载以及Class的查找都是由该类调用它的native方法完成的。(DexFile为DexPathList的内部类Element的成员属性)

Dex的加载以及Class额查找都是由DexFile调用它的native方法完成的,我们来看看它的实现。 我们来看看Dex文件加载、类的查找加载的序列图,如下所示:

从上图Dex加载的流程可以看出,optimizedDirectory决定了调用哪一个DexFile的构造函数。

如果optimizedDirectory为空,这个时候其实是PathClassLoader,则调用:

DexFile(File file, ClassLoader loader, DexPathList.Element[] elements)
        throws IOException {
    this(file.getPath(), loader, elements);
}

如果optimizedDirectory不为空,这个时候其实是DexClassLoader,则调用:

private DexFile(String sourceName, String outputName, int flags, ClassLoader loader,
        DexPathList.Element[] elements) throws IOException {
    if (outputName != null) {
        try {
            String parent = new File(outputName).getParent();
            if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
                throw new IllegalArgumentException("Optimized data directory " + parent
                        + " is not owned by the current user. Shared storage cannot protect"
                        + " your application from code injection attacks.");
            }
        } catch (ErrnoException ignored) {
            // assume we'll fail with a more contextual error later
        }
    }

    mCookie = openDexFile(sourceName, outputName, flags, loader, elements);
    mFileName = sourceName;
    //System.out.println("DEX FILE cookie is " + mCookie + " sourceName=" + sourceName + " outputName=" + outputName);
}

所以你可以看到DexClassLoader在加载Dex文件的时候比PathClassLoader多了一个openDexFile()方法,该方法调用的是native方法openDexFileNative()方法。

这个方法并不是真的打开Dex文件,而是将Dex文件以一种mmap的方式映射到虚拟机进程的地址空间中去,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,虚拟机进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。 关于mmap,它是一种很有用的文件读写方式,限于篇幅这里不再展开,更多关于mmap的内容可以参见文章:认真分析mmap:是什么 为什么 怎么用

android类加载机制总结

Android虚拟机有两个主要的类加载器DexClassLoader与PathClassLoader,它们都继承于BaseDexClassLoader,它们内部都维护了一个DexPathList的对象,DexPathList主要用来存放指明包含dex文件、native库和优化odex目录。 Dex文件采用DexFile这个类来描述,Dex的加载以及类的查找都是通过DexFile调用它的native方法来完成的。

使用DexClassLoader动态加载dex的简单例子

前面我们说过DexClassLoader可以加载外部获取的dex/jar/apk文件来实现热修复或插件化,接下来先演示一个简单的动态加载dex示例,该示例实现了:从 SD 卡中动态加载一个包含 class.dex 的 jarr文件,加载其中的类,并调用其方法。(关于热修复要比这个复杂一些,下次再详解)

主要步骤如下:

  1. 将修复的代码打成jar包 新建两个java类ISayHello.java 和 SayHello.java
package com.jaeger;

public interface ISayHello {

    String say();
}

package com.jaeger;
public class SayHello implements ISayHello
{
    @Override
    public String say()
    {
        return "这是新的文字";
    }
}

这里我们通过android studio的gradle脚本打出jar包:sayHello.jar

task sayHelloJar(type: Jar) {
    dependsOn 'build'
    from ("${buildDir}/intermediates/classes/debug/")
    include "com/jaeger/**"
    exclude "com/jaeger/testclassloader/**" //这个包下放MainActivity,不一起打包,排除掉
    archiveName "sayHello.jar"
    destinationDir new File("${buildDir}/outputs/libs")//打好的jar包会在这个目录
}
  1. 将jar包打成dex包

将上面的sayHello.jar放在sdk的dx 工具所在的目录下(android 23及以上dx工具的目录在:sdk\build-tools\sdk版本\下,更早之前的版本可能在sdk\platform-tools目录下),打开命令行工具,进入该目录,然后执行如下

D:\Android\sdk\build-tools\28.0.3>dx --dex --output=sayHello_dex.jar sayHello.jar

这样,就打出了sayHello_dex.jar的dex包了

  1. dex包sayHello_dex.jar直接拖到手机存储根目录下

  2. 使用DexClassLoader加载dex中的类,并调用相关方法

记得先删除项目中的SayHello.java,防止在加载dex包下的SayHello之前就先加载了项目中的SayHello

package com.jaeger.testclassloader;

import android.os.Bundle;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import com.jaeger.ISayHello;
import com.jaeger.SayHello;

import dalvik.system.DexClassLoader;
import java.io.File;

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "TestClassLoader";
    private TextView mTvInfo;
    private Button mBtnLoad;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTvInfo = findViewById(R.id.tv_info);
        mBtnLoad = findViewById(R.id.btn_load);
        mBtnLoad.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                // 获取到包含 class.dex 的 jar 包文件
                final File jarFile =
                    new File(Environment.getExternalStorageDirectory().getPath() + File.separator + "sayHello_dex.jar");

                //6.0以上的自行手动申请权限,这里重点不是这个就不写了,或者可以到设置界面直接开启对应的权限
                Log.d(TAG, jarFile.length() + "");
                Log.d(TAG, jarFile.canRead() + "");

                if (!jarFile.exists()) {
                    Log.e(TAG, "sayHello_dex.jar not exists");
                    return;
                }

                // getCodeCacheDir() 方法在 API 21 才能使用,实际测试替换成 getExternalCacheDir() 等也是可以的
                // 只要有读写权限的路径均可
                DexClassLoader dexClassLoader =
                    new DexClassLoader(jarFile.getAbsolutePath(), getExternalCacheDir().getAbsolutePath(), null, getClassLoader());
                try {
                    // 加载 SayHello 类
                    Class clazz = dexClassLoader.loadClass("com.jaeger.SayHello");
                    // 强转成 ISayHello, 注意 ISayHello 的包名需要和 jar 包中的
                    ISayHello iSayHello = (ISayHello) clazz.newInstance();
                    mTvInfo.setText(iSayHello.say());
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                } catch (InstantiationException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tv_info"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="36dp"
        android:text="Hello World!"/>

    <Button
        android:id="@+id/btn_load"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="36dp"
        android:text="load dex form SD card"/>
</LinearLayout>

AndroidManifest.xml记得授权

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
  1. 运行项目,点击按钮,上面的文字被成功替换成了新的文字

android类加载器与java类加载器异同

根据前面的分析,我们总结下,android与java在类加载上的异同

相同:

  • Android类加载器和Java的类加载器工作机制是类似的,使用双亲委托机制

不同:

  • 加载的字节码不同 Android虚拟机运行的是dex字节码,Java虚拟机运行的class字节码。

  • 类加载器不同以及类加载器的类体系结构不同 如上面的类加载器结构图

  • BootClassLoader和Java的BootStrapClassLoader区别:Android虚拟机中BootClassLoader是ClassLoader内部类,由java代码实现而不是c++实现,是Android平台上所有ClassLoader的最终parent,这个内部类是包内可见,所以我们没法使用。 Java虚拟机中BootStrapClassLoader是由原生代码(C++)编写的,负责加载java核心类库(例如rt.jar等)

  • ClassLoader类中的findBootstrapClassOrNull方法,android sdk直接返回null,jdk会去调用native方法findBootstrapClass,如下源码

    • jdk8的findBootstrapClassOrNull方法:
    
        /**
         * Returns a class loaded by the bootstrap class loader;
         * or return null if not found.
         */
        private Class<?> findBootstrapClassOrNull(String name)
        {
            if (!checkName(name)) return null;
    
            return findBootstrapClass(name);
        }
    
        // return null if not found
        private native Class<?> findBootstrapClass(String name);
    
    

    android sdk的findBootstrapClassOrNull方法:

       /**
         * Returns a class loaded by the bootstrap class loader;
         * or return null if not found.
         */
        private Class<?> findBootstrapClassOrNull(String name)
        {
            return null;
        }
    

参考资料

深入理解Java类加载器(ClassLoader)

Android虚拟机框架:类加载机制

热修复入门