(4.1.53)Android ClassLoader详解


我们在 (2.1.1.5)Java ClassLoader体系结构使用详解学习了Java的ClassLoader,很多同学会把Java和Android的ClassLoader搞混,甚至会认为Android中的ClassLoader和Java中的ClassLoader是一样的

这显然是不对的,这一篇文章我们就来学习Android中的ClassLoader,来看看它和Java中的ClassLoader有何不同

一、Android 中的 dex 文件

Java程序中,JVM虚拟机是通过类加载器ClassLoader加载.jar文件里面的类的。Android也类似,不过Android用的是Dalvik/ART虚拟机,不是JVM,也不能直接加载.jar文件,而是加载dex文件。
在这里插入图片描述

.jar文件里面包含多个.class文件,每个class文件里面包含了该类的常量池、类信息、属性等,当jvm加载该jar文件的时候,会加载里面上有所的class文件,这种加载方式很慢,对于内存有限的移动设备不适合。
而在.apk文件中只包含了一个dex文件,这个dex文件将所有的class文件锁包含的信息全部整合在一起,去除了冗余信息,减少了io操作,加快了类的查找速度

Google 使用了自己的 Dalvik 来运行应用,所以class 不能在 AndroidDalvik 的 java 环境中运行, android 的 class 文件实际上只是编译过程中的中间目标文件,需要链接成 dex 文件后才能在 dalvik 上运行。

  • Android 应用打包成 apk 文件时,class 文件会被打包成一个或者多个 dex 文件。将一个 apk 文件后缀改成 .zip 格式解压后(也可以直接解压,apk 文件本质是个 zip 文件),里面就有 class.dex 文件,由于 Android 的 65K 问题(不要纠结是 64K 还是 65K),使用 MultiDex 就会生成多个 dex 文件
  • 当 Android 系统安装一个应用的时候,会针对不同平台对 Dex 进行优化,这个过程由一个专门的工具来处理,叫 DexOpt 。DexOpt 是在第一次加载 Dex 文件的时候执行的,该过程会生成一个 ODEX 文件,即 Optimised Dex。执行 ODEX 的效率会比直接执行 Dex 文件的效率要高很多,加快 App 的启动和响应
  • 最终在Android虚拟机上执行的并非Java 字节码,而是另一种字节码:dex 格式的字节码。在编译 Java 代码之 后 ,通过 Android 平台上的工具可以将 Java 字节码转换成 Dex 字节码。

总之,Android 中的 Dalvik/ART 无法像 JVM 那样 直接 加载 class 文件和 jar 文件中的 class,需要通过 dx 工具来优化转换成 Dalvik byte code 才行,只能通过 dex 或者 包含 dex 的jar、apk 文件来加载(注意 odex 文件后缀可能是 .dex 或 .odex,也属于 dex 文件),因此 Android 中的 ClassLoader 工作就**主要交给了 BaseDexClassLoader **来处理

注:如果 jar 文件包含有 dex 文件,此时 jar 文件也是可以用来加载的,不过实际加载的还是其中的 dex 文件,不要弄混淆了。
注:有的Android应用能直接加载.jar文件,那是因为这个.jar文件已经经过优化,只不过后缀名没改(其实已经是.dex文件)

更多Odex请参看ODEX格式及生成过程ART和Dalvikwhat-are-odex-files-in-android

1.1 dvm和jvm区别

  1. 执行的字节码不一样
  2. 基于的架构不一样
  3. Dalvik 经过优化,允许在有限的内存中同时运行多个虚拟机的实例,并且每一个Dalvik 应用作为一个独立的Linux 进程执行。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。
  4. Dalvik虚拟机在android2.2之后使用JIT (Just-In-Time)技术,与传统JVM的JIT并不完全相同,
  5. Dalvik虚拟机有自己的 bytecode,并非使用 Java bytecode。

二、ClassLoader的类型

Android中的ClassLoader类型和Java中的ClassLoader类型类似,也分为两种类型:

  • 系统内置
    • BootClassLoader
    • PathClassLoader
    • DexClassLoader
  • 用户自定义
    • ...

运行一个Android程序需要用到几种类型的类加载器呢?如下所示

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ClassLoader loader = MainActivity.class.getClassLoader();
        while (loader != null) {
            Log.d("liuwangshu",loader.toString());//1
            loader = loader.getParent();
        }
    }
}

可以看到有两种类加载器:

  1. 一种是PathClassLoader. 在 Android 中,App 安装到手机后,apk 里面的 class.dex 中的 class 均是通过 PathClassLoader 来加载的
    • DexPathList中包含了很多apk的路径,其中/data/app/com.example.liuwangshu.moonclassloader-2/base.apk就是示例应用安装在手机上的位置。关于DexPathList后续文章会进行介绍
  2. 另一种则是BootClassLoader
10-07 07:23:02.835 8272-8272/? D/liuwangshu: dalvik.system.PathClassLoader[DexPathList
		[
			[zipfile“/data/app/com.example.liuwangshu.moonclassloader-2/base.apk”,
			zipfile“/data/app/com.example.liuwangshu.moonclassloader-2/split_lib_dependencies_apk.apk”,
			zipfile“/data/app/com.example.liuwangshu.moonclassloader-2/split_lib_slice_0_apk.apk”,
			zipfile“/data/app/com.example.liuwangshu.moonclassloader-2/split_lib_slice_1_apk.apk”,
			zipfile“/data/app/com.example.liuwangshu.moonclassloader-2/split_lib_slice_2_apk.apk”,
			zipfile“/data/app/com.example.liuwangshu.moonclassloader-2/split_lib_slice_3_apk.apk”,
			zipfile“/data/app/com.example.liuwangshu.moonclassloader-2/split_lib_slice_4_apk.apk”,
			zipfile“/data/app/com.example.liuwangshu.moonclassloader-2/split_lib_slice_5_apk.apk”,
			zipfile“/data/app/com.example.liuwangshu.moonclassloader-2/split_lib_slice_6_apk.apk”,
			zipfile“/data/app/com.example.liuwangshu.moonclassloader-2/split_lib_slice_7_apk.apk”,
			zipfile“/data/app/com.example.liuwangshu.moonclassloader-2/split_lib_slice_8_apk.apk”,
			zipfile“/data/app/com.example.liuwangshu.moonclassloader-2/split_lib_slice_9_apk.apk”],
			
			nativeLibraryDirectories=[/data/app/com.example.liuwangshu.moonclassloader-2/lib/x86,
			/vendor/lib,
			/system/lib]
		]
	]
