[关闭]
@JeromeLiee 2020-02-06T15:00:38.000000Z 字数 7418 阅读 810

插件化原理


插件化.png

参考:《Android插件化技术——原理篇》

1. 目的

可以动态扩展功能,避免频繁地发布版本来更新迭代,随之带来的另外一个好处就是减少了apk包的体积。

2. 问题

  1. 插件中的类加载和宿主的互相调用
  2. 插件中的资源加载和宿主的互相调用
  3. 插件中的四大组件的支持以及生命周期管理

3. 原理

3.1 类加载

  1. /**
  2. * DexClassLoader的构造方法
  3. *
  4. * @param dexPath APK的文件目录
  5. * @param optimizedDirectory 内部存储路径,用来缓存系统创建的Dex文件
  6. * @param librarySearchPath 库文件的存储路径
  7. * @param parent 父ClassLoader
  8. */
  9. public void DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
  10. }

双亲委派机制

  1. protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
  2. synchronized (getClassLoadingLock(name)) {
  3. // 首先,检查这个类是否已经加载过
  4. // First, check if the class has already been loaded
  5. Class<?> c = findLoadedClass(name);
  6. if (c == null) {
  7. long t0 = System.nanoTime();
  8. try {
  9. // c==null,说明没有加载过,那么让父加载器去加载
  10. if (parent != null) {
  11. c = parent.loadClass(name, false);
  12. } else {
  13. c = findBootstrapClassOrNull(name);
  14. }
  15. } catch (ClassNotFoundException e) {
  16. // ClassNotFoundException thrown if class not found
  17. // from the non-null parent class loader
  18. }
  19. if (c == null) {
  20. // 如果找到最顶层的ClassLoader都没有加载过,那么就由自己加载
  21. // If still not found, then invoke findClass in order
  22. // to find the class.
  23. long t1 = System.nanoTime();
  24. c = findClass(name);
  25. ...
  26. }
  27. }
  28. if (resolve) {
  29. resolveClass(c);
  30. }
  31. return c;
  32. }
  33. }

双亲委派机制整个流程:先检查自己有没有加载过该类,如果没有则交由父加载器去加载,如果一直递归到最顶层加载器也没有加载过,则最终由调用自己的findClass()方法加载,这种机制避免了类的重复加载。

BaseDexClassLoader.java

  1. // BaseDexClassLoader.java
  2. protected Class<?> findClass(String name) throws ClassNotFoundException {
  3. List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
  4. Class c = pathList.findClass(name, suppressedExceptions);
  5. if (c == null) {
  6. ClassNotFoundException cnfe = new ClassNotFoundException(
  7. "Didn't find class \"" + name + "\" on path: " + pathList);
  8. for (Throwable t : suppressedExceptions) {
  9. cnfe.addSuppressed(t);
  10. }
  11. throw cnfe;
  12. }
  13. return c;
  14. }
  1. // DexPathList.java
  2. public Class<?> findClass(String name, List<Throwable> suppressed) {
  3. for (Element element : dexElements) {
  4. Class<?> clazz = element.findClass(name, definingContext, suppressed);
  5. if (clazz != null) {
  6. return clazz;
  7. }
  8. }
  9. if (dexElementsSuppressedExceptions != null) {
  10. suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
  11. }
  12. return null;
  13. }
3.1.1 单DexClassLoader和多DexClassLoader

通过给插件创建一个DexClassLoader便可在宿主中调用插件中的类,有两种处理方式:只创建一个DexClassLoader和为每个插件都单独创建一个DexClassLoader

但宿主调用插件的类,有两种方式:

多DexClassLoader

undefined

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

单DexClassLoader

undefined

将插件DexClassLoader中的DexPathList合并到宿主DexClassLoader的DexPathList,好处是在不同的插件中都可以和宿主通过类名互相调用类和方法,并且可以将不同插件的公共模块抽出来放在一个common插件中直接供其他插件使用。Small采用的是这种方式。

3.1.2 互相调用

3.2 资源加载

Android中资源是通过Resources类加载的,而Resources类是由AssetManager创建的,具体如下:

  1. // ResourcesManager.java #getTopLevelResources()
  2. Resources getTopLevelResources(String resDir, String[] splitResDirs,
  3. String[] overlayDirs, String[] libDirs, int displayId,
  4. Configuration overrideConfiguration, CompatibilityInfo compatInfo) {
  5. Resources r;
  6. ...
  7. AssetManager assets = new AssetManager()
  8. // 将apk路径添加到AssetManager中,路径可以是一个目录或zip压缩文件,而apk正好是一个压缩文件
  9. // resDir can be null if the 'android' package is creating a new Resources object.
  10. // This is fine, since each AssetManager automatically loads the 'android' package
  11. // already.
  12. if (resDir != null) {
  13. if (assets.addAssetPath(resDir) == 0) {
  14. return null;
  15. }
  16. }
  17. ...
  18. r = new Resources(assets, dm, config, compatInfo);
  19. synchronized (this) {
  20. ...
  21. return r;
  22. }
  23. }

因此,只要将插件apk的路径传入AssetManager的addAssetPath()方法中,便可以加载插件的资源文件。

