“类加载器”(ClassLoader),顾名思义,就是用来动态加载class文件的。
ClassLoader作用主要有三个:
有兴趣的小伙伴可以看看JVM是如何加载一个类的 类的加载机制
ClassLoader(Java)
Class clz = Classloader.loadClass(类全名),其实就是通过一个类的全名,生成这个类的Class对象。loadClass()内部是先进行parent.loadClass()让父类先进行加载,如果加载不成功,再使用该classLoader加载(双亲委派)。然后,通过findClass(类全名)来加载得到Class对象。我们如果想自定义一个classLoader,那么就是重写findClass()方法。findClass()中,我们拿到要加载的路径,然后拿到路径对应文件的数据流。然后使用classLoader定义好的defineClass(inputStream)来生成Class对象就可以了,主要就是给它提供一个路径,然后类全名能找到这个路径下对应的.class文件,然后生成inputStream流。
ClassLoader(Android)
Android中的ClassLoader于Java中的稍微有些不同,虽然两者都是满足双亲委派,但是直接findClass()会抛异常,所以我们不能直接继承classloader来自定义classLoader。要使用BaseDexClassLoader,BaseDexClassLoader重写了findClass(),要注意的是这里的classloader加载的是dex,不是class字节码。
ClassLoader比较常用的分为两种,PathClassLoader和DexClassLoader,虽然两者继承于BaseDexClassLoader,BaseDexClassLoader继承于ClassLoader,但是前者只能加载已安装的Apk里面的dex文件,后者则支持加载apk、dex以及jar,也可以从SD卡里面加载。
作为 ClassLoader 的子类,复写了父类的 findClass 方法:
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
//在自己的成员变量DexPathList中寻找,找不到抛异常
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;
}
DexPathList 的 findClass 方法:
public Class findClass(String name, List<Throwable> suppressed) {
//循环遍历成员变量dexElements,调用DexFile.loadClassBinaryName加载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;
}
通过以上两段代码我们可以看出,虽然 Android 中的 ClassLoader 的findClass 方法的实现被取消了,但是 ClassLoader 的基类 BaseDexClassLoader 实现了 findClass 方法来加载指定的 Class文件。
DexClassLoader
DexClassLoader用来加载外部的类,外部类的dexpath路径在构造方法中传入,比如从网络下载的dex等,或插件化的apk,从网络下载到dex后,new 一个DexClassLoader,new的时候就把网络下载到的dex路径告诉DexClassLoader,DexClassLoader会将dex一步步封装,放到DexClassLoader中的pathList里面。dex放到DexClassLoader之后,使用DexClassLoader.loadClass(需要加载的patch类全名)得到补丁类的Class对象,然后class.newInstance()对应的实例。通过这个实例里的信息来找到要修补的是哪个类,然后找到这个类对应的Class对象,如果没有就使用PathClassLoader加载。
DexClassLoader的构造方法如下:
DexClassLoader (String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent)
参数 | 含义 |
---|---|
dexPath | 包含dex文件的jar包或apk文件路径 |
optimizedDirectory | 释放目录,可以理解为缓存目录,必须为应用私有目录,不能为空 |
librarySearchPath | native库的路径(so文件),可为空 |
parent | 父类加载器 |
需要注意的是:DexClassLoader中还要传入一个ClassLoader作为该DexClassLoader的父类。这样,我们使用DexClassLoader加载一个类时,根据双亲委派,会先让父类classloader进行加载。
双亲委派模型
双亲委派模型是一种组织类加载器之间关系的一种规范,他的工作原理是:如果一个类加载器收到了类加载的请求,它不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,这样层层递进,最终所有的加载请求都被传到最顶层的启动类加载器中,只有当父类加载器无法完成这个加载请求(它的搜索范围内没有找到所需的类)时,才会交给子类加载器去尝试加载。
优点:java类随着它的类加载器一起具备了带有优先级的层次关系,这是十分必要的。比如java.langObject,它存放在\jre\lib\rt.jar中,它是所有java类的父类,因此无论哪个类加载都要加载这个类,最终所有的加载请求都汇总到顶层的启动类加载器中,因此Object类会由启动类加载器来加载,所以加载的都是同一个类,如果不使用双亲委派模型,由各个类加载器自行去加载的话,系统中就会出现不止一个Object类,应用程序就会全乱了。
最后就来说说Android动态加载Dex那些事…
Android动态加载Dex
新建一个Android工程,在包下面创建一个Impl文件夹,里面创建DexImpl类,然后在上一层创建一个接口类IDex,代码如下:
DexImpl:继承Idex接口类,重写其方法,并简单输出一句话,“包名+ is loaded by DexClassLoader”
public class DexImpl implements IDex {
@Override
public String getMessage() {
return new StringBuilder(getClass().getName()).append(" is loaded by DexClassLoader").toString();
}
}
接口类IDex:
public interface IDex {
public String getMessage();
}
具体的工程目录如下图
点击Build -> make project,这时候会在build/intermediates/javac/debug/classes目录下生成对应的classes文件(这个因AS的版本而异,我现在用的不是androidx的版本,所以我的文件在build/intermediates/javac/debug/classes下面)
然后我们需要把DexImpl这个class转换成Dalvik可识别的dex文件,分两步:
1.先导出DexImpl这个类为jar包的形式;
2.通过android sdk自带的dx.jar工具转换jar包为包含dex文件的Jar文件。
打开app目录下的build.gradle文件,切记不是根目录的build.gradle文件,加上以下代码:
//删除dynamic.jar包任务
task clearJar(type: Delete) {
delete 'libs/dynamic.jar'
}
//打包任务
task makeJar(type:org.gradle.api.tasks.bundling.Jar) {
//指定生成的jar名
baseName 'dynamic'
//从哪里打包class文件
from('build/intermediates/javac/debug/classes/com/xy/dex/plugin/impl/')
//打包到jar后的目录结构
into('com/xy/dex/plugin/impl')
//去掉不需要打包的目录和文件
exclude('test/', 'IDex.class', 'BuildConfig.class', 'R.class', 'FileUtils.class')
//去掉R$开头的文件
exclude{
it.name.startsWith('R$');}
}
makeJar.dependsOn(clearJar, build)
写完这段代码之后,我们在AS的界面最右边可以看到Gradle,点击去,然后在app->other里面找到makeJar这个东西,双击它,然后AS就会帮你生成你所需要的Jar包了,生成后的Jar包路径在app\build\lib文件夹下面
然后使用sdk提供的dx.jar将导出的 dynamic.jar转换成Dalvik可识别的dex格式,我是将这个jar包copy出来,然后置于某个文件夹下(需要跟dx.jar同目录),然后执行dx --dex --output=out.jar dynamic.jar,就会得到含dex的out.jar(这里你可以右键这个jar包,看看里面是不是有一个dex文件,是的话就没错了,那我们继续…)
因为等下我们要使用的是dex下面的IDex实现类,所以我们需要删除当前工程下的DexImpl文件和impl包,避免运行时出错。同时,我们要把刚刚生成的out.jar文件放到assets目录下,等下需要把它copy到app/data下使用,删除后的整个工程目录如下:
FileUtils类是从assets目录下copy文件到app/data/cache目录:
public class FileUtils {
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 i;
while ((i = in.read(bytes)) != -1)
out.write(bytes, 0 , i);
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
if (in != null)
in.close();
if (out != null)
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static boolean hasExternalStorage() {
return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
}
/**
* 获取缓存路径
*
* @param context
* @return 返回缓存文件路径
*/
public static File getCacheDir(Context context) {
File cache;
if (hasExternalStorage()) {
cache = context.getExternalCacheDir();
} else {
cache = context.getCacheDir();
}
if (!cache.exists())
cache.mkdirs();
return cache;
}
}
核心思想就是使用DexClassLoader去加载dex,然后通过反射调用我们之前定义的方法获取相关资源.
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void Track(View view) {
loadDexClass();
}
private void loadDexClass() {
// getDir("dex1", 0)会在/data/data/**package/下创建一个名叫”app_dex1“的文件夹,其内存放的文件是自动生成output.dex
File OutputDir = FileUtils.getCacheDir(getApplicationContext());
String dexPath = OutputDir.getAbsolutePath() + File.separator + "out.jar";
File desFile=new File(dexPath);
try {
if (!desFile.exists()) {
desFile.createNewFile();
FileUtils.copyFiles(this,"out.jar",desFile);
}
} catch (IOException e) {
e.printStackTrace();
}
/**
* 参数1 dexPath:待加载的dex文件路径,如果是外存路径,一定要加上读外存文件的权限
* 参数2 optimizedDirectory:解压后的dex存放位置,此位置一定要是可读写且仅该应用可读写(安全性考虑),所以只能放在data/data下。
* 参数3 libraryPath:指向包含本地库(so)的文件夹路径,可以设为null
* 参数4 parent:父级类加载器,一般可以通过Context.getClassLoader获取到,也可以通过ClassLoader.getSystemClassLoader()取到。
*/
DexClassLoader classLoader = new DexClassLoader(dexPath, OutputDir.getAbsolutePath(),null,getClassLoader());
try {
// 该方法将Class文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化.该方法因为需要得到一个ClassLoader对象
Class clz = classLoader.loadClass("com.xy.dex.plugin.impl.DexImpl");
IDex dex = (IDex) clz.newInstance();
Toast.makeText(this, dex.getMessage(), Toast.LENGTH_LONG).show();
} catch (Exception e) {
e.printStackTrace();
}
}
}
附上下载链接Android动态加载Dex