10-07 07:23:02.835 8272-8272/? D/liuwangshu: java.lang.BootClassLoader@e175998

三、ClassLoader的继承关系

和Java中的ClassLoader一样,虽然系统所提供的类加载器有3种类型,但是系统提供的ClassLoader相关类却不只3个。ClassLoader的继承关系如下图所示

这里写图片描述
【processon 图Android ClassLoader的继承关系 (2)】

可以看到上面一共有8个ClassLoader相关类,其中有一些和Java中的ClassLoader相关类十分类似,下面简单对它们进行介绍:

  • ClassLoader是一个抽象类,其中定义了ClassLoader的主要功能。
  • BootClassLoader是它的内部类,用于**预加载preload()**常用类,加载一些系统Framework层级需要的类,我们的Android应用里也需要用到一些系统的类等
  • SecureClassLoader类和JDK8中的SecureClassLoader类的代码是一样的,它继承了抽象类ClassLoader。SecureClassLoader并不是ClassLoader的实现类,而是拓展了ClassLoader类加入了权限方面的功能,加强了ClassLoader的安全性。
    • URLClassLoader类和JDK8中的URLClassLoader类的代码是一样的,它继承自SecureClassLoader,用来通过URl路径从jar文件和文件夹中加载类和资源。在Android中基本无法使用
  • BaseDexClassLoader继承自ClassLoader,是抽象类ClassLoader的具体实现类,PathClassLoader和DexClassLoader都继承它。
    • PathClassLoader加载系统类和应用程序的类,如果是加载非系统应用程序类,则会加载data/app/目录下的dex文件以及包含dex的apk文件或jar文件
    • DexClassLoader 可以加载自定义的dex文件以及包含dex的apk文件或jar文件,也支持从SD卡进行加载
    • InMemoryDexClassLoader是Android8.0新增的类加载器,继承自BaseDexClassLoader,用于加载内存中的dex文件。

3.1 ClassLoader

java.lang.ClassLoader是所有ClassLoader的最终父类.

3.1.1 构造方法

  • 构造方法主要以下两种
    1. 显式传入一个 父构造器实例
    2. 无参默认构造法
      • 还是调用了 需要传入父构造器实例 的构造方法
      • 像java虚拟机里父构造器为空时默认的父构造器为getSystemClassLoader() = AppClassLoader,Android中默认无父构造器传入的情况下,默认父构造器为一个getSystemClassLoader() = PathClassLoader且此PathClassLoader父构造器为BootClassLoader
package java.lang;

public abstract class ClassLoader {

	private ClassLoader parent;
	
	//【1】
    protected ClassLoader(ClassLoader parentLoader) {
        this(parentLoader, false);
    }
	ClassLoader(ClassLoader parentLoader, boolean nullAllowed) {
        if (parentLoader == null && !nullAllowed) {
            throw new NullPointerException("parentLoader == null && !nullAllowed");
        }
        parent = parentLoader;
    }

	/**【2】
     * Constructs a new instance of this class with the system class loader as
     * its parent.
     */
    protected ClassLoader() {
        this(getSystemClassLoader(), false);
    }
	
	public static ClassLoader getSystemClassLoader() {
        return SystemClassLoader.loader;
    }
	
	static private class SystemClassLoader {
        public static ClassLoader loader = ClassLoader.createSystemClassLoader();
    }
	
	/**
     * Create the system class loader. Note this is NOT the bootstrap class
     * loader (which is managed by the VM). We use a null value for the parent
     * to indicate that the bootstrap loader is our parent.
     */
    private static ClassLoader createSystemClassLoader() {
        String classPath = System.getProperty("java.class.path", ".");

        // TODO Make this a java.net.URLClassLoader once we have those?
        return new PathClassLoader(classPath, BootClassLoader.getInstance());
    }
}

3.1.2 loadclass与双亲委托

  • ClassLoader中重要的方法是loadClass(String name),其他的子类都继承了此方法且没有进行复写
    • 和java虚拟机中常见的双亲委派模型一致的
    • 在加载类时首先判断这个类是否之前被加载过,如果有则直接返回,如果没有则首先尝试让parent ClassLoader进行加载,加载不成功才在自己的findClass()中进行加载
    • 这种模型并不是一个强制性的约束模型,比如你可以继承ClassLoader复写loadCalss方法来破坏这种模型,只不过双亲委派模是一种被推荐的实现类加载器的方式,而且jdk1.2以后已经不提倡用户在覆盖loadClass方法,而应该把自己的类加载逻辑写到findClass中
public Class<?> loadClass(String className) throws ClassNotFoundException {
        return loadClass(className, false);
    }
	
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
		//【1】是否已经加载过
        Class<?> clazz = findLoadedClass(className);

        if (clazz == null) {
            ClassNotFoundException suppressed = null;
            try {
				//【2】向上委托
                clazz = parent.loadClass(className, false);
            } catch (ClassNotFoundException e) {
                suppressed = e;
            }

            if (clazz == null) {
                try {
					//【3】向下查找
                    clazz = findClass(className);
                } catch (ClassNotFoundException e) {
                    e.addSuppressed(suppressed);
                    throw e;
                }
            }
        }

        return clazz;
    }
  • loadClass() 和java的加载过程类似,但是具体实现变得不同,后文会讲到:
    • findLoadedClass()
    • findClass()
      • defineClass()
        不同于java,Android ClassLoader#defineClass(String name)该方法被废弃使用,改为使用DexPathList#findClass(String name)