但由于AssetManager的addAssetPath()方法是hide类型不对外开放,所以需要使用反射来调用。

3.2.1 资源路径的处理

要加载插件中的资源,需传入插件的路径,最终生成Resources对象,同类的处理一样,资源的处理也有两种方式:

undefined

合并式由于添加了宿主和所有插件的资源路径,因此生成的Resources在宿主和插件中都能访问所有的资源文件。但由于宿主和插件是分别编译的,所以资源id可能会产生冲突。

独立式由于各自生成了宿主和插件的Resources,所以资源是隔离的不会有冲突,但想要互相访问彼此的资源文件,需要持有彼此的Resources对象。

3.2.1 Context的Resources对象替换

我们平时访问资源的时候,都是通过Context获取Resources对象,那么就需要对Context里的mResources进行替换,这里以VirtualApk为例:

第一步:创建Resources对象

  1. if (Constants.COMBINE_RESOURCES) {
  2.  // 创建插件的Resources并与宿主的Resources合并,生成一个新的Resources对象
  3. Resources resources = ResourcesManager.createResources(context, apk.getAbsolutePath());    
  4. // 插件和主工程资源合并时需要hook住主工程的资源  
  5. ResourcesManager.hookResources(context, resources);     
  6. return resources;
  7. } else {     
  8. // 插件资源独立,该resource只能访问插件自己的资源    
  9. Resources hostResources = context.getResources();  
  10. AssetManager assetManager = createAssetManager(context, apk);       
  11. return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
  12. }

VirtualApk支持合并式和独立式资源路径处理。

第二步:替换Context中的成员变量mResources

  1. public static void hookResources(Context base, Resources resources) {   
  2. try {            
  3. // 替换Context中的mResources
  4. ReflectUtil.setField(base.getClass(), base, "mResources", resources);            
  5. Object loadedApk = ReflectUtil.getPackageInfo(base);            
  6. // 替换LoadedApk中的mResources
  7. ReflectUtil.setField(loadedApk.getClass(), loadedApk, "mResources", resources);            
  8. Object activityThread = ReflectUtil.getActivityThread(base);            
  9. Object resManager = ReflectUtil.getField(activityThread.getClass(), activityThread, "mResourcesManager");         
  10. if (Build.VERSION.SDK_INT < 24) {                
  11. Map<Object, WeakReference<Resources>> map = (Map<Object, WeakReference<Resources>>) ReflectUtil.getField(resManager.getClass(), resManager, "mActiveResources");                
  12. Object key = map.keySet().iterator().next();                
  13. map.put(key, new WeakReference<>(resources));            
  14. } else {                
  15. // still hook Android N Resources, even though it's unnecessary, then nobody will be strange.                
  16. Map map = (Map) ReflectUtil.getFieldNoException(resManager.getClass(), resManager, "mResourceImpls");                
  17. Object key = map.keySet().iterator().next();                
  18. Object resourcesImpl = ReflectUtil.getFieldNoException(Resources.class, resources, "mResourcesImpl");                
  19. map.put(key, new WeakReference<>(resourcesImpl));            
  20. }    
  21. } catch (Exception e) {        
  22. e.printStackTrace();
  23. }
  24. }

上述代码hook了三个点:

第三步:关联Activity和Resources

  1. Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent); activity.setIntent(intent);
  2. // 设置Activity的mResources属性,Activity中访问资源时都通过mResources
  3. ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources());

在Activity生成后替换其父类ContextThemeWrapper的成员变量mResources,这样在插件Activity中直接可以通过getResources获取Resources对象。

接下来就可以在插件的Activity中使用setContentView、inflate等方法来加载布局了。

3.2.2 资源id冲突

在合并式的资源处理方式中,很大概率会产生资源冲突,因为不同的apk编译打包过程中,资源id名称可能会相同。那么解决冲突就是让宿主和插件的资源id保持不同。

资源id由8位16进制表示,格式为0xPPTTNNNN,其中PP段用来区分包空间,TT段用来区分资源类型,NNNN段是在apk里从0000开始递增。

undefined

那么要保证宿主和插件的资源id不一致,我们一般修改PP段的值即可,有如下两种方式:

由于第一种修改AAPT工具侵入性比较大,所以推荐使用第二种,可参考 插件化-解决插件资源ID与宿主资源ID冲突的问题

4. 四大组件支持及生命周期管理

4.1 Activity生命周期管理

经过上述步骤后,便实现了插件Activity的启动,并且该插件Activity中并不需要什么额外的处理,和常规的Activity一样。那问题来了,之后的onResume,onStop等生命周期怎么办呢?答案是所有和Activity相关的生命周期函数,系统都会调用插件中的Activity。原因在于AMS在处理Activity时,通过一个token表示具体Activity对象,而这个token正是和启动Activity时创建的对象对应的,而这个Activity被我们替换成了插件中的Activity,所以之后AMS的所有调用都会传给插件中的Activity。

4.2 其它组件支持及生命周期管理

四大组件中Activity的支持是最复杂的,其他组件的实现原理要简单很多,简要概括如下:

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注