自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

深入理解Android插件化技術(shù)原理

移動(dòng)開(kāi)發(fā) Android
支持插件化的app可以在運(yùn)行時(shí)加載和運(yùn)行插件,這樣便可以將app中一些不常用的功能模塊做成插件,一方面減小了安裝包的大小,另一方面可以實(shí)現(xiàn)app功能的動(dòng)態(tài)擴(kuò)展;

[[431328]]

前言

插件化技術(shù)最初源于免安裝運(yùn)行apk的想法,這個(gè)免安裝的apk可以理解為插件。

支持插件化的app可以在運(yùn)行時(shí)加載和運(yùn)行插件,這樣便可以將app中一些不常用的功能模塊做成插件,一方面減小了安裝包的大小,另一方面可以實(shí)現(xiàn)app功能的動(dòng)態(tài)擴(kuò)展;

今天我們就來(lái)講下插件化

一、插件化介紹

1、插件化介紹

在 Android 系統(tǒng)中,應(yīng)用是以 Apk 的形式存在的,應(yīng)用都需要安裝才能使用。但實(shí)際上 Android 系統(tǒng)安裝應(yīng)用的方式相當(dāng)簡(jiǎn)單,其實(shí)就是把應(yīng)用 Apk 拷貝到系統(tǒng)不同的目錄下、然后把 so 解壓出來(lái)而已;

常見(jiàn)的應(yīng)用安裝目錄有:

  • /system/app:系統(tǒng)應(yīng)用
  • /system/priv-app:系統(tǒng)應(yīng)用
  • /data/app:用戶應(yīng)用

Apk 的構(gòu)成,一個(gè)常見(jiàn)的 Apk 會(huì)包含如下幾個(gè)部分:

  • classes.dex:Java 代碼字節(jié)碼
  • res:資源目錄
  • lib:so 目錄
  • assets:靜態(tài)資產(chǎn)目錄
  • AndroidManifest.xml:清單文件

其實(shí) Android 系統(tǒng)在打開(kāi)應(yīng)用之后,也只是開(kāi)辟進(jìn)程,然后使用 ClassLoader 加載 classes.dex 至進(jìn)程中,執(zhí)行對(duì)應(yīng)的組件而已;

那大家可能會(huì)想一個(gè)問(wèn)題,既然 Android 本身也是使用類似反射的形式加載代碼執(zhí)行,憑什么我們不能執(zhí)行一個(gè) Apk 中的代碼呢?

這其實(shí)就是插件化的目的,讓 Apk 中的代碼(主要是指 Android 組件)能夠免安裝運(yùn)行,這樣能夠帶來(lái)很多收益,最顯而易見(jiàn)的優(yōu)勢(shì)其實(shí)就是通過(guò)網(wǎng)絡(luò)熱更新、熱修復(fù);

2、插件化技術(shù)難點(diǎn)

  • 反射并執(zhí)行插件 Apk 中的代碼(ClassLoader Injection)
  • 讓系統(tǒng)能調(diào)用插件 Apk 中的組件(Runtime Container)
  • 正確識(shí)別插件 Apk 中的資源(Resource Injection)

3、雙親委托機(jī)制

ClassLoader調(diào)用loadClass方法加載類,代碼如下:

  1. protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {  
  2.        //首先從已經(jīng)加載的類中查找 
  3.         Class<?> clazz = findLoadedClass(className);     
  4.     if (clazz == null) { 
  5.             ClassNotFoundException suppressed = null;      
  6.            try {    
  7.                 //如果沒(méi)有加載過(guò),先調(diào)用父加載器的loadClass 
  8.                 clazz = parent.loadClass(className, false); 
  9.             } catch (ClassNotFoundException e) { 
  10.                 suppressed = e; 
  11.             }       
  12.         if (clazz == null) {         
  13.                 try {            
  14.                   //父加載器都沒(méi)有加載,則嘗試加載 
  15.                     clazz = findClass(className); 
  16.                 } catch (ClassNotFoundException e) { 
  17.                     e.addSuppressed(suppressed);        
  18.                      throw e; 
  19.                 } 
  20.             } 
  21.         }     
  22.             return clazz; 
  23.     } 

可以看出ClassLoader加載類時(shí),先查看自身是否已經(jīng)加載過(guò)該類,如果沒(méi)有加載過(guò)會(huì)首先讓父加載器去加載,如果父加載器無(wú)法加載該類時(shí)才會(huì)調(diào)用自身的findClass方法加載,該機(jī)制很大程度上避免了類的重復(fù)加載;

二、插件化詳解

1、ClassLoader Injection

簡(jiǎn)單來(lái)說(shuō),插件化場(chǎng)景下,會(huì)存在同一進(jìn)程中多個(gè) ClassLoader 的場(chǎng)景:

  • 宿主 ClassLoader:宿主是安裝應(yīng)用,運(yùn)行即自動(dòng)創(chuàng)建
  • 插件 ClassLoader:使用 new DexClassLoader 創(chuàng)建

我們稱這個(gè)過(guò)程叫做 ClassLoader 注入;

完成注入后,所有來(lái)自宿主的類使用宿主的 ClassLoader 進(jìn)行加載,所有來(lái)自插件 Apk 的類使用插件 ClassLoader 進(jìn)行加載;

而由于 ClassLoader 的雙親委派機(jī)制,實(shí)際上系統(tǒng)類會(huì)不受 ClassLoader 的類隔離機(jī)制所影響,這樣宿主 Apk 就可以在宿主進(jìn)程中使用來(lái)自于插件的組件類了;

2、Runtime Container