//【1】
protected final Class<?> findLoadedClass(String className) {
        ClassLoader loader;
        if (this == BootClassLoader.getInstance())
            loader = null;
        else
            loader = this;
        return VMClassLoader.findLoadedClass(loader, className);
    }	
	
//【2】不同子类复写此方法,实现不同路径的查找	
protected Class<?> findClass(String className) throws ClassNotFoundException {
	throw new ClassNotFoundException(className);
}

//【3】将class二进制内容转换成Class对象,如果不符合要求的会抛出各种异常
@Deprecated
protected final Class<?> defineClass(byte[] classRep, int offset, int length)
		throws ClassFormatError {
	throw new UnsupportedOperationException("can't load this type of class file");
}

3.1.3 其他Utils方法

ClassLoader还提供了一系列的Utils方法

  • 包缓存
    • Package definePackage(),根据参数构建一个java.lang.Package实例
    • Package[] getPackages(): 返回当前ClassLoader知道的所有包
    • Package getPackage(String name): 返回指定当前ClassLoader知道的指定路径的包
/**
 * The packages known to the class loader.
 */
private Map<String, Package> packages = new HashMap<String, Package>();  

//Returns the package with the specified name. Package information is searched in this class loader.
protected Package getPackage(String name) {
	synchronized (packages) {
		return packages.get(name);
	}
}
//Returns all the packages known to this class loader.
protected Package[] getPackages() {
	synchronized (packages) {
		Collection<Package> col = packages.values();
		Package[] result = new Package[col.size()];
		col.toArray(result);
		return result;
	}
}	

/**
* Defines and returns a new {@code Package} using the specified information. 
* If {@code sealBase} is {@code null}, the package is left unsealed. Otherwise, the package is sealed using this URL.
*/
protected Package definePackage(String name, String specTitle, String specVersion,
		String specVendor, String implTitle, String implVersion, String implVendor, URL sealBase)
		throws IllegalArgumentException {

	synchronized (packages) {
		if (packages.containsKey(name)) {
			throw new IllegalArgumentException("Package " + name + " already defined");
		}

		Package newPackage = new Package(name, specTitle, specVersion, specVendor, implTitle,
				implVersion, implVendor, sealBase);

		packages.put(name, newPackage);

		return newPackage;
	}
}
  • Resource的双亲委托模型
    • URL getResource(String resName)查找具备resName的资源的全路径,具体怎么查找由子类复写该方法
      • URL findResource(String resName)
    • Enumeration<URL> getResources(String resName)
      • Enumeration<URL> findResources(String resName)
    • InputStream getResourceAsStream(String resName)
public URL getResource(String resName) {
	URL resource = parent.getResource(resName);
	if (resource == null) {
		resource = findResource(resName);
	}
	return resource;
}

protected URL findResource(String resName) {
	return null;
}

  • String findLibrary(String libName):返回一个全路径,指向java.library.path目录下的libName名称的library
    • 具体实现由子类复写
protected String findLibrary(String libName) {
	return null;
}

3.2 BootClassLoader

Android系统启动时会使用BootClassLoader来预加载常用类.

与Java中的BootClassLoader不同,它并不是由C/C++代码实现,而是由Java实现的,BootClassLoade的代码如下所示

  • BootClassLoader是ClassLoader的内部类,并继承自ClassLoader。
  • BootClassLoader是一个单例类
  • 需要注意的是BootClassLoader的访问修饰符是默认的,只有在同一个包中才可以访问,因此我们在应用程序中是无法直接调用的

libcore/ojluni/src/main/java/java/lang/ClassLoader.java

class BootClassLoader extends ClassLoader {

    private static BootClassLoader instance;
	
    @FindBugsSuppressWarnings("DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED")
    public static synchronized BootClassLoader getInstance() {
        if (instance == null) {
            instance = new BootClassLoader();
        }
        return instance;
    }
	
	public BootClassLoader() {
        super(null, true);
    }
	...
}

我们来看下核心的loadClass的实现

@Override
protected Class<?> loadClass(String className, boolean resolve)
	   throws ClassNotFoundException {
	Class<?> clazz = findLoadedClass(className);//父类的方法,上边已经讲过

	if (clazz == null) {
		clazz = findClass(className);
	}

	return clazz;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
	return Class.classForName(name, false, null);
}

//# 最终调用 java.lang.Class中的native方法
static native Class<?> classForName(String className, boolean shouldInitialize,
            ClassLoader classLoader) throws ClassNotFoundException;
	

3.3 SecureClassLoader

SecureClassLoader类和JDK8中的SecureClassLoader类的代码是一样的,它继承了抽象类ClassLoader。

SecureClassLoader并不是ClassLoader的实现类,而是拓展了ClassLoader类加入了权限方面的功能,加强了ClassLoader的安全性。

  • URLClassLoader类和JDK8中的URLClassLoader类的代码是一样的,它继承自SecureClassLoader,用来通过URl路径从jar文件和文件夹中加载类和资源。但是由于 dalvik 不能直接识别jar,所以在 Android 中无法使用这个加载器
  • JSClassLoader
  • AppClassLoader

3.4 BaseDexClassLoader

在这里为了方便理解,我们将dex文件以及包含dex的apk文件或jar文件统称为dex相关文件

