@JeromeLiee
2020-02-06T15:00:38.000000Z
字数 7418
阅读 1026

可以动态扩展功能,避免频繁地发布版本来更新迭代,随之带来的另外一个好处就是减少了apk包的体积。
/*** DexClassLoader的构造方法** @param dexPath APK的文件目录* @param optimizedDirectory 内部存储路径,用来缓存系统创建的Dex文件* @param librarySearchPath 库文件的存储路径* @param parent 父ClassLoader*/public void DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {}
双亲委派机制
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) {// 首先,检查这个类是否已经加载过// First, check if the class has already been loadedClass<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {// c==null,说明没有加载过,那么让父加载器去加载if (parent != null) {c = parent.loadClass(name, false);} else {c = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader}if (c == null) {// 如果找到最顶层的ClassLoader都没有加载过,那么就由自己加载// If still not found, then invoke findClass in order// to find the class.long t1 = System.nanoTime();c = findClass(name);...}}if (resolve) {resolveClass(c);}return c;}}
双亲委派机制整个流程:先检查自己有没有加载过该类,如果没有则交由父加载器去加载,如果一直递归到最顶层加载器也没有加载过,则最终由调用自己的findClass()方法加载,这种机制避免了类的重复加载。
// BaseDexClassLoader.javaprotected 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.javapublic Class<?> findClass(String name, List<Throwable> suppressed) {for (Element element : dexElements) {Class<?> clazz = element.findClass(name, definingContext, suppressed);if (clazz != null) {return clazz;}}if (dexElementsSuppressedExceptions != null) {suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));}return null;}
通过给插件创建一个DexClassLoader便可在宿主中调用插件中的类,有两种处理方式:只创建一个DexClassLoader和为每个插件都单独创建一个DexClassLoader
但宿主调用插件的类,有两种方式:

为每个插件都创建一个对应的DexClassLoader,当加载插件的类时,需要通过对应的DexClassLoader。这样不同的插件的类是隔离的,如果两个插件依赖了同一个第三方库的不同版本,是不会有冲突的。

将插件DexClassLoader中的DexPathList合并到宿主DexClassLoader的DexPathList,好处是在不同的插件中都可以和宿主通过类名互相调用类和方法,并且可以将不同插件的公共模块抽出来放在一个common插件中直接供其他插件使用。Small采用的是这种方式。
Android中资源是通过Resources类加载的,而Resources类是由AssetManager创建的,具体如下:
// ResourcesManager.java #getTopLevelResources()Resources getTopLevelResources(String resDir, String[] splitResDirs,String[] overlayDirs, String[] libDirs, int displayId,Configuration overrideConfiguration, CompatibilityInfo compatInfo) {Resources r;...AssetManager assets = new AssetManager()// 将apk路径添加到AssetManager中,路径可以是一个目录或zip压缩文件,而apk正好是一个压缩文件// resDir can be null if the 'android' package is creating a new Resources object.// This is fine, since each AssetManager automatically loads the 'android' package// already.if (resDir != null) {if (assets.addAssetPath(resDir) == 0) {return null;}}...r = new Resources(assets, dm, config, compatInfo);synchronized (this) {...return r;}}
因此,只要将插件apk的路径传入AssetManager的addAssetPath()方法中,便可以加载插件的资源文件。
但由于AssetManager的addAssetPath()方法是hide类型不对外开放,所以需要使用反射来调用。
要加载插件中的资源,需传入插件的路径,最终生成Resources对象,同类的处理一样,资源的处理也有两种方式:

合并式由于添加了宿主和所有插件的资源路径,因此生成的Resources在宿主和插件中都能访问所有的资源文件。但由于宿主和插件是分别编译的,所以资源id可能会产生冲突。
独立式由于各自生成了宿主和插件的Resources,所以资源是隔离的不会有冲突,但想要互相访问彼此的资源文件,需要持有彼此的Resources对象。
我们平时访问资源的时候,都是通过Context获取Resources对象,那么就需要对Context里的mResources进行替换,这里以VirtualApk为例:
第一步:创建Resources对象
if (Constants.COMBINE_RESOURCES) {// 创建插件的Resources并与宿主的Resources合并,生成一个新的Resources对象Resources resources = ResourcesManager.createResources(context, apk.getAbsolutePath());// 插件和主工程资源合并时需要hook住主工程的资源ResourcesManager.hookResources(context, resources);return resources;} else {// 插件资源独立,该resource只能访问插件自己的资源Resources hostResources = context.getResources();AssetManager assetManager = createAssetManager(context, apk);return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());}
VirtualApk支持合并式和独立式资源路径处理。
第二步:替换Context中的成员变量mResources
public static void hookResources(Context base, Resources resources) {try {// 替换Context中的mResourcesReflectUtil.setField(base.getClass(), base, "mResources", resources);Object loadedApk = ReflectUtil.getPackageInfo(base);// 替换LoadedApk中的mResourcesReflectUtil.setField(loadedApk.getClass(), loadedApk, "mResources", resources);Object activityThread = ReflectUtil.getActivityThread(base);Object resManager = ReflectUtil.getField(activityThread.getClass(), activityThread, "mResourcesManager");if (Build.VERSION.SDK_INT < 24) {Map<Object, WeakReference<Resources>> map = (Map<Object, WeakReference<Resources>>) ReflectUtil.getField(resManager.getClass(), resManager, "mActiveResources");Object key = map.keySet().iterator().next();map.put(key, new WeakReference<>(resources));} else {// still hook Android N Resources, even though it's unnecessary, then nobody will be strange.Map map = (Map) ReflectUtil.getFieldNoException(resManager.getClass(), resManager, "mResourceImpls");Object key = map.keySet().iterator().next();Object resourcesImpl = ReflectUtil.getFieldNoException(Resources.class, resources, "mResourcesImpl");map.put(key, new WeakReference<>(resourcesImpl));}} catch (Exception e) {e.printStackTrace();}}
上述代码hook了三个点:
第三步:关联Activity和Resources
Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent); activity.setIntent(intent);// 设置Activity的mResources属性,Activity中访问资源时都通过mResourcesReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources());
在Activity生成后替换其父类ContextThemeWrapper的成员变量mResources,这样在插件Activity中直接可以通过getResources获取Resources对象。
接下来就可以在插件的Activity中使用setContentView、inflate等方法来加载布局了。
在合并式的资源处理方式中,很大概率会产生资源冲突,因为不同的apk编译打包过程中,资源id名称可能会相同。那么解决冲突就是让宿主和插件的资源id保持不同。
资源id由8位16进制表示,格式为0xPPTTNNNN,其中PP段用来区分包空间,TT段用来区分资源类型,NNNN段是在apk里从0000开始递增。

那么要保证宿主和插件的资源id不一致,我们一般修改PP段的值即可,有如下两种方式:
由于第一种修改AAPT工具侵入性比较大,所以推荐使用第二种,可参考 插件化-解决插件资源ID与宿主资源ID冲突的问题
经过上述步骤后,便实现了插件Activity的启动,并且该插件Activity中并不需要什么额外的处理,和常规的Activity一样。那问题来了,之后的onResume,onStop等生命周期怎么办呢?答案是所有和Activity相关的生命周期函数,系统都会调用插件中的Activity。原因在于AMS在处理Activity时,通过一个token表示具体Activity对象,而这个token正是和启动Activity时创建的对象对应的,而这个Activity被我们替换成了插件中的Activity,所以之后AMS的所有调用都会传给插件中的Activity。
四大组件中Activity的支持是最复杂的,其他组件的实现原理要简单很多,简要概括如下:
Service:Service和Activity的差别在于,Activity的生命周期是由用户交互决定的,而Service的生命周期是我们通过代码主动调用的,且Service实例和manifest中注册的是一一对应的。实现Service插件化的思路是通过在manifest中预埋StubService,hook系统startService等调用替换启动的Service,之后在StubService中创建插件Service,并手动管理其生命周期。
BroadCastReceiver:解析插件的manifest,将静态注册的广播转为动态注册。
ContentProvider:类似于Service的方式,对插件ContentProvider的所有调用都会通过一个在manifest中占坑的ContentProvider分发。