Android 动态加载dex

首先如果仅仅是因为64K method的问题可以直接看这里DexGuard、Proguard、Multi-dex给出的解决方案。

本文主要讨论从编译层面,dex动态加载器选择层面以及安全层面讨论dex动态加载


I. 类加载器

比较两个类是否相等: 前提是采用的是同样的加载器加载的,否则必不相等。

一般加载器类别

虚拟机的角度

1. 启动类加载器(Bootstrap ClassLoader)

使用C++语言实现,虚拟机自身的一部分。

2. 其他的类加载器

使用Java语言实现,独立于JVM外部,全部继承自类java.lang.ClassLoader

开发人员角度

1. 启动类加载器(Bootstrap ClassLoader)
  • 层级: native层实现。
  • 负责加载: JAVA_HOME\lib目录中能被JVM识别的类库加载到JVM内存中(名称不符合的类库不会被加载)(java的核心类,如 java.langjava.util等,是java运行时环境所需类的一部分, 如果是Android,还会加载Android sdk层,如TextUtils, TextView等)。
  • 使用关系: 无法被Java程序直接引用。
2. 扩展类加载器(Extersion ClassLoader)
  • 负责加载: JAVA_HOME\lib\ext目录中的类库(继承自java核心类的类)。
  • 使用关系: 可以被开发者直接使用。
3. 应用程序类加载器(Application ClassLoader)

系统类加载器。

  • 负责加载: 用户类路径(Classpath)上所指定的类库。Android中大多数的应用中的类(从odex中加载出来的类: 如 MainActivity、自定义View、XXXApplication)。
  • 使用关系: 可以被开发者直接使用。
  • 备注: 一般是应用程序默认的类加载器。

特性

什么是双亲委派模型?

一个类收到了类加载请求,会将请求先委派给父类加载,每层皆如此,因此所有的类加载是从上而下的,只有上层无法加载了才到下层加载。

参考ClassLoader中给出的解释:

1
2
3
4
Loads the class with the specified name, optionally linking it after loading. The following steps are performed:
1. Call findLoadedClass(java.lang.String) to determine if the requested class has already been loaded.
2. If the class has not yet been loaded: Invoke this method on the parent class loader.
3. If the class has still not been loaded: Call findClass(java.lang.String) to find the class.

为什么要遵循双亲委派模型?

为了保证所加载的类的唯一性,保证相同的类只会被一个加载器所加载。也保证了一个类不会被多个加载器加载到JVM中,导致同一个类在JVM被不同的加载器加载多次。

Dalvik虚拟机的类加载器与其他Java虚拟机的不同?

一般的Java虚拟机,是自定义继承自ClassLoader的类加载器,然后通过defineClass方法从二进制流中加载Class,或者从Class文件中读取。而Dalvik虚拟机是阉割以及修改过的,无法从二进制流中加载,Dalvik只识别dex文件,因此我们能加载的只是dex文件或包含dex文件的.jar.apk

其他梗

  • 当java源码被编译为binary class时,编译器会插入一个静态常量final static java.lang.Class class,因为任意类我们都可以通过.class获得当前类的java.lang.Class
  • java中的静态对象是attach到对应的class上面的,也就是attach在该class的ClassLoader上面的,当ClassLoader被unload的时候,如果再次访问该静态对象,就是一个全新的静态对象在一个新的ClassLoader上了。
  • 可以通过Class.getClassLoader()来获得该类的ClassLoader

II. Android 动态加载Dex的方式

DexFile

Android中的这几种类加载器实际是依赖DexFile的,对于DexFile有以下两点:

  1. 打开的DEX文件不会直接存储在DexFile对象中,而是存储在对于虚拟机只读的memory-mapped上。
  2. 我们无法直接调用DexFile.loadClass进行对dex的加载,只能通过ClassLoader进行加载。

案例参考:

III. 编译层面实现打指定独立dex

Ant

可以参考这里后面的Build Process

Gradle

在编译层面将指定的module拆分出来打包成dex放入assets中,完全可以参考这个方案: secondary-dex-gradle/app/build.gradle

如果不理解的可以看我fork的,我添加了中文注解: Jacksgong/secondary-dex-gradle/app/build.gradle

IV. 安全性讨论

动态加载Dex的安全性主要存在两方面:

  1. 存储dex的文件暴露在其他应用可读写的目录下。
  2. 加载外部dex的时候没有做好完整的安全性校验。

解决方案

  1. 尽量将dex放到当前应用的私有目录下,保证只有当前应用uid可以读甚至写(一般就只有Context.getFileDir()/ Context.getDir(String, MODE_PRIVATE) / Context.getCacheDir()),这方面目录相关知识可以参看: Android中尽量不用Storage Permission
  2. 对从服务端下载或者外部加载的dex,做校验(对文件进行哈希值校验等)。
  3. 将dex文件加密,通过JNI将解密代码写在Native层,解密之后通过defineClass指定路径加载完成后,删除解密后文件。


Jacksgong wechat
欢迎关注Jacks Blog公众号,第一时间接收原创技术沉淀干货。