PathClassLoader和DexClassLoader都继承自BaseDexClassLoader,其中的主要逻辑都是在BaseDexClassLoader完成的。这些源码在java/dalvik/system

  • 构造函数。BaseDexClassLoader的构造函数包含四个参数,分别为:
    • dexPath,指目标类所在的APK或jar文件的路径
      类装载器将从该路径中寻找指定的目标类,该类必须是APK或jar的全路径.
      如果要包含多个路径,路径之间必须使用特定的分割符分隔,特定的分割符可以使用System.getProperty(“path.separtor”)获得。
      上面"支持加载APK、DEX和JAR,也可以从SD卡进行加载"指的就是这个路径,最终做的是将dexPath路径上的文件ODEX优化到内部位置optimizedDirectory,然后,再进行加载的。
    • File optimizedDirectory
      • optimizedDirectory是用来缓存我们需要加载的dex文件的,并创建一个DexFile对象,如果它为null,那么会直接使用dex文件原有的路径来创建DexFile对象
      • 由于dex文件被包含在APK或者Jar文件中,因此在装载目标类之前需要先从APK或Jar文件中解压出dex文件,该参数就是制定解压出的dex 文件存放的路径。
      • 这也是对apk中dex根据平台进行ODEX优化的过程。其实APK是一个程序压缩包,里面包含dex文件,ODEX优化就是把包里面的执行程序提取出来,就变成ODEX文件,因为你提取出来了,系统第一次启动的时候就不用去解压程序压缩包的程序,少了一个解压的过程。这样的话系统启动就加快了。
      • 为什么说是第一次呢?是因为DEX版本的也只有第一次会解压执行程序到 /data/dalvik-cache(针对PathClassLoader)或者optimizedDirectory(针对DexClassLoader)目录,之后也是直接读取目录下的的dex文件,所以第二次启动就和正常的差不多了。当然这只是简单的理解,实际生成的ODEX还有一定的优化作用。
      • 无论哪种动态加载,ClassLoader只能加载内部存储路径中的dex文件,所以这个路径必须为内部路径。
    • libPath,指目标类中所使用的C/C++库存放的路径
    • classload,是指该装载器的父装载器
      一般为当前执行类的装载器,例如在Android中以context.getClassLoader()作为父装载器。

DexClassLoader可以指定自己的optimizedDirectory,所以它可以加载外部的dex,因为这个dex会被复制到内部路径的optimizedDirectory;而PathClassLoader没有optimizedDirectory,所以它只能加载内部的dex,这些大都是存在系统中已经安装过的apk里面的

package dalvik.system;
public class BaseDexClassLoader extends ClassLoader {

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

        if (reporter != null) {
            reporter.report(this.pathList.getDexPaths());
        }
    }

	public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
        // TODO We should support giving this a library search path maybe.
        super(parent);
        this.pathList = new DexPathList(this, dexFiles);
    }
	
	@Override public String toString() {
        return getClass().getName() + "[" + pathList + "]";
    }
}
  • BaseDexClassLoader的相关操作都是委托DexPathList执行的
	@Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        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;
    }
	
	@Override
    protected URL findResource(String name) {
        return pathList.findResource(name);
    }

    @Override
    protected Enumeration<URL> findResources(String name) {
        return pathList.findResources(name);
    }
	
	@Override
    public String findLibrary(String name) {
        return pathList.findLibrary(name);
    }

3.4.1 ClassLoader加载class的过程

BaseDexClassLoader中有个pathList对象,pathList中包含一个DexFile的数组dexElements

  • dexElements数组就是odex文件的集合
    • odex文件是 dexPath指向的原始dex(.apk,.zip,.jar等)文件在optimizedDirectory文件夹中生成相应的优化后的文件
    • 如果不分包一般这个数组只有一个Element元素,也就只有一个DexFile文件

对于类加载呢,就是遍历这个集合,通过DexFile去寻找,并最终调用native方法的defineClass

#BaseDexClassLoader  
@Override  
protected Class<?> findClass(String name) throws ClassNotFoundException {   
    Class clazz = pathList.findClass(name);  
    if (clazz == null) {   
        throw new ClassNotFoundException(name);   
    }   
    return clazz;  
}  

#DexPathList  
public Class findClass(String name) {   
    for (Element element : dexElements) {   
        DexFile dex = element.dexFile;  
        if (dex != null) {   
            Class clazz = dex.loadClassBinaryName(name, definingContext);   
          if (clazz != null) {   
              return clazz;   
          }   
        }   
    }   
    return null;  
}  

#DexFile  
public Class loadClassBinaryName(String name, ClassLoader loader) {   
    return defineClass(name, loader, mCookie);  
}  
private native static Class defineClass(String name, ClassLoader loader, int cookie);  

3.4.2 DexPathList

构造过程
  • DexPathList 的构造方法
    • String dexPath:之前传进来的包含 dex 的 apk、jar、dex 的路径集
    • String libraryPath:native 库的路径集
    • File optimizedDirectory: 缓存优化的 odex 文件的路径
final class DexPathList {  

	private static final String DEX_SUFFIX = ".dex";
	private static final String zipSeparator = "!/";
	
	private final ClassLoader definingContext;
	private Element[] dexElements;
	private final NativeLibraryElement[] nativeLibraryPathElements;
   
	public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {
		
		...
		this.definingContext = definingContext;
		this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext);


	}

}
  • 调用 makePathElements() 方法生成一个Element[] dexElements 数组
    • Element 是 DexPathList 的一个嵌套类
static class Element {
	private final File dir;
	private final boolean isDirectory;
	private final File zip;
	private final DexFile dexFile;
	private ZipFile zipFile;
	private boolean initialized;
}

