Android插件化(二):使用DexClassLoader动态载入assets中的apk
简单介绍
上一篇博客讲到。我们能够使用MultiDex.java载入离线的apk文件。须要注意的是,apk中的类是载入到当前的PathClassLoader其中的,假设apk文件过多。可能会出现ANR的情况。那么。我们能不能使用DexClassLoader载入apk呢?当然是能够的!
首先看一下Doc文档.
A class loader that loads classes from .jar and .apk files containing a classes.dex entry. This can be used to execute code not installed as part of an application.
也就是说,DexClassLoader能够载入一个含有classes.dex文件的压缩包,既能够是jar也能够是apk。那么载入一个离线的apk文件须要注意哪些呢?
1.DexClassLoader的构造方法:
DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)2.私有文件夹
This class loader requires an application-private, writable directory to cache optimized classes.
了解到上述两点,我们就能够依据DexClassLoader所须要的參数。动态载入assets中的apk了。
源代码
BundleClassLoaderManager
该类主要是负责管理这些DexClassLoader的,首先,我们定义了一个叫做BundleDexClassLoader的类,它继承自DexClassLoader,用于载入离线的apk文件。每个apk文件相应一个BundleDexClassLoader,而BundleClassLoaderManager则保存了一个List,在载入的时候。用于查找类。
详细代码例如以下:
package net.mobctrl.hostapk;
import java.io.File;
import java.io.FilenameFilter;
import java.util.ArrayList;
import java.util.List;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
/** * @Author Zheng Haibo * @PersonalWebsite http://www.mobctrl.net * @version $Id: BundleClassLoaderManager.java, v 0.1 2015年12月11日 下午7:30:59 * mochuan.zhb Exp $ * @Description */
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public class BundleClassLoaderManager {
public static List<BundleDexClassLoader> bundleDexClassLoaderList = new ArrayList<BundleDexClassLoader>();
/** * 载入Assets里的apk文件 * @param context */
public static void install(Context context) {
AssetsManager.copyAllAssetsApk(context);
// 获取dex文件列表
File dexDir = context.getDir(AssetsManager.APK_DIR,
Context.MODE_PRIVATE);
File[] szFiles = dexDir.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String filename) {
return filename.endsWith(AssetsManager.FILE_FILTER);
}
});
for (File f : szFiles) {
System.out.println("debug:load file:" + f.getName());
BundleDexClassLoader bundleDexClassLoader = new BundleDexClassLoader(
f.getAbsolutePath(), dexDir.getAbsolutePath(), null,
context.getClassLoader());
bundleDexClassLoaderList.add(bundleDexClassLoader);
}
}
/** * 查找类 * * @param className * @return * @throws ClassNotFoundException */
public static Class<?
> loadClass(Context context,String className) throws ClassNotFoundException { try { Class<?> clazz = context.getClassLoader().loadClass(className); if (clazz != null) { System.out.println("debug: class find in main classLoader"); return clazz; } } catch (Exception e) { e.printStackTrace(); } for (BundleDexClassLoader bundleDexClassLoader : bundleDexClassLoaderList) { try { Class<?> clazz = bundleDexClassLoader.loadClass(className); if (clazz != null) { System.out.println("debug: class find in bundle classLoader"); return clazz; } } catch (Exception e) { e.printStackTrace(); } } throw new ClassCastException(className + " not found exception"); } }
注意点:
-
1.install方法
install方法主要是将assets中的apk所有复制到私有文件夹,然后再遍历私有文件夹。使用BundleDexClassLoader载入apk文件。然后将这些BundleDexClassLoader保存到数组中。 2.loadClass方法
该方法先从当前的ClassLoader中查找须要的类,假设找不到,在从List中遍历查找。
DEMO执行
在MainActivity中,我们能够通过例如以下方式。调用apk类中的方法:
private void loadApk() {
try {
Class<?
> clazz = BundleClassLoaderManager.loadClass(getApplicationContext(), "net.mobctrl.normal.apk.Utils"); Constructor<?> constructor = clazz.getConstructor(); Object bundleUtils = constructor.newInstance(); Method printSumMethod = clazz.getMethod("printSum", Context.class, int.class, int.class, String.class); printSumMethod.setAccessible(true); Integer sum = (Integer) printSumMethod.invoke(bundleUtils, getApplicationContext(), 10, 20, "计算结果"); System.out.println("debug:sum = " + sum); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SecurityException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } }
与MultiDex不同一时候。我们是通过BundleClassLoaderManager来载入类的,而不是当前的ClassLoader。
改进方案
正如BundleClassLoaderManager中的loadClass方法。事实上我们创建一个ClassLoader对象,通过重写当前ClassLoader的findClass方法就可以,然后在Override的findClass方法中,首先从当前ClassLoader中查找类,然后再从BundleDexClassLoader中遍历查找,这样既能够在Host项目中调用Bundle中的类,也能够在Bundle中调用Host中的类。
mClassLoader = new ClassLoader(super.getClassLoader()) {
@Override
protected Class<?> findClass(String className)
throws ClassNotFoundException {
Class clazz = BundleClassLoaderManager.loadClass(context,className);
if (clazz == null) {
throw new ClassNotFoundException(className);
}
return clazz;
}
};
总结
上一篇博客和这一篇博客将的都是类的载入。假设所须要载入的类都是工具类,不须要载入资源等,那么上面的方案都没啥问题。可是假设载入的类是Fragment或者Activity等UI,须要引用资源文件,这又改怎样处理呢?
下一篇博文:Android资源的离线载入。
參考
1.BaseDexClassLoader源代码