Android插件化(二):使用DexClassLoader动态载入assets中的apk

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源代码

上一篇:Android插件化的兼容性(中):Android P的适配


下一篇:android 中获取视频文件的缩略图(非原创)