ClassLoader 的加载机制

ClassLoader 的加载机制

1. Java中的类何时被加载器加载

在Java程序启动的时候,并不会一次性加载程序中所有的.class文件,而是在程序运行过程中,动态的加载相应的类到内存中。

通常情况下,Java程序中的.class文件会在以下两种情况下被ClassLoader主动加载到内存中:

  1. 调用类构造器
  2. 调用类中的静态(static)变量或者静态方法

2. Java中的ClassLoader

JVM中自带的3个类加载器:

  1. 启动类加载器:BootstrapClassLoader
  2. 扩展类加载器:ExtClassLoader(JDK1.9之后,改名为PlatformClassLoader)
  3. 系统类加载器:AppClassLoader

1. AppClassLoader系统类加载器

部分源码如下:

在这里插入图片描述

可以看出,AppClassLoader主要加载系统属性“java.class.path"配置下类文件,也就是环境变量CLASS_PATH配置的路径。因此AppClassLoader是面向用户的类加载器,我们自己编写的代码以及使用的第三方jar包通常都是由它来加载的。

例如:

一个Java项目名为TestProject

那么AppClassLoader所加载的目录是TestProject/bin

可以在代码中通过System.getProperty("java.class.path");查看

2. ExtClassLoader扩展类加载器

部分源码如下:

在这里插入图片描述

可以看出,ExtClassLoader加载系统属性”java.ext.dirs"配置下的类文件,可以打印出这个属性来查看具体有哪些文件:

\Library\Java\jre1.8.0_201\lib\ext;

3. BootstrapClassLoader启动类加载器

BootstrapClassLoader同上面两种ClassLoader不太一样。

首先,它不是使用Java代码实现的,而是由C\C++语言编写,它本身属于虚拟机的一部分。因此我们无法在Java代码中直接获取它的引用。如果尝试在Java层获取BootstrapClassLoader的引用,系统会返回null。

BootstrapClassLoader加载系统属性“sun.boot.class.path"配置下类文件,可以打印出这个属性来查看具体有哪些文件:

\Library\Java\jre1.8.0_201\lib\resources.jar;
\Library\Java\jre1.8.0_201\lib\rt.jar;
\Library\Java\jre1.8.0_201\lib\sunrsasign.jar;
\Library\Java\jre1.8.0_201\lib\jsse.jar;
\Library\Java\jre1.8.0_201\lib\jce.jar;
\Library\Java\jre1.8.0_201\lib\charsets.jar;
\Library\Java\jre1.8.0_201\lib\jfr.jar;
\Library\Java\jre1.8.0_201\classes

可以看到,这些全是jre目录下的jar包或者.class文件。

3. 双亲委派模式

既然JVM中已经有了者3中ClassLoader,那么JVM又是如何知道该使用哪一个类加载器去加载相应的类呢?答案就是:双亲委派模式

双亲委派模式

所谓双亲委派模式就是,当类加载器收到加载类或资源的请求时,通常都是现委托给父类加载器加载,也就是说,只有当父类加载器找不到指定类或资源时,自身才会执行实际的类加载过程。

其具体实现代码是在ClassLoader.java中的loadClass方法中,如下所示:

在这里插入图片描述

解释说明:

  1. 判断该Class是否已加载,如果已加载,则直接将该Class返回。
  2. 如果该Class没有被加载过,则判断parent是否为空,如果不为空则将加载任务委托给parent。
  3. 如果parent == null,则直接调用BootstrapClassLoader加载该类。
  4. 如果parent或者BootstrapClassLoader都没有加载成功,则调用当前的ClassLoader的findClass方法继续尝试加载。

那这个parent是什么呢?我们可以看一下ClassLoader的构造器,如下:

在这里插入图片描述

可以看出,在每一个ClassLoader中都有一个ClassLoader类型的parent的引用,并且在构造器中传入值。如果我们继续查看源码,可以看到AppClassLoader传入的parent就是ExtClassLoader,而ExtClassLoader并没有传入任何parent,也就是null。

举例

Test test = new Test();

默认情况下,JVM首先使用AppClassLoader去加载Test类。

  1. AppClassLoader将加载的任务委派给它的父类加载器(parent)-ExtClassLoader
  2. ExtClassLoader的parent为null,所以直接将加载任务委派给BootstrapClassLoader
  3. BootstrapClassLoader在lib目录下无法找到Test类,因此返回的Class为null。
  4. 因为parent和BootstrapClassLoader都没有加载Test类,所以AppClassLoader会调用自身的findClass方法来加载Test。

最终Test类就是被AppClassLoader加载到内存中,可以通过如下代码验证:

public static void main(String[] args) {
		ClassLoader cl = Test.class.getClassLoader();
		System.out.println("cl is "+cl);
		
		ClassLoader parent = cl.getParent();
		System.out.println("parent is "+parent);
		
		ClassLoader boot_strap = parent.getParent();
		System.out.println("boot_strap is "+boot_strap);
	}
}

打印结果为:

cl is sun.misc.Launcher A p p C l a s s L o a d e r @ 73 d 16 e 93 p a r e n t i s s u n . m i s c . L a u n c h e r AppClassLoader@73d16e93 parent is sun.misc.Launcher ExtClassLoader@15db9742
boot_strap is null

可以看出,Test的ClassLoader为AppClassLoader类型,而AppClassLoader的parent为ExtClassLoader类型。ExtClassLoader的parent为null。

注意:“双亲委派”机制只是Java推荐的机制,并不是强制的机制。我们可以继承java.lang.ClassLoader类,实现自己的类加载器。如果想保持双亲委派模型,就应该重写findClass(name)方法;如果想破坏双亲委派模型,可以重写loadClass(name)方法。

4. 自定义ClassLoader

  1. 自定义一个类继承抽象类ClassLoader。
  2. 重写findClass方法。
  3. 在findClass中,调用defineClass方法将字节码转换成Class对象,并返回。

用一段伪代码来描述这段过程如下:

class 自定义ClassLoader extends ClassLoader{
    Class findClass(String name){
        //寻找字节码
        byte[] code = findCodeFromSomeWhere(name);
        //根据字节码组装Class对象,并返回
        return this.defineClass(code,name);
    }
}

自定义ClassLoader实践

首先,在本地电脑上创建一个测试类Test.java,代码如下:

public class Test{
	public void printTest(){
		System.out.println("I`m boy~");
	}
}

测试类所在磁盘路径如下:

C:\TestLoader\

接下来,创建DiskClassLoader继承ClassLoader,重写findClass方法,并在其中调用defineClass创建Class,如下:

public class DiskClassLoader extends ClassLoader {
	
	String filePath;

	public DiskClassLoader(String path){
		filePath = path;
	}
	
	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		//newPath = "file:///TestLoader/Test.class"
		String newPath = filePath+name+".class";
		byte[] classBytes = null;
		Path path = null;
		try{
			path = Paths.get(new URI(newPath));
			classBytes = Files.readAllBytes(path);
		}catch(IOException | URISyntaxException e){
			e.printStackTrace();
		}
		
		return defineClass(name,classBytes,0,classBytes.length);
	}
}

最后,在主方法中进行自定义类加载器的测试:

public static void main(String[] args) {
    //创建自定义ClassLoader对象
		DiskClassLoader diskLoader = new DiskClassLoader("file:///TestLoader/");
		try {
			Class c = diskLoader.findClass("Test");
			if(c!=null){
				Object obj = c.newInstance();
                //通过反射调用Test的printTest方法
				Method method = c.getDeclaredMethod("printTest", null);
				method.invoke(obj, null);
			}
		} catch (Exception e) {
			
			e.printStackTrace();
		} 
		
	}

最后执行上述,并打印如下结果,说明我们的自定义DiskClassLoader可以正常工作。

在这里插入图片描述

5. Android中的ClassLoader

本质上,Android和传统的JVM是一样的,也需要通过ClassLoader将目标类加载进内存,类加载器之间也符合双亲委派模型。但是在Android中,ClassLoader的加载细节有略微的差别。

在Android虚拟机里是无法直接运行.class文件的,Android会将所有的.class文件转换成一个.dex文件,并且Android将加载.dex文件的实现封装在BaseDexClassLoader中,而我们一般只使用它的两个子类:PathClassLoader和DexClassLoader。

PathClassLoader

PathClassLoader用来加载系统apk和被安装到手机中的apk内的dex文件。它的2个构造函数如下:

public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }

    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }
}

参数说明:

  • dexPath:dex文件路径,或者包含dex文件的jar包路径
  • librarySearchPath:C/C++native库的路径

PathClassLoader里面除了这两个构造方法以外就没有其他的代码了,具体实现都是在BaseDexClassLoader里面,其dexPath比较受限制,一般是已经安装应用的apk文件路径。

当一个App被安装到手机后,apk里面的class.dex中的class均是通过PathClassLoader来加载的。

DexClassLoader

先来看看官方对DexClassLoader的描述:

A class loader that loads classes from .jar and .apk filescontaining a classes.dex entry.

This can be used to execute code notinstalled as part of an application.

很明显,对比PathClassLoader只能加载已经安装应用的dex或apk文件,DexClassLoader则没有此限制,可以从SD卡上加载包含class.dex的.jar和.apk文件,这也是插件化和热修复的基础,在不需要安装应用的情况下,完成需要使用的dex的加载。

DexClassLoader的源码里面只有一个构造方法,代码如下:

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
        super((String)null, (File)null, (String)null, (ClassLoader)null);
        throw new RuntimeException("Stub!");
    }
}