rivate static Element[] makePathElements(List<File> files, File optimizedDirectory,
                                          List<IOException> suppressedExceptions) {
    List<Element> elements = new ArrayList<>();
    // 遍历所有的包含 dex 的文件
    for (File file : files) {
        File zip = null;
        File dir = new File("");
        DexFile dex = null;
        String path = file.getPath();
        String name = file.getName();
        // 判断是不是 zip 类型
        if (path.contains(zipSeparator)) {
            String split[] = path.split(zipSeparator, 2);
            zip = new File(split[0]);
            dir = new File(split[1]);
        } else if (file.isDirectory()) {
            // 如果是文件夹,则直接添加 Element,这个一般是用来处理 native 库和资源文件
            elements.add(new Element(file, true, null, null));
        } else if (file.isFile()) {
            // 直接是 .dex 文件,而不是 zip/jar 文件(apk 归为 zip),则直接加载 dex 文件
            if (name.endsWith(DEX_SUFFIX)) {
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException ex) {
                    System.logE("Unable to load dex file: " + file, ex);
                }
            } else {
                // 如果是 zip/jar 文件(apk 归为 zip),则将 file 值赋给 zip 字段,再加载 dex 文件
                zip = file;
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException suppressed) {
                    suppressedExceptions.add(suppressed);
                }
            }
        } else {
            System.logW("ClassLoader referenced unknown path: " + file);
        }
        if ((zip != null) || (dex != null)) {
            elements.add(new Element(dir, false, zip, dex));
        }
    }
    // list 转为数组
    return elements.toArray(new Element[elements.size()]);
}
private static DexFile loadDexFile(File file, File optimizedDirectory) throws IOException {
    if (optimizedDirectory == null) {
        return new DexFile(file);
    } else {
        String optimizedPath = optimizedPathFor(file, optimizedDirectory);
        return DexFile.loadDex(file.getPath(), optimizedPath, 0);
    }
} 

/**
 * Converts a dex/jar file path and an output directory to an      
 * output file path for an associated optimized dex file.
 */
private static String optimizedPathFor(File path, File optimizedDirectory) {
    String fileName = path.getName();
    if (!fileName.endsWith(DEX_SUFFIX)) {
        int lastDot = fileName.lastIndexOf(".");
        if (lastDot < 0) {
            fileName += DEX_SUFFIX;
        } else {
            StringBuilder sb = new StringBuilder(lastDot + 4);
            sb.append(fileName, 0, lastDot);
            sb.append(DEX_SUFFIX);
            fileName = sb.toString();
        }
    }
    File result = new File(optimizedDirectory, fileName);
    return result.getPath();
}
加载类的过程
  • 【核心】DexPathList 的 findClass() 方法,其根据传入的完整的类名来加载对应的 class
public Class findClass(String name, List<Throwable> suppressed) {
	// 遍历 dexElements 数组,依次寻找对应的 class,一旦找到就终止遍历
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;
        if (dex != null) {
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    // 抛出异常
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
} 

这里有关于热修复实现的一个点,就是将补丁 dex 文件放到 dexElements 数组前面,这样在加载 class 时,优先找到补丁包中的 dex 文件,加载到 class 之后就不再寻找,从而原来的 apk 文件中同名的类就不会再使用,从而达到修复的目的,虽然说起来较为简单,但是实现起来还有很多细节需要注意

  • loadClassBinaryName中调用了Native方法defineClass()加载类
    • 标准JVM中,ClassLoader是用defineClass加载类的,而Android中defineClass被弃用了,改用了loadClass方法,而且加载类的过程也挪到了DexFile中,在DexFile中加载类的具体方法也叫defineClass,相信这也是维护代码可读性
public Class loadClassBinaryName(String name, ClassLoader loader) {
        return defineClass(name, loader, mCookie);
}

private native static Class defineClass(String name, ClassLoader loader, int cookie);

至此,BaseDexClassLader 寻找 class 的路线就清晰了:

  1. 当传入一个完整的类名,调用 BaseDexClassLader 的 findClass(String name) 方法
  2. BaseDexClassLader 的 findClass 方法会交给 DexPathList 的 findClass(String name, List<Throwable> suppressed 方法处理
  3. 在 DexPathList 方法的内部,会遍历 dexFile ,通过 DexFile 的 dex.loadClassBinaryName(name, definingContext, suppressed) 来完成类的加载

3.4.3 DexClassLoader

DexClassLoader可以加载dex文件以及包含dex的apk文件或jar文件,也支持从SD卡进行加载,这也就意味着DexClassLoader可以在应用未安装的情况下加载dex相关文件。因此,它是热修复和插件化技术的基础

上面说dalvik不能直接识别jar,DexClassLoader却可以加载jar文件,这难道不矛盾吗?其实在BaseDexClassLoader里对".jar",".zip",".apk",".dex"后缀的文件最后都会生成一个对应的dex文件,所以最终处理的还是dex文件,而URLClassLoader并没有做类似的处理

  • DexClassLoader 也继承自BaseDexClassLoader ,方法实现也都在BaseDexClassLoader中

libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java

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

我们再次强调下 String librarySearchPath 的意义:

我们知道应用程序第一次被加载的时候,为了提高以后的启动速度和执行效率,Android系统会对dex相关文件做一定程度的优化,并生成一个ODEX文件,此后再运行这个应用程序的时候,只要加载优化过的ODEX文件就行了,省去了每次都要优化的时间,而参数optimizedDirectory就是代表存储ODEX文件的路径,这个路径必须是一个内部存储路径。

3.4.4 PathClassLoader

Android系统使用PathClassLoader来加载系统类和应用程序的类,如果是加载非系统应用程序类,则会加载data/app/目录下的dex文件以及包含dex的apk文件或jar文件,不管是加载那种文件,最终都是要加载dex文件

也就是说,在 Android 中,App 安装到手机后,apk 里面的 class.dex 中的 class 均是通过 PathClassLoader 来加载的

  • PathClassLoader不建议开发直接使用
  • PathClassLoader继承自BaseDexClassLoader,很明显PathClassLoader的方法实现都在BaseDexClassLoader中。
  • 从PathClassLoader的构造方法也可以看出它遵循了双亲委托模式

libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java

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);
    }
}

