插件化
1)插件化技术最初源于免安装运行apk的想法
2)免安装的apk我们称为插件
3)支持插件的APP我们称为宿主
插件化解决的问题:
1)APP的功能模块越来越多,体积越来越大
2)模块之间的耦合度高,协同开发沟通成本越来越来
3)方法数目可能超过65535,APP占用内存过大
4)应用之间的相互调用
插件化和组件化的区别:
组件化开发就是将一个APP分成多个模块,每个模块都是一个组件,开发的过程中我们让这些组件相互依赖或者单独调试部分组件等,但是最终发布的时候将这些组件合并到APP壳中,这就是组件化开发
插件化开发和组件化开发略有不同,插件化开发是将整个APP拆分成多个模块,这些模块包括一个宿主和多个插件,每一个模块都是apk,最终打包发布是时候宿主apk和插件apk分开打包。
我们在开发过程中可以尝试使用DroidPlugin第三方框架。
插件化实现思路:
1)如何加载插件的类
2)如何启动插件的四大组件
3)如何加载插件的资源
1)如何加载插件的类
我们得要知道我们是虚拟机是如何加载我们是类的
1、通过一个类的全限定名来获取此类的二进制字节流
2、将这个字节流所代表的静态存储结构转化为方法区运行时的数据结构
3、在Java堆中生成一个代表这个类的Class对象,作为方法区数据的访问入口
其次我们要了解不同ClassLoader的作用:
PathClassLoader和DexClassLoader的区别:
网上说:
PathClassLoader:只能加载系统中已经安装过的apk
DexClassLoader:能够加载未安装的jar/apk/dex
在8.0(API26)之前,它们二者唯一的区别就是第二个参数optimizedDirectory,这个参数是意思是生成的odex(优化后的dex)存放路径
在8.0(API26)及以后,二者就完全一样了,一般我们自定义ClassLoader建议使用DexClassLoader。
BootClassLoader和PathClassLoader区别:
public void loadPlugin(){
ClassLoader classLoader = getClassLoader();
if (classLoader != null) {
Log.e("joung","classLoader : "+classLoader);
classLoader = classLoader.getParent();
Log.e("joung","classLoader : "+classLoader);
}
}
BootClassLoader是PathClassLoader的parent,但不是父类。
我们去加载一个dex文件,并执行里面的类的方法:
public void loadDexFile(){
DexClassLoader dexClassLoader = new DexClassLoader(PATH,
this.getCacheDir().getAbsolutePath(),
null,getClassLoader());
try {
Class<?> aClass = dexClassLoader.loadClass("com.joung.plugin.Test");
Method print = aClass.getMethod("print");
print.invoke(null);
} catch (Exception e) {
e.printStackTrace();
}
}
双亲委派机制:
1、避免重复加载,当父加载器已经加载过了该类的时候,就没有必要子ClassLoader再次加载
2、安全性考虑,防止核心API被篡改
加载类loadClass的时候
1、判断有没有加载过,加载过直接返回
2、没加载过,交给parent加载
3、parent加载成功返回
4、parent加载失败,自己加载返回
我们要加载插件中类,那么我们就要找到在哪个地方加载了我们的dex文件,这样我们通过反射、动态代理,把插件中的类给加载进来:
1、类查找
通过父加载器去加载,我们发现最后调用的是findLoadedClass:native是native方法,这个我们操作不了
父加载器不行的话,我们看一下加载器自身去加载的流程发现:
DexClassLoader没有重写findClass方法,而是使用父类的BaseDexClassLoader加载。最终通过Element类中的方法返回,而每一个Element就对应一个dex文件,
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;
}
DexPathList:
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
//一个Element对应一个dex文件
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
2、实现步骤:
1)创建创建的DexClassLoader类加载器,然后通过反射获取前插件的dexElement
2、获取宿主的PathClassLoader类加载器,然后通过反射获取宿主的dexElement
3、合并宿主的dexElement与插件的dexElement,生产新的Element[]
4、最后通过反射将新的Element[]复制给宿主的dexElement
2)如何启动插件的四大组件
经过第一步,我们已经把插件的类加载到宿主中了。但是我们该如何启动插件中的四大组件呢比如Activity?因为Activity是有生命周期的类,不像我们普通的类。一般来说,我们启动一个Activity需要在清单中注册才能使用,我们不可能把插件的组件注册到宿主的清单中。
因此我们先需要了解Activity的启动流程:
Activity的启动流程(一个Activity启动另外一个Activity):
在AMS中会进行检查Activity是否注册,所以我们可以如下实现:
1、在宿主APP中创建一个代理ProxyActivity代替插件中的MainActivity,这个过程我们需要找到startActivity方法,通过动态代理和反射Hook AMS,把 startActivity中的Intent改成跳转插件中的MainActivity并代替startActivity方法,
2、经过了AMS之后,我们再Hook Handler,通过反射,改成启动插件中的MainActivity。
3)如何加载插件的资源
通过步骤二,我们启动了插件中的Activity,那么我们如何加载插件中的资源呢?
Resource:
@NonNull
public String getString(@StringRes int id) throws NotFoundException {
return getText(id).toString();
}
@NonNull public CharSequence getText(@StringRes int id) throws NotFoundException {
CharSequence res = mResourcesImpl.getAssets().getResourceText(id);
if (res != null) {
return res;
}
throw new NotFoundException("String resource ID #0x"
+ Integer.toHexString(id));
}
ResourcesImpl:
@UnsupportedAppUsage
public AssetManager getAssets() {
return mAssets;
}
从上面的代码,我们可以看出getResource获取资源其实是通过AssetManager获取的。
这样我们是不是可以在插件创建一个AssetManager来加载插件的资源呢,然后我们加载插件的AssetManager,这样就可以在插件的Activity中加载资源。
至于为什么直接加载插件的AssetManager累呢?
因为直接加载插件的AssetManager的话,生成的资源ID会和宿主的资源ID冲突。
实现思路:
1、需要创建一个Resources对象,用来加载插件的资源
2、Resources的创建需要AssetManager对象
3、AssetManager创建的时候,指定资源路径
插件化的弊端:每个版本的API源码都不同,当我们去Hook是时候基本每个版本都需要修改,我们需要适配所有的版本就很头疼。