参数说明:

  • dexPath:包含class.dex的apk,jar文件路径,多个路径用文件分隔符(默认是“:”)分隔。
  • optimizedDirectory:用来缓存优化的dex文件的路径,即从apk或jar文件中提取出来的dex文件。该路径不可以为空,且应该是私有的,有读写权限的路径。

使用DexClassLoader实现热修复

创建Android项目DexCLassLoaderHotFix

项目结构如下:

在这里插入图片描述

ISay.java是一个接口,内部只定义了一个方法 saySomething

在这里插入图片描述

SayException.java实现了ISay接口,但是在saySomething方法中,打印“something wrong here”来模拟一个线上的bug。

在这里插入图片描述

最后在MainActivity.java中,当点击button的时候,将saySomething返回的内容通过Toast显示在屏幕上。

public class MainActivity extends AppCompatActivity{
    privete final String TAG = "MainActivity";
    
    ISay say;
    
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        say = new SayException();
        
        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener(){
            public void onClick(View v){
                Log.e(TAG,"say_hotfix.jar not exists");
                Toast.makeText(MainActivity.this,say.saySomething(),Toast.LENGTH_SHORT).show();
                
            }
        });
    }
}

最后运行效果如下:
在这里插入图片描述

创建HotFix patch包

新建 Java 项目,并分别创建两个文件 ISay.java 和 SayHotFix.java。

package com.example.dexclassloaderhotfix;

public interface ISay {
    String saySomething();
}
package com.example.dexclassloaderhotfix;

public class SayHotFix implements ISay {
    
    public String saySomething() {
        return "Everything is right!";
    }
}

ISay 接口的包名和类名必须和 Android 项目中保持一致。SayHotFix 实现 ISay 接口,并在 saySomething 中返回了新的结果,用来模拟 bug 修复后的结果。

将 ISay.class 和 SayHotFix.class 打包成 say_something.jar,然后通过 dx 工具将生成的 say_something.jar 包中的 class 文件优化为 dex 文件。

使用Android studio 打包成jar包过程

在app/build.gradle添加如下代码

task makeJar(type:Jar){
    baseName 'say_something'

    from('build/intermediates/javac/debug/classes/com/example/dexclassloaderhotfix/')//这个路径为class文件路径

    into('com/example/dexclassloaderhotfix')

    exclude('BuildConfig.class','MainActivity.class','R.class')

    exclude{it.name.startsWith('R$')}
}
makeJar.dependsOn(build)

在这里插入图片描述

在这里插入图片描述

优化为dex文件方法

在androidsdk\build-tools\下找到dx.bat,并把jar包移到此路径

打开命令行:

输入dx --dex --output=say_something_hotfix.jar say_something.jar

上述 say_something_hotfix.jar 就是我们最终需要用作 hotfix 的 jar 包。

将 HotFix patch 包拷贝到 手机,并使用 DexClassLoader 加载 SD 卡中的 ISay 接口

接下来,修改 MainActivity 中的逻辑,使用 DexClassLoader 加载 HotFix patch 中的 SayHotFix 类,如下:

public class MainActivity extends AppCompatActivity {
    
    private static final String TAG = "MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void onClick(View view) {
       final File jarFile = new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath()+File.separator+"say_something_hotfix.jar");
       if (!jarFile.exists()){
            ISay say = new SayException();
           Toast.makeText(this, say.saySomething(), Toast.LENGTH_SHORT).show();
       }else{
           DexClassLoader dexClassLoader = new DexClassLoader(jarFile.getAbsolutePath(),getExternalCacheDir().getAbsolutePath(),null,getClassLoader());
           Log.d(TAG, "onClick: cache:"+getExternalCacheDir().getAbsolutePath());
           Log.d(TAG, "onClick: jar"+jarFile.getAbsolutePath());
           try {
               Class clazz = dexClassLoader.loadClass("com.example.dexclassloaderhotfix.SayHotFix");
               ISay say = (ISay)clazz.newInstance();
               Toast.makeText(this, say.saySomething(), Toast.LENGTH_SHORT).show();
           } catch (Exception e) {
               e.printStackTrace();
           }
       }
    }
}

最后运行效果如下:
在这里插入图片描述

总结

  • ClassLoader 就是用来加载 class 文件的,不管是 jar 中还是 dex 中的 class。
  • Java 中的 ClassLoader 通过双亲委托来加载各自指定路径下的 class 文件。
  • 可以自定义 ClassLoader,一般覆盖 findClass() 方法,不建议重写 loadClass 方法。
  • Android 中常用的两种 ClassLoader 分别为:PathClassLoader 和 DexClassLoader。

猜你喜欢

转载自blog.csdn.net/qq_43621019/article/details/105666409