PathClassLoader的构造方法有三个参数:

  1. dexPath:dex文件以及包含dex的apk文件或jar文件的路径集合,多个路径用文件分隔符分隔,默认文件分隔符为‘:’。
  2. librarySearchPath:包含 C/C++ 库的路径集合,多个路径用文件分隔符分隔分割,可以为null。
  3. parent:ClassLoader的parent [双亲委托模式]

PathClassLoader没有参数optimizedDirectory,这是因为PathClassLoader已经默认了参数optimizedDirectory的路径为:/data/dalvik-cache

很多博客里说PathClassLoader只能加载已安装的apk的dex,其实这说的应该是在dalvik虚拟机上,在art虚拟机上PathClassLoader可以加载未安装的apk的dex(在art平台上已验证),然而在/data/dalvik-cache 确未找到相应的dex文件,怀疑是art虚拟机判断apk未安装,所以只是将apk优化后的odex放在内存中,之后进行释放,这只是个猜想,希望有知道的可以告知一下。因为dalvik上无法使用,所以我们也没法使用

四、ClassLoader的实例化顺序

BootClassLoader是在Zygote进程的入口方法中创建的,PathClassLoader则是在Zygote进程创建SystemServer进程时创建的,查找路径为java.library.path

4.1 BootClassLoader的创建

BootClassLoader是在何时被创建的呢?这得先从Zygote进程开始说起,不了解Zygote进程的可以查看(4.1.52)Android启动流程分析

ZygoteInit的main方法如下所示

frameworks/base/core/java/com/android/internal/os/ZygoteInit.java

 public static void main(String argv[]) {
   ...
        try {
             ...
                preload(bootTimingsTraceLog);
             ... 
        }
    }
	
	

main方法是ZygoteInit入口方法,其中调用了ZygoteInit的preload方法,preload方法中又调用了ZygoteInit的preloadClasses方法,如下所示。

frameworks/base/core/java/com/android/internal/os/ZygoteInit.java

