刚进入一家公司,需要解决以下问题:公司主要是做仪器的,目前有4款app,以后还会增加几款,这样,同一个用户手机上可能需要装多款我们的app,这样不仅安装麻烦,而且不同的功能要打开不同的app,给用户造成不便,用户体验会很差。
需求1:把目前已有的几款app整合成一个大的app,在这个大的app上给每个小app中的功能留有入口,这样用户安装一个大app后就可以从对应入口使用各个小app中的功能,用户不需要安装多个app,提升了用户体验
需求2:每个小app的功能可以独立修改升级,不能修改其中一个app中的功能,其他小app也要跟着升级发版,做到各个小app中的功能能够互相独立。
考虑了以下,准备使用android的动态加载技术,把每个功能模块作为一个插件,有一个宿主app,和多个插件,安装宿主app后,用户需要的时候才去下载插件。
插件化需要做的几件事
加载插件dex
将dex包注入ClassLoader(原理分析)
这里谈到注入,就要谈到Android的ClassLoader体系。
由上图可以看出,在叶子节点上,我们能使用到的是DexClassLoader和PathClassLoader,通过查阅开发文档,我们发现他们有如下使用场景:
- 关于PathClassLoader,文档中写到: Android uses this class for its system class loader and for its application class loader(s),
由此可知,Android应用就是用它来加载;
- DexClass可以加载apk,jar,及dex文件,但PathClassLoader只能加载已安装到系统中(即/data/app目录下)的apk文件。
- 两个叶子节点的类都继承BaseDexClassLoader中,而具体的类加载逻辑也在此类中:
BaseDexClassLoader:
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
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;
}
由上述函数可知,当我们需要加载一个class时,实际是从pathList中去寻找的,查阅源码,发现pathList是DexPathList类的一个实例。ok,接着去分析DexPathList类中的findClass函数
DexPathList:
public Class findClass(String name, List<Throwable> suppressed) {
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;
}
上述函数的大致逻辑为:遍历一个装载dex文件(每个dex文件实际上是一个DexFile对象)的数组(Element数组,Element是一个内部类),然后依次去加载所需要的class文件,直到找到为止。
看到这里,注入的解决方案也就浮出水面,假如我们将第二个dex文件放入Element数组中,那么在加载第二个dex包中的类时,应该可以直接找到。
将dex包注入ClassLoader(代码实现)
在我们自定义的BaseApplication的onCreate中,我们执行注入操作:
public String inject(String libPath) {
boolean hasBaseDexClassLoader = true;
try {
Class.forName("dalvik.system.BaseDexClassLoader");
} catch (ClassNotFoundException e) {
hasBaseDexClassLoader = false;
}
if (hasBaseDexClassLoader) {
PathClassLoader pathClassLoader = (PathClassLoader)getClassLoader();
PluginManager.getInstance().setContext(this);
try {
Object dexElements = combineArray(getDexElements(getPathList(pathClassLoader)), getDexElements(getPathList( PluginManager.getInstance().loadApk(libPath))));
Object pathList = getPathList(pathClassLoader);
setField(pathList, pathList.getClass(), "dexElements", dexElements);
return "SUCCESS";
} catch (Throwable e) {
e.printStackTrace();
return android.util.Log.getStackTraceString(e);
}
}
return "SUCCESS";
}
private static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException,IllegalAccessException {
return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}
private static Object getField(Object obj, Class cls, String str) throws NoSuchFieldException, IllegalAccessException {
Field declaredField = cls.getDeclaredField(str);
declaredField.setAccessible(true);
return declaredField.get(obj);
}
private static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {
return getField(obj, obj.getClass(), "dexElements");
}
private static Object combineArray(Object obj, Object obj2) {
Class componentType = obj2.getClass().getComponentType();
int length = Array.getLength(obj2);
int length2 = Array.getLength(obj) + length;
Object newInstance = Array.newInstance(componentType, length2);
for (int i = 0; i < length2; i++) {
if (i < length) {
Array.set(newInstance, i, Array.get(obj2, i));
} else {
Array.set(newInstance, i, Array.get(obj, i - length));
}
}
return newInstance;
}
private static void setField(Object obj, Class cls, String str, Object obj2) throws NoSuchFieldException, IllegalAccessException {
Field declaredField = cls.getDeclaredField(str);
declaredField.setAccessible(true);
declaredField.set(obj, obj2);
}
Activity等组件问题
插件中的组件,由于没有在manifest中进行注册,加载进来以后就是一个普通的类,不具有生命周期,所以不可以使用intent进行跳转,为了解决这个问题,可以使用代理的方式解决。但是,我这里没有,由于app都是自己开发的,所以把每个插件里面的组件,都在宿主的manifest中进行注册。
加载插件中的资源
加载的方法是通过反射,通过调用AssetManager中的addAssetPath方法,我们可以将一个apk中的资源加载到Resources中,由于addAssetPath是隐藏api我们无法直接调用,所以只能通过反射,下面是它的声明,通过注释我们可以看出,传递的路径可以是zip文件也可以是一个资源目录,而apk就是一个zip,所以直接将apk的路径传给它,资源就加载到AssetManager中了,然后再通过AssetManager来创建一个新的Resources对象,这个对象就是我们可以使用的apk中的资源了,这样我们的问题就解决了。
public Resources getResources() {
AssetManager assets = null;
try {
assets = AssetManager.class.newInstance();
Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPath.invoke(assets, PluginManager.getInstance().getCacheDir(BaseActivity.this)+ File.separator + "otherapk-debug.apk");
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return new Resources(assets, super.getResources().getDisplayMetrics(), super.getResources().getConfiguration());
}