深入理解Android插件化技术原理

移动开发 Android
支持插件化的app可以在运行时加载和运行插件,这样便可以将app中一些不常用的功能模块做成插件,一方面减小了安装包的大小,另一方面可以实现app功能的动态扩展;

[[431328]]

前言

插件化技术最初源于免安装运行apk的想法,这个免安装的apk可以理解为插件。

支持插件化的app可以在运行时加载和运行插件,这样便可以将app中一些不常用的功能模块做成插件,一方面减小了安装包的大小,另一方面可以实现app功能的动态扩展;

今天我们就来讲下插件化

一、插件化介绍

1、插件化介绍

在 Android 系统中,应用是以 Apk 的形式存在的,应用都需要安装才能使用。但实际上 Android 系统安装应用的方式相当简单,其实就是把应用 Apk 拷贝到系统不同的目录下、然后把 so 解压出来而已;

常见的应用安装目录有:

  • /system/app:系统应用
  • /system/priv-app:系统应用
  • /data/app:用户应用

Apk 的构成,一个常见的 Apk 会包含如下几个部分:

  • classes.dex:Java 代码字节码
  • res:资源目录
  • lib:so 目录
  • assets:静态资产目录
  • AndroidManifest.xml:清单文件

其实 Android 系统在打开应用之后,也只是开辟进程,然后使用 ClassLoader 加载 classes.dex 至进程中,执行对应的组件而已;

那大家可能会想一个问题,既然 Android 本身也是使用类似反射的形式加载代码执行,凭什么我们不能执行一个 Apk 中的代码呢?

这其实就是插件化的目的,让 Apk 中的代码(主要是指 Android 组件)能够免安装运行,这样能够带来很多收益,最显而易见的优势其实就是通过网络热更新、热修复;

2、插件化技术难点

  • 反射并执行插件 Apk 中的代码(ClassLoader Injection)
  • 让系统能调用插件 Apk 中的组件(Runtime Container)
  • 正确识别插件 Apk 中的资源(Resource Injection)

3、双亲委托机制

ClassLoader调用loadClass方法加载类,代码如下:

  1. protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {  
  2.        //首先从已经加载的类中查找 
  3.         Class<?> clazz = findLoadedClass(className);     
  4.     if (clazz == null) { 
  5.             ClassNotFoundException suppressed = null;      
  6.            try {    
  7.                 //如果没有加载过,先调用父加载器的loadClass 
  8.                 clazz = parent.loadClass(className, false); 
  9.             } catch (ClassNotFoundException e) { 
  10.                 suppressed = e; 
  11.             }       
  12.         if (clazz == null) {         
  13.                 try {            
  14.                   //父加载器都没有加载,则尝试加载 
  15.                     clazz = findClass(className); 
  16.                 } catch (ClassNotFoundException e) { 
  17.                     e.addSuppressed(suppressed);        
  18.                      throw e; 
  19.                 } 
  20.             } 
  21.         }     
  22.             return clazz; 
  23.     } 

可以看出ClassLoader加载类时,先查看自身是否已经加载过该类,如果没有加载过会首先让父加载器去加载,如果父加载器无法加载该类时才会调用自身的findClass方法加载,该机制很大程度上避免了类的重复加载;

二、插件化详解

1、ClassLoader Injection

简单来说,插件化场景下,会存在同一进程中多个 ClassLoader 的场景:

  • 宿主 ClassLoader:宿主是安装应用,运行即自动创建
  • 插件 ClassLoader:使用 new DexClassLoader 创建

我们称这个过程叫做 ClassLoader 注入;

完成注入后,所有来自宿主的类使用宿主的 ClassLoader 进行加载,所有来自插件 Apk 的类使用插件 ClassLoader 进行加载;

而由于 ClassLoader 的双亲委派机制,实际上系统类会不受 ClassLoader 的类隔离机制所影响,这样宿主 Apk 就可以在宿主进程中使用来自于插件的组件类了;

2、Runtime Container

ClassLoader 注入后,就可以在宿主进程中使用插件 Apk 中的类,但是我们都知道 Android 组件都是由系统调用启动的,未安装的 Apk 中的组件,是未注册到 AMS 和 PMS 的,就好比你直接使用 startActivity 启动一个插件 Apk 中的组件,系统会告诉你无法找到;