private static void preloadClasses() {
        final VMRuntime runtime = VMRuntime.getRuntime();
        InputStream is;
        try {
            is = new FileInputStream(PRELOADED_CLASSES);//【1】
        } catch (FileNotFoundException e) {
            Log.e(TAG, "Couldn't find " + PRELOADED_CLASSES + ".");
            return;
        }
        ...
        try {
            BufferedReader br
                = new BufferedReader(new InputStreamReader(is), 256);//【2】

            int count = 0;
            String line;
            while ((line = br.readLine()) != null) {//【3】
                line = line.trim();
                if (line.startsWith("#") || line.equals("")) {
                    continue;
                }
                  Trace.traceBegin(Trace.TRACE_TAG_DALVIK, line);
                try {
                    if (false) {
                        Log.v(TAG, "Preloading " + line + "...");
                    }
                    Class.forName(line, true, null);//【4】
                    count++;
                } catch (ClassNotFoundException e) {
                    Log.w(TAG, "Class not found for preloading: " + line);
                } 
        ...
        } catch (IOException e) {
            Log.e(TAG, "Error reading " + PRELOADED_CLASSES + ".", e);
        } finally {
            ...
        }
    }

preloadClasses方法用于Zygote进程初始化时预加载常用类

  1. 注释1处】将/system/etc/preloaded-classes文件封装成FileInputStream,preloaded-classes文件中存有预加载类的目录,这个文件在系统源码中的路径为frameworks/base/preloaded-classes.

这里列举一些preloaded-classes文件中的预加载类名称,如下所示

android.app.ApplicationLoaders
android.app.ApplicationPackageManager
android.app.ApplicationPackageManager$OnPermissionsChangeListenerDelegate
android.app.ApplicationPackageManager$ResourceName
android.app.ContentProviderHolder
android.app.ContentProviderHolder$1
android.app.ContextImpl
android.app.ContextImpl$ApplicationContentResolver
android.app.DexLoadReporter
android.app.Dialog
android.app.Dialog$ListenersHandler
android.app.DownloadManager
android.app.Fragment

可以看到preloaded-classes文件中的预加载类的名称有很多都是我们非常熟知的。预加载属于拿空间换时间的策略,Zygote环境配置的越健全越通用,应用程序进程需要单独做的事情也就越少,预加载除了预加载类,还有预加载资源和预加载共享库,因为不是本文重点,这里就不在延伸讲下去了。

  1. 注释2处】,将FileInputStream封装为BufferedReade
  2. 注释3处】遍历BufferedReader,读出所有预加载类的名称,每读出一个预加载类的名称就调用【注释4处】的代码加载该类,Class的forName方法如下所示

libcore/ojluni/src/main/java/java/lang/Class.java

 @CallerSensitive
 public static Class<?> forName(String name, boolean initialize,
                                   ClassLoader loader)
        throws ClassNotFoundException
    {
        if (loader == null) {
            loader = BootClassLoader.getInstance();//【1】创建了BootClassLoader,并将BootClassLoader实例传入到了注释2处的classForName方法中
        }
        Class<?> result;
        try {
            result = classForName(name, initialize, loader);//【2】 classForName方法是Native方法,它的实现由c/c++代码来完成
        } catch (ClassNotFoundException e) {
            Throwable cause = e.getCause();
            if (cause instanceof LinkageError) {
                throw (LinkageError) cause;
            }
            throw e;
        }
        return result;
    }
	
 @FastNative
 static native Class<?> classForName(String className, boolean shouldInitialize,
            ClassLoader classLoader) throws ClassNotFoundException;

4.2 PathClassLoader的创建

PathClassLoader的创建也得从Zygote进程开始说起,Zygote进程启动SyetemServer进程时会调用ZygoteInit的startSystemServer()方法,如下所示。

frameworks/base/core/java/com/android/internal/os/ZygoteInit.java

private static boolean startSystemServer(String abiList, String socketName)
           throws MethodAndArgsCaller, RuntimeException {
    ...
        int pid;
        try {
            parsedArgs = new ZygoteConnection.Arguments(args);//2
            ZygoteConnection.applyDebuggerSystemProperty(parsedArgs);
            ZygoteConnection.applyInvokeWithSystemProperty(parsedArgs);
            /*1*/
            pid = Zygote.forkSystemServer(
                    parsedArgs.uid, parsedArgs.gid,
                    parsedArgs.gids,
                    parsedArgs.debugFlags,
                    null,
                    parsedArgs.permittedCapabilities,
                    parsedArgs.effectiveCapabilities);
        } catch (IllegalArgumentException ex) {
            throw new RuntimeException(ex);
        }
       if (pid == 0) {//【2】
           if (hasSecondZygote(abiList)) {
               waitForSecondaryZygote(socketName);
           }
           handleSystemServerProcess(parsedArgs);//【3】
       }
       return true;
   }

注释1处,Zygote进程通过forkSystemServer()方法fork自身创建子进程(SystemServer进程)。注释2处如果forkSystemServer方法返回的pid等于0,说明当前代码是在新创建的SystemServer进程中执行的,接着就会执行注释3处handleSystemServerProcess()方法:

frameworks/base/core/java/com/android/internal/os/ZygoteInit.java

 private static void handleSystemServerProcess(
            ZygoteConnection.Arguments parsedArgs)
            throws Zygote.MethodAndArgsCaller {

    ...
        if (parsedArgs.invokeWith != null) {
           ...
        } else {
            ClassLoader cl = null;
            if (systemServerClasspath != null) {
                cl = createPathClassLoader(systemServerClasspath, parsedArgs.targetSdkVersion);//【4】
                Thread.currentThread().setContextClassLoader(cl);
            }
            ZygoteInit.zygoteInit(parsedArgs.targetSdkVersion, parsedArgs.remainingArgs, cl);
        }
    }

注释4处调用了createPathClassLoader()方法,如下所示

//frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
 static PathClassLoader createPathClassLoader(String classPath, int targetSdkVersion) {
      String libraryPath = System.getProperty("java.library.path");
      return PathClassLoaderFactory.createClassLoader(classPath,
                                                      libraryPath,
                                                      libraryPath,
                                                      ClassLoader.getSystemClassLoader(),
                                                      targetSdkVersion,
                                                      true /* isNamespaceShared */);
    }
	
//createPathClassLoader方法中又会调用PathClassLoaderFactory的createClassLoader方法,看来PathClassLoader是用工厂来进行创建的
//frameworks/base/core/java/com/android/internal/os/PathClassLoaderFactory.java
 public static PathClassLoader createClassLoader(String dexPath,
                                                    String librarySearchPath,
                                                    String libraryPermittedPath,
                                                    ClassLoader parent,
                                                    int targetSdkVersion,
                                                    boolean isNamespaceShared) {
        PathClassLoader pathClassloader = new PathClassLoader(dexPath, librarySearchPath, parent);
      ...
        return pathClassloader;
    }


在PathClassLoaderFactory的createClassLoader方法中会创建PathClassLoader

另外,在 ApplicationLoaders 中用来加载系统安装过的 apk,用来加载 apk 内的 class ,其调用是在 LoadApk 类中的 getClassLoader() 方法中调用的,得到的也是 PathClassLoader

mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip, lib,
        mBaseClassLoader);

五、自定义ClassLoader

5.1 示例之 SD卡加载

从 SD 卡中动态加载一个包含 class.dex 的 jar 文件,加载其中的类,并调用其方法

  • 新建一个 Java 项目,包含两个文件:ISayHello.javaHelloAndroid.java.
package com.jaeger;
public interface ISayHello {
   String say();
}
   
package com.jaeger;
public class HelloAndroid implements ISayHello {
   @Override
   public String say() {
	   return "Hello Android";
   }
}
  • 导出 jar 包
  • 使用 SDK 目录 > platform-tools 里面的 dx 工具生成包含 class.dex 的 jar 包
    • 将上一步生成的 sayhello.jar 放到 你的 SDK 下的 platform-tools 文件夹下,使用下面的命令生成 dex 化的 jar 文件,其中是 output 后面的sayhello_dex.jar 就是最终生成的 jar 包
    • dx --dex --output=sayhello_dex.jar sayhello.jar
  • 将 sayhello_dex.jar 文件拷贝到手机存储空间的根目录,不一定是内存卡
  • 新建一个 Android 项目,在 MainActivity 中添加如下的代码:
    • DexClassLoader并不能直接加载外部存储的.dex文件,而是要先拷贝到内部存储里。这里的dexPath就是.dex的外部存储路径,而optimizedDirectory则是内部路径,libraryPath用null即可,parent则是要传入当前应用的ClassLoader,这与ClassLoader的“双亲代理模式”有关
package com.jaeger;
public interface ISayHello {
    String say();
}

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 = (TextView) findViewById(R.id.tv_info);
        mBtnLoad = (Button) 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");
                   
                // 如果没有读权限,确定你在 AndroidManifest 中是否声明了读写权限
                Log.d(TAG, jarFile.canRead() + "");

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

				//]【核心!!!!】jarFile.getAbsolutePath()
                // getCodeCacheDir() 方法在 API 21 才能使用,实际测试替换成 getExternalCacheDir() 等也是可以的
                // 只要有读写权限的路径均可
                DexClassLoader dexClassLoader =
                    new DexClassLoader(jarFile.getAbsolutePath(), getExternalCacheDir().getAbsolutePath(), null, getClassLoader());
                try {
                    // 加载 HelloAndroid 类
                    Class clazz = dexClassLoader.loadClass("com.jaeger.HelloAndroid");
                    // 强转成 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();
                }
            }
        });
    }
}

