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

目录结构

什么是类加载器?
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)类加载器(也称应用类加载器),下面分别介绍。php

Java类加载器

BootstrapClassLoader(启动类加载器)

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

ExtensionClassLoader(扩展类加载器)

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

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

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

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

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

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

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

  • 启动类加载器,由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虚拟机框架:类加载机制

热修复入门

相关文章
相关标签/搜索