Android能够实现动态加载机制,得益于java虚拟机团队设计的类加载,把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何获取所需要的的类,实现这个动作的代码模块称为“类加载器”(参考《深入理解Java虚拟机–JVM高级特性与最佳实践》7.4类加载器)。
一、Android中的ClassLoader
Java中的ClassLoader是加载class文件,而Android中的虚拟机无论是dvm还是art都只能识别dex文件。因此Java中的ClassLoader在Android中不适用。Android中的java.lang.ClassLoader这个类也不同于Java中的java.lang.ClassLoader。
为什么Android要自创dex,而不用java的class?
- dvm是基于寄存器的虚拟机 而jvm执行是基于虚拟栈的虚拟机。寄存器存取速度比栈快的多,dvm可以根据硬件实现最大的优化,比较适合移动设备。
- 传统Class文件是一个Java源码文件会生成一个.class文件,而Android是把所有Class文件进行合并,优化,然后生成一个最终的class.dex,目的是把不同class文件重复的东西只需保留一份,如果我们的Android应用不进行分dex处理,最后一个应用的apk只会有一个dex文件。
Android中的ClassLoader类型也可分为系统ClassLoader和自定义ClassLoader。其中系统ClassLoader包括3种分别是:
- BootClassLoader,Android系统启动时会使用BootClassLoader来预加载常用类,与Java中的Bootstrap ClassLoader不同的是,它并不是由C/C++代码实现,而是由Java实现的。BootClassLoader是ClassLoader的一个内部类。
- PathClassLoader,全名是dalvik/system.PathClassLoader,可以加载已经安装的Apk,也就是/data/app/package 下的apk文件,也可以加载/vendor/lib, /system/lib下的nativeLibrary。
- DexClassLoader,全名是dalvik/system.DexClassLoader,可以加载一个未安装的apk文件。
在MainActivity中打印当前的ClassLoader,
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ClassLoader classLoader = getClassLoader();
while (classLoader != null) {
System.out.println("classLoader: " + classLoader);
classLoader = classLoader.getParent();
}
}
}
结果如下:
dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.sososeen09.classloadtest-1/base.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]]
java.lang.BootClassLoader@aced87d
从打印的结果也可以证实:App系统类加载器是PathClassLoader,而BootClassLoader是其parent类加载器。
二、ClassLoader分析
在Android中我们主要关心的是PathClassLoader和DexClassLoader。
PathClassLoader和DexClasLoader都是继承自 dalviksystem.BaseDexClassLoader,它们的类加载逻辑全部写在BaseDexClassLoader中。PathClassLoader用来操作本地文件系统中的文件和目录的集合。DexClassLoader可以加载一个未安装的APK,也可以加载其它包含dex文件的JAR/ZIP类型的文件。DexClassLoader需要一个对应用私有且可读写的文件夹来缓存优化后的class文件。而且一定要注意不要把优化后的文件存放到外部存储上,避免使自己的应用遭受代码注入攻击。
Android中具体负责类加载的并不是哪个ClassLoader,而是通过DexFile的defineClassNative()方法来加载的。
三、动态加载(热修复)实例
1、新建一个module叫dextest,加入这就是我们要动态加载进来的模块。
里面新建一个叫做ShowToast 的类:
public class ShowToast {
public void showToast(Context context) {
Toast.makeText(context, "动态加载ShowToast", Toast.LENGTH_SHORT).show();
}
}
ShowToast 类里面只有一个showToast方法,该方法需要一个context参数,展示一个toast。
2、把java类打包到dex中
编译之后,在如图所示的目录(不同版本的android studio中可能路径会不一样)中会生成相应的class文件,由于class文件是java虚拟机所用的,但是在安卓中的Dalvik虚拟机要使用dex文件,所以我们需要再进行转换。
还是这个module中的build.gradle文件最后添加:
//删除isshowtoast.jar包任务
task clearJar(type: Delete) {
delete 'build/libs/in.jar'
}
task makeJar(type:org.gradle.api.tasks.bundling.Jar){
//指定生成的jar名
baseName 'in'
//从哪里打包class文件
from('build/intermediates/javac/debug/classes/com/example/dextest/')
//打包到jar后的目录结构
into('com/example/dextest/')
//去掉不需要打包的目录和文件
exclude('test/','BuildConfig.class','R.class')
//去掉R$开头的文件
exclude{it.name.startsWith('R$')}
}
makeJar.dependsOn(clearJar,build)
这样就把我们需要的文件打包到jar包中了,然而jar包并不是我们需要的,继续。
在Android的SDK中为我们提供了一个dx命令(在\android-sdk\build-tools\version[23.0.1] 或 \android-sdk\platform-tools下能找到);命令使用方式为:dx --dex --output=out.jar in.jar,该命令将包含class的in.jar转化为包含dex的out.jar文件。
3、app端调用
把生成的out.jar包复制到app项目中的assets目录下:
我们这里是为了方便,真是场景可能是远程服务器下载到指定位置。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
loadDex();
}
});
}
/**
* 点击事件
*/
public void loadDex() {
File cacheFile = getDir("dex",0);
String internalPath = cacheFile.getAbsolutePath() + File.separator + "out.jar";
File desFile=new File(internalPath);
try {
if (!desFile.exists()) {
desFile.createNewFile();
FileUtils.copyFiles(this,"out.jar",desFile);
}
} catch (IOException e) {
e.printStackTrace();
}
//下面开始加载dex class
//1.待加载的dex文件路径,如果是外存路径,一定要加上读外存文件的权限,
//2.解压后的dex存放位置,此位置一定要是可读写且仅该应用可读写
//3.指向包含本地库(so)的文件夹路径,可以设为null
//4.父级类加载器,一般可以通过Context.getClassLoader获取到,也可以通过ClassLoader.getSystemClassLoader()取到。
DexClassLoader dexClassLoader=new DexClassLoader(internalPath,cacheFile.getAbsolutePath(),null,getClassLoader());
try {
Class clz = dexClassLoader.loadClass("com.example.dextest.ShowToast");
Method method = clz.getDeclaredMethod("showToast", Context.class);
method.invoke(clz.newInstance(), this);
} catch (Exception e) {
e.printStackTrace();
}
}
}
这里的FileUtils文件就是把jar包从assets目录拷贝到app的data目录下:
public class FileUtils {
// 把assets目录的文件复制到desFile中
public static void copyFiles(Context context, String fileName, File desFile){
InputStream in=null;
OutputStream out=null;
try {
in=context.getApplicationContext().getAssets().open(fileName);
out=new FileOutputStream(desFile.getAbsolutePath());
byte[] bytes=new byte[1024];
int len=0;
while ((len=in.read(bytes))!=-1)
out.write(bytes,0,len);
out.flush();
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if (in!=null)
in.close();
if (out!=null)
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
一开始在这里我碰到一个ClassNotFoundException的异常:
ClassLoader referenced unknown path: android.content.res.AssetManager@d75e3f9/out.jar
java.lang.ClassNotFoundException: Didn't find class "com.example.dextest.ShowToast" on path: DexPathList[[],nativeLibraryDirectories=[/system/lib, /system/vendor/lib]]
at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:134)
at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
at com.example.test.MainActivity.loadDex(MainActivity.java:44)
因为一开始我直接使用assets目录下的jar包,好像加载不进来,然后我就使用FileUtils把jar包复制到另外一个目录就可以了。
在测试机点击按钮就可以看到效果了:
总结一下就是使用DexClassLoader来获取dex中的类,然后通过反射的方式运行类中的方法。
还有几个问题:
a、如果原有的类有bug,现在要动态替换原有类,如何操作?
参考android热修复,使用Javassist。
b、如果要动态加载新的activity,如何管理生命周期?
c、如何加载资源文件?
参考:类加载机制系列2——深入理解Android中的类加载器
Android动态加载dex入门
Android动态加载Dex过程