ClassLoader 注入后,就可以在宿主進(jìn)程中使用插件 Apk 中的類,但是我們都知道 Android 組件都是由系統(tǒng)調(diào)用啟動(dòng)的,未安裝的 Apk 中的組件,是未注冊(cè)到 AMS 和 PMS 的,就好比你直接使用 startActivity 啟動(dòng)一個(gè)插件 Apk 中的組件,系統(tǒng)會(huì)告訴你無(wú)法找到;

我們的解決方案很簡(jiǎn)單,即運(yùn)行時(shí)容器技術(shù),簡(jiǎn)單來(lái)說(shuō)就是在宿主 Apk 中預(yù)埋一些空的 Android 組件,以 Activity 為例,我預(yù)置一個(gè) ContainerActivity extends Activity 在宿主中,并且在 AndroidManifest.xml 中注冊(cè)它;

它要做的事情很簡(jiǎn)單,就是幫助我們作為插件 Activity 的容器,它從 Intent 接受幾個(gè)參數(shù),分別是插件的不同信息,如:

  • pluginName;
  • pluginApkPath;
  • pluginActivityName等,其實(shí)最重要的就是 pluginApkPath 和 pluginActivityName,當(dāng) ContainerActivity 啟動(dòng)時(shí),我們就加載插件的 ClassLoader、Resource,并反射 pluginActivityName 對(duì)應(yīng)的 Activity 類;

當(dāng)完成加載后,ContainerActivity 要做兩件事:

  • 轉(zhuǎn)發(fā)所有來(lái)自系統(tǒng)的生命周期回調(diào)至插件 Activity
  • 接受 Activity 方法的系統(tǒng)調(diào)用,并轉(zhuǎn)發(fā)回系統(tǒng)

我們可以通過(guò)復(fù)寫 ContainerActivity 的生命周期方法來(lái)完成第一步,而第二步我們需要定義一個(gè) PluginActivity,然后在編寫插件 Apk 中的 Activity 組件時(shí),不再讓其集成 android.app.Activity,而是集成自我們的 PluginActivity,后面再通過(guò)字節(jié)碼替換來(lái)自動(dòng)化完成這部操作,后面再說(shuō)為什么,我們先看偽代碼;

  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.     // ...... 

但大概原理就是這么簡(jiǎn)單,啟動(dòng)插件組件需要依賴容器,容器負(fù)責(zé)加載插件組件并且完成雙向轉(zhuǎn)發(fā),轉(zhuǎn)發(fā)來(lái)自系統(tǒng)的生命周期回調(diào)至插件組件,同時(shí)轉(zhuǎn)發(fā)來(lái)自插件組件的系統(tǒng)調(diào)用至系統(tǒng);

3、Resource Injection

最后要說(shuō)的是資源注入,其實(shí)這一點(diǎn)相當(dāng)重要,Android 應(yīng)用的開(kāi)發(fā)其實(shí)崇尚的是邏輯與資源分離的理念,所有資源(layout、values 等)都會(huì)被打包到 Apk 中,然后生成一個(gè)對(duì)應(yīng)的 R 類,其中包含對(duì)所有資源的引用 id;

資源的注入并不容易,好在 Android 系統(tǒng)給我們留了一條后路,最重要的是這兩個(gè)接口:

  • PackageManager#getPackageArchiveInfo:根據(jù) Apk 路徑解析一個(gè)未安裝的 Apk 的 PackageInfo;
  • PackageManager#getResourcesForApplication:根據(jù) ApplicationInfo 創(chuàng)建一個(gè) Resources 實(shí)例;

我們要做的就是在上面 ContainerActivity#onCreate 中加載插件 Apk 的時(shí)候,用這兩個(gè)方法創(chuàng)建出來(lái)一份插件資源實(shí)例。具體來(lái)說(shuō)就是先用 PackageManager#getPackageArchiveInfo 拿到插件 Apk 的 PackageInfo,有了 PacakgeInfo 之后我們就可以自己組裝一份 ApplicationInfo,然后通過(guò) PackageManager#getResourcesForApplication 來(lái)創(chuàng)建資源實(shí)例,大概代碼像這樣:

  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.     // ... 

拿到資源實(shí)例后,我們需要將宿主的資源和插件資源 Merge 一下,編寫一個(gè)新的 Resources 類,用這樣的方式完成自動(dòng)代理:

  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.     // ... 

然后我們?cè)?ContainerActivity 完成插件組件加載后,創(chuàng)建一份 Merge 資源,再?gòu)?fù)寫 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、解決資源沖突

合并式的資源處理方式,會(huì)引入資源沖突,原因在于不同插件中的資源id可能相同,所以解決方法就是使得不同的插件資源擁有不同的資源id;

資源id是由8位16進(jìn)制數(shù)表示,表示為0xPPTTNNNN。PP段用來(lái)區(qū)分包空間,默認(rèn)只區(qū)分了應(yīng)用資源和系統(tǒng)資源,TT段為資源類型,NNNN段在同一個(gè)APK中從0000遞增;

總結(jié)

市面上的插件化框架實(shí)際很多,如 Tecent 的 Shadow、Didi 的 VirtualApk、360 的 RePlugin。他們各有各的長(zhǎng)處,不過(guò)大體上差不多;

他們大體原理其實(shí)都差不多,運(yùn)行時(shí)會(huì)有一個(gè)宿主 Apk 在進(jìn)程中跑,宿舍 Apk 是真正被安裝的應(yīng)用,宿主 Apk 可以加載插件 Apk 中的組件和代碼運(yùn)行,插件 Apk 可以任意熱更新;

 

責(zé)任編輯:武曉燕 來(lái)源: Android開(kāi)發(fā)編程
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)