这里需要注意几点:

  • 因为需要从存储空间中读取 jar 文件,需要在 AndroidManifest 中声明读写权限
  • ISayHello 接口的包名必须一致
  • getCodeCacheDir() 方法在 API 21 才能使用,实际测试替换成 getExternalCacheDir() 等也是可以的
  • 接下来就是运行,运行的结果如图,和预期的一样,完美收工。

示例代码以及 jar 包上传到 GitHub 了,请前往 laobie/TestClassLoader 去查看

其他

ART虚拟机的兼容性问题

Android Runtime(缩写为ART),在Android 5.0及后续Android版本中作为正式的运行时库取代了以往的Dalvik虚拟机。

ART能够把应用程序的字节码转换为机器码,是Android所使用的一种新的虚拟机。它与Dalvik的主要不同在于:

  • Dalvik采用的是JIT技术,字节码都需要通过即时编译器(just in time ,JIT)转换为机器码,这会拖慢应用的运行效率
  • ART采用Ahead-of-time(AOT)技术,应用在第一次安装的时候,字节码就会预先编译成机器码,这个过程叫做预编译。
    • 即使用Android系统自带的dex2oat工具把APK里面的.dex文件转化成OAT文件,OAT文件是一种Android私有ELF文件格式,它不仅包含有从DEX文件翻译而来的本地机器指令,还包含有原来的DEX文件内容
      ART同时也改善了性能、垃圾回收(Garbage Collection)、应用程序除错以及性能分析。但是请注意,运行时内存占用空间较少同样意味着编译二进制需要更高的存储

ART模式的系统里,同样存在DexClassLoader类,包名路径也没变,只不过它的具体实现与原来的有所不同,但是接口是一致的。实际上,ART运行时就是和Dalvik虚拟机一样,实现了一套完全兼容Java虚拟机的接口

动态加载类中的缓存干扰问题

如果你希望通过动态加载的方式,加载一个新版本的dex文件,使用里面的新类替换原有的旧类,从而修复原有类的BUG,那么你必须保证在加载新类的时候,旧类还没有被加载,因为如果已经加载过旧类,那么ClassLoader会一直优先使用旧类,因为会先命中缓存

如果旧类总是优先于新类被加载,我们也可以使用一个与加载旧类的ClassLoader没有树的继承关系的另一个ClassLoader来加载新类,因为ClassLoader只会检查其Parent有没有加载过当前要加载的类,如果两个ClassLoader没有继承关系,那么旧类和新类都能被加载

不过这样一来又有另一个问题了,在Java中,只有当两个实例的类名、包名以及加载其的ClassLoader都相同,才会被认为是同一种类型。上面分别加载的新类和旧类,虽然包名和类名都完全一样,但是由于加载的ClassLoader不同,所以并不是同一种类型,在实际使用中可能会出现类型不符异常

同一个Class = 相同的 ClassName + PackageName + ClassLoader

这个在采用动态加载功能的开发中容易出现,请注意

参考文献

identity 身份认证 购VIP最低享 7 折! triangle vip 30元优惠券将在 04:24:36 后过期 去使用 triangle 数据可视化是将复杂的数据集通过图表、图像等视觉元素进行呈现,以便于人们更容易地理解和解读数据。在“数据可视化期末课设~学生成绩可视化分析.zip”这个压缩包中,我们可以看到一系列与数据可视化相关的资源,包括Jupyter代码、HTML图片、答辩PPT以及Word文档,这些内容涵盖了数据可视化的基础到高级应用,适合于完成一个全面的期末课程设计项目。 Jupyter代码是使用Python编程语言进行数据处理和可视化的主要工具。在这个项目中,学生可能使用了pandas库来加载和清洗数据,可能涉及到的数据处理步骤包括去除重复值、处理缺失值以及数据类型转换等。接着,他们可能使用matplotlib或seaborn库来创建各种图表,如直方图、散点图、箱线图等,以展示学生成绩的分布、对比和趋势。此外,更高级的可视化库如plotly或bokeh可能也被用来实现交互式图表,增加用户对数据的理解深度。 保存的HTML图片是Jupyter Notebook的输出结果,它展示了代码运行后的可视化效果。这些图片可以直观地揭示学生成绩的统计特征,例如平均分、标准差、最高分和最低分等。通过颜色编码或者图例,我们可以识别出不同科目或者不同班级的表现,帮助分析教学质量和学生学习情况。 答辩PPT则可能包含项目的概述、目的、方法、结果和结论。在PPT中,学生可能会详细阐述他们选择特定可视化方法的理由,如何解读图表,以及从数据中得出的洞察。此外,PPT的制作也是展示其表达和沟通能力的重要部分,要求清晰、有逻辑地组织信息。 Word文档可能是项目报告,详细记录了整个过程,包括数据来源、预处理步骤、使用的可视化技术、分析结果以及可能遇到的问题和解决方案。报告中的数据分析部分会详细解释图表背后的含义,例如通过对比不同学科的分数分布,找出哪些科目可能存在困难,或者分析成绩与特定因素(如性别、年级等)的关系。 这个压缩包提供了完整的数据可视化项目实例,涉及了数据获取、处理、可视化和解释的一系列步骤,对于学习和掌握数据可视化技能非常有价值。通过这样的练习,学生不仅能够提高编程技巧,还能培养数据驱动思维和问题解决能力,为未来从事数据分析或相关领域的工作打下坚实的基础。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值