深入理解Android插件化技術原理
前言
插件化技術最初源于免安裝運行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方法加載類,代碼如下:
- protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
- //首先從已經加載的類中查找
- Class<?> clazz = findLoadedClass(className);
- if (clazz == null) {
- ClassNotFoundException suppressed = null;
- try {
- //如果沒有加載過,先調用父加載器的loadClass
- clazz = parent.loadClass(className, false);
- } catch (ClassNotFoundException e) {
- suppressed = e;
- }
- if (clazz == null) {
- try {
- //父加載器都沒有加載,則嘗試加載
- clazz = findClass(className);
- } catch (ClassNotFoundException e) {
- e.addSuppressed(suppressed);
- throw e;
- }
- }
- }
- return clazz;
- }
可以看出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,后面再通過字節碼替換來自動化完成這部操作,后面再說為什么,我們先看偽代碼;
- public class ContainerActivity extends Activity {
- private PluginActivity pluginActivity;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- String pluginActivityName = getIntent().getString("pluginActivityName", "");
- pluginActivity = PluginLoader.loadActivity(pluginActivityName, this);
- if (pluginActivity == null) {
- super.onCreate(savedInstanceState);
- return;
- }
- pluginActivity.onCreate();
- }
- @Override
- protected void onResume() {
- if (pluginActivity == null) {
- super.onResume();
- return;
- }
- pluginActivity.onResume();
- }
- @Override
- protected void onPause() {
- if (pluginActivity == null) {
- super.onPause();
- return;
- }
- pluginActivity.onPause();
- }
- // ...
- }
- public class PluginActivity {
- private ContainerActivity containerActivity;
- public PluginActivity(ContainerActivity containerActivity) {
- this.containerActivity = containerActivity;
- }
- @Override
- public <T extends View> T findViewById(int id) {
- return containerActivity.findViewById(id);
- }
- // ...
- }
- // 插件 `Apk` 中真正寫的組件
- public class TestActivity extends PluginActivity {
- // ......
- }
但大概原理就是這么簡單,啟動插件組件需要依賴容器,容器負責加載插件組件并且完成雙向轉發,轉發來自系統的生命周期回調至插件組件,同時轉發來自插件組件的系統調用至系統;
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 來創建資源實例,大概代碼像這樣:
- PackageManager packageManager = getPackageManager();
- PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(
- pluginApkPath,
- PackageManager.GET_ACTIVITIES
- | PackageManager.GET_META_DATA
- | PackageManager.GET_SERVICES
- | PackageManager.GET_PROVIDERS
- | PackageManager.GET_SIGNATURES
- );
- packageArchiveInfo.applicationInfo.sourceDir = pluginApkPath;
- packageArchiveInfo.applicationInfo.publicSourceDir = pluginApkPath;
- Resources injectResources = null;
- try {
- injectResources = packageManager.getResourcesForApplication(packageArchiveInfo.applicationInfo);
- } catch (PackageManager.NameNotFoundException e) {
- // ...
- }
拿到資源實例后,我們需要將宿主的資源和插件資源 Merge 一下,編寫一個新的 Resources 類,用這樣的方式完成自動代理:
- public class PluginResources extends Resources {
- private Resources hostResources;
- private Resources injectResources;
- public PluginResources(Resources hostResources, Resources injectResources) {
- super(injectResources.getAssets(), injectResources.getDisplayMetrics(), injectResources.getConfiguration());
- this.hostResources = hostResources;
- this.injectResources = injectResources;
- }
- @Override
- public String getString(int id, Object... formatArgs) throws NotFoundException {
- try {
- return injectResources.getString(id, formatArgs);
- } catch (NotFoundException e) {
- return hostResources.getString(id, formatArgs);
- }
- }
- // ...
- }
然后我們在 ContainerActivity 完成插件組件加載后,創建一份 Merge 資源,再復寫 ContainerActivity#getResources,將獲取到的資源替換掉:
- public class ContainerActivity extends Activity {
- private Resources pluginResources;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- // ...
- pluginResources = new PluginResources(super.getResources(), PluginLoader.getResources(pluginApkPath));
- // ...
- }
- @Override
- public Resources getResources() {
- if (pluginActivity == null) {
- return super.getResources();
- }
- return pluginResources;
- }
- }
這樣就完成了資源的注入
4、解決資源沖突
合并式的資源處理方式,會引入資源沖突,原因在于不同插件中的資源id可能相同,所以解決方法就是使得不同的插件資源擁有不同的資源id;
資源id是由8位16進制數表示,表示為0xPPTTNNNN。PP段用來區分包空間,默認只區分了應用資源和系統資源,TT段為資源類型,NNNN段在同一個APK中從0000遞增;
總結
市面上的插件化框架實際很多,如 Tecent 的 Shadow、Didi 的 VirtualApk、360 的 RePlugin。他們各有各的長處,不過大體上差不多;
他們大體原理其實都差不多,運行時會有一個宿主 Apk 在進程中跑,宿舍 Apk 是真正被安裝的應用,宿主 Apk 可以加載插件 Apk 中的組件和代碼運行,插件 Apk 可以任意熱更新;