上篇文章讲解了《类加载的原理》,本篇是《动态加载Jar/Dex》实战篇。
同一个Class = ClassName + PackageName + ClassLoaderId(instance)
只要是写在Eclipse中的类(其实就是指classPath)都是被AppClassLoader加载的,其他以classLoader.load("com.xx.xx") 形式的都是自定义ClassLoader 经过 处理的。
知道这一点是很重要的,后面我们自定义ClassLoader可以利用该特性。
同理,Android中能够写在 Android Studio中的代码 (classPath)都是被PathClassLoader加载的,其他以classLoader.load("com.xx.xx") 形式的都是自定义ClassLoader 经过 处理的。
所以一般情况下Java和Android我们经常利用的是
Java -> AppClassLoader
Android -> PathClassLoader
而自定义ClassLoader利用的是
Java -> URLClassLoader(可选) or ClassLoader的子类
Android -> DexClassLoader(可选) or ClassLoader的子类
动态加载方案(Java版)
- 反射方式
插件类全部写在远端,然后用自定义ClassLoader加载,只留一个Object引用(Object由super.loadClass() 让parent处理), 然后用反射调用插件类的方法。这种比较简单,案例后面代码会给出。 - 接口方式
本地项目里面的留一个接口类,远端实现该接口的方法,然后打包远端实现类和接口类,经过测试,Java提供给我们的URLClassLoader不需要去除jar里面的接口类的,原理就是利用双亲委派 优先使用parent加载项目里面的接口类,所以才能够多态引用到该jar中的实现类。
再来试试我们自己的解决方案
待加载的类Dog
package com.less.bean;
public interface Animal {
void say();
}
package com.less.bean;
public class Dog implements Animal {
@Override
public void say() {
System.out.println("I am a Dog");
}
}
文件加载ClassLoader
public class FileBadClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
File file = new File("F:/" + fileName);
if(!file.exists()){
System.out.println("========> 没找到文件,使用默认逻辑加载 " + name);
return super.loadClass(name);
}else{
System.out.println("========> 找到文件 ,开始加载 " + name);
InputStream inputStream = new FileInputStream(file);
byte[] data = new byte[inputStream.available()];
inputStream.read(data);
inputStream.close();
// Android和Java的重要实现区别就在于这里,Android不支持直接加载.class或.jar,而是.dex,所以被修改为能够动态加载dex的逻辑。
return defineClass(name, data, 0, data.length);
}
} catch (Exception e) {
throw new ClassNotFoundException(name);
}
}
}
这是一个破坏了双亲委派模式的 自定义ClassLoader,加载顺序和双亲委派 正好相反。
检测磁盘是否存在我们先要加载的class文件,如果存在就自己加载,不存在就交给super.loadClass();默认逻辑处理。
public static void main(String[] args){
FileBadClassLoader badClassLoader = new FileBadClassLoader ();
Class<?> clazz = badClassLoader.loadClass("com.less.bean.Dog");
Animal animal = (Animal) clazz.newInstance();
animal.say();
}
如图所示,F盘存在两个class,Animal(接口)和Dog(实现类)
运行main,结果报了一个ClassCastException。
为什么会出现这种错
Animal animal = (Animal) clazz.newInstance(); 不是正常引用吗?
根据上面的分析,这个错误很容易判断,因为Animal和Dog都存在F盘,所以都被FileBadClassLoader加载了,而 上面有个结论 提到 写在Eclipse代码中的类(classpath)都是被APPClassLoader加载的,这行代码用了两个不同的ClassLoader实例,所以Animal animal = (Animal) 这里的引用和clazz.newInstance()不是一个类型的,所以不能互相转换。
如何解决
删除F盘中的Animal.class即可,删除后FileBadClassLoader找不到Animal.class就交给APPClassLoader加载,加载成功后
Animal animal = (Animal) clazz.newInstance()就可以相互引用了。
备注:贴上测试代码
public class Main {
/**
* 个人吐槽下,ClassLoader的加载类的loadClass和findClass方法的名称互换下感觉更贴切些,毕竟名字和代码逻辑反了,搞得有时候犯迷糊。find是先找后加载,注意下这里就行了。
* 注: 即使是自己实现的类加载器,如果myClassLoader.loadClass(clazz);加载的clazz被parent加载了,那么clazz.getClassLoader()就是parent而不是myClassLoader.
* @throws Exception
*/
public static void main(String[] args) throws Exception {
testIsSameClassLoader();
// testIsSameClass();
// testObeyParent1();
// testObeyParent2();
// testDisObeyParent();
// testDynamicByReflect();
// testDynamicByInterface1();
// testDynamicByInterface2();
}
private static void testIsSameClassLoader() {
ClassLoader classLoader1 = ClassLoader.getSystemClassLoader();
ClassLoader classLoader2 = Main.class.getClassLoader();
ClassLoader classLoader3 = Cat.class.getClassLoader();
ClassLoader classLoader4 = ClassLoader.class.getClassLoader();
System.out.println(classLoader1 == classLoader2);
System.out.println(classLoader2 == classLoader3);
System.out.println(classLoader4);// BootstrapClassLoader
}
private static void testIsSameClass() throws ClassNotFoundException {
// 测试两个ClassLoader加载一个类的关系。
ClassLoader badclassLoader1 = new BadClassLoader();
ClassLoader badclassLoader2 = new BadClassLoader();
Class<?> badClazz1 = badclassLoader1.loadClass("com.less.bean.Cat");
Class<?> badClazz2 = badclassLoader2.loadClass("com.less.bean.Cat");
// 说明判断两个类是否是同一个类型的前提是: 同类 + 同包 + 同类加载器实例,即使两者都是加载远端的同一个类文件,但是却不是一个类型,故无法强制转换或者互相引用等等。
System.out.println(badClazz1 == badClazz2);
}
private static void testObeyParent1() throws Exception {
// 通常推荐重写的是findClass,而不是loadClass, 因为双亲委派的具体逻辑就写在loadClass中,loadClass的逻辑里如果parent加载失败,则会调用我们重写的findClass
// 完成加载,这样就可以保证新写出的类加载器是符合双亲委派规则的,保证了各个类加载器基础类的 统一问题(越基础的类越有上层的类加载器加载)。
// 但是双亲委派也是可以破坏掉的,常见的使用场景就是热修复,OGSi是这方面非常好的应用。
// 无参的ClassLoader会默认设置 getSystemClassLoader() 即AppClassLoader为parent,见ClassLoader的构造器源码。
ClassLoader mygoodClassLoader1 = new ClassLoader() {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
return super.findClass(name);
}
};
System.out.println("mygoodClassLoader1 ---> " + mygoodClassLoader1.getParent());
Class<?> clazz1 = mygoodClassLoader1.loadClass("com.less.bean.Person");
Object obj1 = clazz1.newInstance();
message("[real classLoader] clazz1 => " + obj1.getClass().getClassLoader());
}
private static void testObeyParent2() {
// 但是如果我们设置ClassLoader的parent为null,那么就没有parent替我们找了,然后直接交给BootstrapClassLoader找,肯定也找不到了,最后我们自己找好了,
// 结果发现ClassLoader.findClass默认实现只是抛出一个异常,throw new ClassNotFoundException(name);所以要想要我们自己的ClassLoader能够加载类就必须实现findClass。
ClassLoader mygoodClassLoader2 = new ClassLoader(null) {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 这里默认实现是--> throw new ClassNotFoundException(name);
return super.findClass(name);
}
};
System.out.println("mygoodClassLoader2 ---> " + mygoodClassLoader2.getParent());
try {
Class<?> clazz2 = mygoodClassLoader2.loadClass("com.less.bean.Dog");
Object obj2 = clazz2.newInstance();
message("[real classLoader] clazz2 => " + obj2.getClass().getClassLoader());
} catch (Exception e) {
message("[real classLoader] clazz2 => " + e.toString());
}
}
private static void testDisObeyParent() throws Exception {
// 破坏双亲委派,直接自己加载
ClassLoader mybadClassLoader = new BadClassLoader();
System.out.println("mybadClassLoader ---> " + mybadClassLoader.getParent());
Class<?> clazz3 = mybadClassLoader.loadClass("com.less.bean.Cat");
Object obj3 = clazz3.newInstance();
message("[real classLoader] clazz3 => " + obj3.getClass().getClassLoader());
/********************** 测试classpath(即我们自己的项目package下的类 和 远程加载的类是否相等) **********************
* 分析: 从BadClassLoader打印的日志可以看出,动态加载一个类的时候,此类里面关联的类(如成员变量,extend,局部变量等等)都会交给此ClassLoader的loadClass进行处理。
* 而且如果没有任何继承的情况下,其[隐式父类Object]都会交给其处理,这时候我们需要把这个Object或它包含的类 都交给 parent处理,否则我们这里的代码都没法有类型去引用这个生成的实例。
* 所有 Java 应用都至少需要引用 java.lang.Object类,也就是说在运行的时候,java.lang.Object这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object类
* 而且这些类之间是不兼容的。对于 Java 核心库的类的加载工作由引导类加载器来统一完成,需要保证Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。
***********************************************************************************************/
System.out.println(obj3.getClass().getClassLoader());
System.out.println(Cat.class.getClassLoader());
System.out.println(obj3 instanceof Cat);
}
private static void testDynamicByReflect() throws Exception {
// URLClassLoader 只能加载jar文件,可以替代我们自定义的ClassLoader加载远程jar,Android也给我们提供了DexClassLoader来实现动态加载dex.
File file = new File("F:/Monkey.jar");
URL url = file.toURL();
URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { url });
Class<?> clazz = urlClassLoader.loadClass("com.less.bean.Monkey");
Object monkey = clazz.newInstance();
Method method = clazz.getDeclaredMethod("say");
method.invoke(null);// 使用静态方法
method.invoke(monkey);// 使用对象调用
}
private static void testDynamicByInterface1() throws Exception {
BadClassLoader badClassLoader = new BadClassLoader();
Class<?> clazz = badClassLoader.loadClass("com.less.bean.Dog");
Animal animal = (Animal) clazz.newInstance();
animal.say();
}
private static void testDynamicByInterface2() throws Exception {
File file = new File("F:/Dog.jar");
URL url = file.toURL();
URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { url });
Class<?> clazz = urlClassLoader.loadClass("com.less.bean.Dog");
Animal animal = (Animal) clazz.newInstance();
animal.say();
}
private static void message(String string) {
StringBuilder builder = new StringBuilder();
builder.append("\r\n");
builder.append("[********************* ");
builder.append(string);
builder.append(" *********************]");
builder.append("\r\n");
System.out.println(builder.toString());
}
static class BadClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
File file = new File("F:/" + fileName);
if(!file.exists()){
System.out.println("========> 没找到文件,使用默认逻辑加载 " + name);
return super.loadClass(name);
}else{
System.out.println("========> 找到文件 ,开始加载 " + name);
InputStream inputStream = new FileInputStream(file);
byte[] data = new byte[inputStream.available()];
inputStream.read(data);
inputStream.close();
// Android和Java的重要实现区别就在于这里,Android不支持直接加载.class或.jar,而是.dex,所以被修改为能够动态加载dex的逻辑。
return defineClass(name, data, 0, data.length);
}
} catch (Exception e) {
throw new ClassNotFoundException(name);
}
}
}
}
动态加载(Android版)
经过Java版的测试,我们基本上使用URLClassLoader即可解决大部分需求。
但是经过Android版的测试,发现直接使用DexClassLoader加载类(接口方式调用)却跟我们上面自定义的FileClassLoader一样的结果。
远端没有去掉接口文件,调用报如下错误:
Class resolved by unexpected DEX: Lcom/less/plugin/Dog;(0x94f51010):0x87a01000 ref [Lcom/less/plugin/Animal;] Lcom/less/plugin/Animal;(0x94f08288):0x84988000
W/dalvikvm: (Lcom/less/plugin/Dog; had used a different Lcom/less/plugin/Animal; during pre-verification)
I/dalvikvm: Failed resolving Lcom/less/plugin/Dog; interface 0 'Lcom/less/plugin/Animal;'
W/dalvikvm: Link of class 'Lcom/less/plugin/Dog;' failed
这个错误按前面的分析不会产生才会,因为DexClassLoader也是双亲委派。
File dexOutputDir = getDir("dex", 0);
DexClassLoader classLoader = new DexClassLoader(outPath, dexOutputDir.getAbsolutePath(), null, getClassLoader());
Class<?> clazz = classLoader.loadClass("com.less.plugin.Dog");
Animal animal = (Animal) clazz.newInstance();
分析:当加载插件Dog时,根据双亲委派模型,首先让parent(getClassLoader即PathClassLoader)加载,PathClassLoader并不能加载Dog,所以给了DexClassLoader加载,Dog被加载后,Dog类里面引用(implement)的Animal开始被加载,Animal存在于本地和远端,优先被PathClassLoader加载,故 Animal是可以成功引用Dog的,思路基本和URLClassLoader一致,且URLClassLoader没有任何问题。
请查看 http://androidxref.com/4.4.4_r1/xref/dalvik/vm/oo/Resolve.cpp
#include "Dalvik.h"
#include <stdlib.h>
/*
* Find the class corresponding to "classIdx", which maps to a class name
* string. It might be in the same DEX file as "referrer", in a different
* DEX file, generated by a class loader, or generated by the VM (e.g.
* array classes).
*
* Because the DexTypeId is associated with the referring class' DEX file,
* we may have to resolve the same class more than once if it's referred
* to from classes in multiple DEX files. This is a necessary property for
* DEX files associated with different class loaders.
*
* We cache a copy of the lookup in the DexFile's "resolved class" table,
* so future references to "classIdx" are faster.
*
* Note that "referrer" may be in the process of being linked.
*
* Traditional VMs might do access checks here, but in Dalvik the class
* "constant pool" is shared between all classes in the DEX file. We rely
* on the verifier to do the checks for us.
*
* Does not initialize the class.
*
* "fromUnverifiedConstant" should only be set if this call is the direct
* result of executing a "const-class" or "instance-of" instruction, which
* use class constants not resolved by the bytecode verifier.
*
* Returns NULL with an exception raised on failure.
*/
ClassObject* dvmResolveClass(const ClassObject* referrer, u4 classIdx,
bool fromUnverifiedConstant)
{
// 略
}
上面有一段注释关键注释:Because the DexTypeId ....
大致翻译为:因为DexTypeId是和DEX文件相关联的,我们必须防止相同的类被多个DEX文件引用,这是不同类加载器关联的DEX文件s的必须的特性。
struct DexTypeItem {
u2 typeIdx;// DexTypeId中的索引下标
};
//rect-mapped "type_list".
struct DexTypeList {
u4 size;// DexTypeItem的个数
DexTypeItem list[1];// DexTypeItem变长数组
};
解决方案
本地只保留接口,远端只保留实现类。
动态加载dex案例总结:
- Java的动态加载jar或类非常简单,你可以直接使用URLClassLoader或者灵活自定义ClassLoader。
- Android 基本DexClassLoader就足够了,但要注意上面提到的问题,接口和实现 必须只有一份。
- 因为使用动态加载,所以项目里面只能有接口,所以每次加载dex都需要下载,如果希望有一份默认的实现,推荐打包后的dex放入assets目录中,需要更新的时候再根据file.lastModified()判断是否从网络下载新的。
- 插件类 如果希望用到第三方库,如okhttp,一般建议在项目里面依赖okhttp,而打包插件的时候去除okhttp依赖即可。除非你十分确定,主项目不会使用某个库,总之确保主项目dex和插件 永远没有重复的类。
AS新建一个plugin library,利用gradle将非常方便生成Jar文件并dx化。
建一个接口文件
public interface Animal {
public interface Callback {
void done(String message);
}
void say(Callback callback);
}
创建实现类
public class Dog implements Animal {
OkHttpClient okhttp = new OkHttpClient();
public void say(final Callback callback) {
Builder builder = (new Builder()).url("http://www.baidu.com");
Call call = this.okhttp.newCall(builder.build());
call.enqueue(new okhttp3.Callback() {
public void onFailure(Call call, IOException e) {
callback.done("error");
}
public void onResponse(Call call, Response response) throws IOException {
String content = response.body().string();
callback.done(content);
}
});
}
}
在gradle中生成3个Task,分别用于以下用途:
- 打Jar包
- Jar包转为dex格式的Jar
- 删除Plugin Library的实现类
然后写个测试类就可以运行app了,下面测试类是把assets下的插件dynamic.jar-1.0.jar 写入SDcard,点击Button时 使用DexClassLoader动态加载即可,代码很简单。
public class TestActivity extends AppCompatActivity {
private static final String TAG = "less";
private String fileName = "dynamic.jar-1.0.jar";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
writeToApp();
}
public void handle(View view) {
try {
String outPath = Environment.getExternalStorageDirectory() + File.separator + fileName;
// 注意这个输出dex的路径需要在自己的目录里
File dexOutputDir = getDir("dex", 0);
DynamicClassLoader classLoader = new DynamicClassLoader(outPath, dexOutputDir.getAbsolutePath());
Class<?> clazz = classLoader.loadClass("com.less.plugin.Dog");
Animal animal = (Animal) clazz.newInstance();
animal.say(new Animal.Callback() {
@Override
public void done(String message) {
Log.i(TAG, " ===> " + message);
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
private void writeToApp() {
String outPath = Environment.getExternalStorageDirectory() + File.separator + fileName;
InputStream inputStream = null;
BufferedInputStream bufferedInputStream = null;
FileOutputStream fileOutputStream = null;
BufferedOutputStream bufferedOutputStream = null;
try {
inputStream = getResources().getAssets().open(fileName);
bufferedInputStream = new BufferedInputStream(inputStream);
fileOutputStream = new FileOutputStream(new File(outPath));
bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
byte[] buffer = new byte[1024];
int hasRead = -1;
while ((hasRead = bufferedInputStream.read(buffer) ) != -1) {
bufferedOutputStream.write(buffer,0,hasRead);
bufferedOutputStream.flush();
}
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
bufferedInputStream.close();
fileOutputStream.close();
bufferedInputStream.close();
inputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if (BuildConfig.DEBUG) {
Log.d(TAG, "写入成功");
}
}
}