我们的解决方案很简单,即运行时容器技术,简单来说就是在宿主 Apk 中预埋一些空的 Android 组件,以 Activity 为例,我预置一个 ContainerActivity extends Activity 在宿主中,并且在 AndroidManifest.xml 中注册它;

它要做的事情很简单,就是帮助我们作为插件 Activity 的容器,它从 Intent 接受几个参数,分别是插件的不同信息,如:

  • pluginName;
  • pluginApkPath;
  • pluginActivityName等,其实最重要的就是 pluginApkPath 和 pluginActivityName,当 ContainerActivity 启动时,我们就加载插件的 ClassLoader、Resource,并反射 pluginActivityName 对应的 Activity 类;

当完成加载后,ContainerActivity 要做两件事:

  • 转发所有来自系统的生命周期回调至插件 Activity
  • 接受 Activity 方法的系统调用,并转发回系统

我们可以通过复写 ContainerActivity 的生命周期方法来完成第一步,而第二步我们需要定义一个 PluginActivity,然后在编写插件 Apk 中的 Activity 组件时,不再让其集成 android.app.Activity,而是集成自我们的 PluginActivity,后面再通过字节码替换来自动化完成这部操作,后面再说为什么,我们先看伪代码;

  1. public class ContainerActivity extends Activity { 
  2.     private PluginActivity pluginActivity; 
  3.     @Override 
  4.     protected void onCreate(Bundle savedInstanceState) { 
  5.         String pluginActivityName = getIntent().getString("pluginActivityName"""); 
  6.         pluginActivity = PluginLoader.loadActivity(pluginActivityName, this); 
  7.         if (pluginActivity == null) { 
  8.             super.onCreate(savedInstanceState); 
  9.             return
  10.         } 
  11.         pluginActivity.onCreate(); 
  12.     } 
  13.     @Override 
  14.     protected void onResume() { 
  15.         if (pluginActivity == null) { 
  16.             super.onResume(); 
  17.             return
  18.         } 
  19.         pluginActivity.onResume(); 
  20.     } 
  21.     @Override 
  22.     protected void onPause() { 
  23.         if (pluginActivity == null) { 
  24.             super.onPause(); 
  25.             return
  26.         } 
  27.         pluginActivity.onPause(); 
  28.     } 
  29.     // ... 
  30. public class PluginActivity { 
  31.     private ContainerActivity containerActivity; 
  32.     public PluginActivity(ContainerActivity containerActivity) { 
  33.         this.containerActivity = containerActivity; 
  34.     } 
  35.     @Override 
  36.     public <T extends View> T findViewById(int id) { 
  37.         return containerActivity.findViewById(id); 
  38.     } 
  39.     // ... 
  40. // 插件 `Apk` 中真正写的组件 
  41. public class TestActivity extends PluginActivity { 
  42.     // ...... 

但大概原理就是这么简单,启动插件组件需要依赖容器,容器负责加载插件组件并且完成双向转发,转发来自系统的生命周期回调至插件组件,同时转发来自插件组件的系统调用至系统;

3、Resource Injection

最后要说的是资源注入,其实这一点相当重要,Android 应用的开发其实崇尚的是逻辑与资源分离的理念,所有资源(layout、values 等)都会被打包到 Apk 中,然后生成一个对应的 R 类,其中包含对所有资源的引用 id;

资源的注入并不容易,好在 Android 系统给我们留了一条后路,最重要的是这两个接口:

  • PackageManager#getPackageArchiveInfo:根据 Apk 路径解析一个未安装的 Apk 的 PackageInfo;
  • PackageManager#getResourcesForApplication:根据 ApplicationInfo 创建一个 Resources 实例;

我们要做的就是在上面 ContainerActivity#onCreate 中加载插件 Apk 的时候,用这两个方法创建出来一份插件资源实例。具体来说就是先用 PackageManager#getPackageArchiveInfo 拿到插件 Apk 的 PackageInfo,有了 PacakgeInfo 之后我们就可以自己组装一份 ApplicationInfo,然后通过 PackageManager#getResourcesForApplication 来创建资源实例,大概代码像这样:

  1. PackageManager packageManager = getPackageManager(); 
  2. PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo( 
  3.     pluginApkPath, 
  4.     PackageManager.GET_ACTIVITIES 
  5.     | PackageManager.GET_META_DATA 
  6.     | PackageManager.GET_SERVICES 
  7.     | PackageManager.GET_PROVIDERS 
  8.     | PackageManager.GET_SIGNATURES 
  9. ); 
  10. packageArchiveInfo.applicationInfo.sourceDir = pluginApkPath; 
  11. packageArchiveInfo.applicationInfo.publicSourceDir = pluginApkPath; 
  12. Resources injectResources = null
  13. try { 
  14.     injectResources = packageManager.getResourcesForApplication(packageArchiveInfo.applicationInfo); 
  15. } catch (PackageManager.NameNotFoundException e) { 
  16.     // ... 

拿到资源实例后,我们需要将宿主的资源和插件资源 Merge 一下,编写一个新的 Resources 类,用这样的方式完成自动代理:

  1. public class PluginResources extends Resources { 
  2.     private Resources hostResources; 
  3.     private Resources injectResources; 
  4.     public PluginResources(Resources hostResources, Resources injectResources) { 
  5.         super(injectResources.getAssets(), injectResources.getDisplayMetrics(), injectResources.getConfiguration()); 
  6.         this.hostResources = hostResources; 
  7.         this.injectResources = injectResources; 
  8.     } 
  9.     @Override 
  10.     public String getString(int id, Object... formatArgs) throws NotFoundException { 
  11.         try { 
  12.             return injectResources.getString(id, formatArgs); 
  13.                     } catch (NotFoundException e) { 
  14.  
  15.             return hostResources.getString(id, formatArgs); 
  16.         } 
  17.     } 
  18.     // ... 

然后我们在 ContainerActivity 完成插件组件加载后,创建一份 Merge 资源,再复写 ContainerActivity#getResources,将获取到的资源替换掉:

  1. public class ContainerActivity extends Activity { 
  2.     private Resources pluginResources; 
  3.     @Override 
  4.     protected void onCreate(Bundle savedInstanceState) { 
  5.         // ... 
  6.         pluginResources = new PluginResources(super.getResources(), PluginLoader.getResources(pluginApkPath)); 
  7.         // ... 
  8.     } 
  9.     @Override 
  10.     public Resources getResources() { 
  11.         if (pluginActivity == null) { 
  12.             return super.getResources(); 
  13.         } 
  14.         return pluginResources; 
  15.     } 

这样就完成了资源的注入

4、解决资源冲突

合并式的资源处理方式,会引入资源冲突,原因在于不同插件中的资源id可能相同,所以解决方法就是使得不同的插件资源拥有不同的资源id;

资源id是由8位16进制数表示,表示为0xPPTTNNNN。PP段用来区分包空间,默认只区分了应用资源和系统资源,TT段为资源类型,NNNN段在同一个APK中从0000递增;

总结

市面上的插件化框架实际很多,如 Tecent 的 Shadow、Didi 的 VirtualApk、360 的 RePlugin。他们各有各的长处,不过大体上差不多;

他们大体原理其实都差不多,运行时会有一个宿主 Apk 在进程中跑,宿舍 Apk 是真正被安装的应用,宿主 Apk 可以加载插件 Apk 中的组件和代码运行,插件 Apk 可以任意热更新;

 

责任编辑:武晓燕 来源: Android开发编程
相关推荐

2024-03-12 00:00:00

Sora技术数据

2012-11-22 13:02:24

jQuery插件Web

2023-06-29 08:41:02

2020-08-10 18:03:54

Cache存储器CPU

2011-04-28 11:01:40

Android消息处理LooperHandler

2021-09-10 07:31:54

AndroidAppStartup原理

2021-03-10 10:55:51

SpringJava代码

2022-11-04 09:43:05

Java线程

2022-09-05 08:39:04

kubernetesk8s

2021-09-08 06:51:52

AndroidRetrofit原理

2021-10-15 09:19:17

AndroidSharedPrefe分析源码

2019-07-01 13:34:22

vue系统数据

2020-11-04 15:35:13

Golang内存程序员

2022-09-05 22:22:00

Stream操作对象

2020-03-17 08:36:22

数据库存储Mysql

2023-10-13 13:30:00

MySQL锁机制

2023-11-13 16:33:46

2023-09-19 22:47:39

Java内存

2020-03-26 16:40:07

MySQL索引数据库

2022-01-14 12:28:18

架构OpenFeign远程
点赞
收藏

51CTO技术栈公众号