《Android插件化技术——原理篇》
| 導語 插件化技術最早從2012年誕生至今,已經走過了5個年頭。從最初只支持Activity的動態加載發展到可以完全模擬app運行時的沙箱系統,各種開源項目層出不窮,在此挑選了幾個代表性的框架,總結其中的技術原理。由于本人水平有限,插件化框架又相當復雜,文中若有錯誤或者不準確的地方望高手指點。
內容概要
一、發展歷史
插件化技術最初源于免安裝運行apk的想法,這個免安裝的apk可以理解為插件。支持插件化的app可以在運行時加載和運行插件,這樣便可以將app中一些不常用的功能模塊做成插件,一方面減小了安裝包的大小,另一方面可以實現app功能的動態擴展。想要實現插件化,主要是解決下面三個問題:
插件中代碼的加載和與主工程的互相調用
插件中資源的加載和與主工程的互相訪問
四大組件生命周期的管理
下面是比較出名的幾個開源的插件化框架,按照出現的時間排序。研究它們的實現原理,可以大致看出插件化技術的發展,根據實現原理我把這幾個框架劃分成了三代。
第一代:dynamic-load-apk最早使用ProxyActivity這種靜態代理技術,由ProxyActivity去控制插件中PluginActivity的生命周期。該種方式缺點明顯,插件中的activity必須繼承PluginActivity,開發時要小心處理context。而DroidPlugin通過Hook系統服務的方式啟動插件中的Activity,使得開發插件的過程和開發普通的app沒有什么區別,但是由于hook過多系統服務,異常復雜且不夠穩定。
第二代:為了同時達到插件開發的低侵入性(像開發普通app一樣開發插件)和框架的穩定性,在實現原理上都是趨近于選擇盡量少的hook,并通過在manifest中預埋一些組件實現對四大組件的插件化。另外各個框架根據其設計思想都做了不同程度的擴展,其中Small更是做成了一個跨平臺,組件化的開發框架。
第三代:VirtualApp比較厲害,能夠完全模擬app的運行環境,能夠實現app的免安裝運行和雙開技術。Atlas是阿里今年開源出來的一個結合組件化和熱修復技術的一個app基礎框架,其廣泛的應用與阿里系的各個app,其號稱是一個容器化框架。
下面詳細介紹插件化框架的原理,分別對應著實現插件化的三個核心問題。
二、基本原理
2.1 類加載
外部apk中類的加載
Android中常用的有兩種類加載器,DexClassLoader和PathClassLoader,它們都繼承于BaseDexClassLoader。
// DexClassLoaderpublic class DexClassLoader extends BaseDexClassLoader { ? ?public DexClassLoader(String dexPath, String optimizedDirectory, ? ? ? ? ? ?String libraryPath, ClassLoader parent) { ? ? ? ?super(dexPath, new File(optimizedDirectory), libraryPath, parent); ? ?} } // PathClassLoader public class PathClassLoader extends BaseDexClassLoader { ? ?public PathClassLoader(String dexPath, ClassLoader parent) { ? ? ? ?super(dexPath, null, null, parent); ? ?} ? ?? ?public PathClassLoader(String dexPath, String libraryPath, ? ? ? ? ? ?ClassLoader parent) { ? ? ? ?super(dexPath, null, libraryPath, parent); ? ?} }
區別在于調用父類構造器時,DexClassLoader多傳了一個optimizedDirectory參數,這個目錄必須是內部存儲路徑,用來緩存系統創建的Dex文件。而PathClassLoader該參數為null,只能加載內部存儲目錄的Dex文件。
所以我們可以用DexClassLoader去加載外部的apk,用法如下
//第一個參數為apk的文件目錄//第二個參數為內部存儲目錄//第三個為庫文件的存儲目錄//第四個參數為父加載器new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent)
雙親委托機制
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方法加載,該機制很大程度上避免了類的重復加載。
DexClassLoader的DexPathList
DexClassLoader重載了findClass方法,在加載類時會調用其內部的DexPathList去加載。DexPathList是在構造DexClassLoader時生成的,其內部包含了DexFile。如下圖所示
DexPathList的loadClass會去遍歷DexFile直到找到需要加載的類
public Class findClass(String name, List<Throwable> suppressed) { ? ? ? ?//循環dexElements,調用DexFile.loadClassBinaryName加載class ? ? ? ?for (Element element : dexElements) { ? ? ? ? ? ?DexFile dex = element.dexFile; ? ? ? ? ? ?if (dex != null) { ? ? ? ? ? ? ? ?Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed); ? ? ? ? ?? ? ? ? ? ? ?if (clazz != null) { ? ? ? ? ?? ? ? ? ? ? ?? return clazz; ? ? ? ? ? ? ? ?} ? ? ? ? ? ?} ? ? ? ?} ? ? ? ?if (dexElementsSuppressedExceptions != null) { ? ? ? ? ? ?suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions)); ? ? ? ?} ? ? ? ? ?? ?return null; ? ?}
有一種熱修復技術正是利用了DexClassLoader的加載機制,將需要替換的類添加到dexElements的前面,這樣系統會使用先找到的修復過的類。
2.2 單DexClassLoader與多DexClassLoader
通過給插件apk生成相應的DexClassLoader便可以訪問其中的類,這邊又有兩種處理方式,有單DexClassLoader和多DexClassLoader兩種結構。
多DexClassLoader?
對于每個插件都會生成一個DexClassLoader,當加載該插件中的類時需要通過對應DexClassLoader加載。這樣不同插件的類是隔離的,當不同插件引用了同一個類庫的不同版本時,不會出問題。RePlugin采用的是該方案。
單DexClassLoader
將插件的DexClassLoader中的pathList合并到主工程的DexClassLoader中。這樣做的好處時,可以在不同的插件以及主工程間直接互相調用類和方法,并且可以將不同插件的公共模塊抽出來放在一個common插件中直接供其他插件使用。Small采用的是這種方式。
互相調用
插件和主工程的互相調用涉及到以下兩個問題
插件調用主工程
在構造插件的ClassLoader時會傳入主工程的ClassLoader作為父加載器,所以插件是可以直接可以通過類名引用主工程的類。
主工程調用插件
若使用多ClassLoader機制,主工程引用插件中類需要先通過插件的ClassLoader加載該類再通過反射調用其方法。插件化框架一般會通過統一的入口去管理對各個插件中類的訪問,并且做一定的限制。
若使用單ClassLoader機制,主工程則可以直接通過類名去訪問插件中的類。該方式有個弊病,若兩個不同的插件工程引用了一個庫的不同版本,則程序可能會出錯,所以要通過一些規范去避免該情況發生。
2.3 資源加載
Android系統通過Resource對象加載資源,下面代碼展示了該對象的生成過程
//創建AssetManager對象 AssetManager assets = new AssetManager(); //將apk路徑添加到AssetManager中 ?if (assets.addAssetPath(resDir) == 0){ ? ? ? ? ? ? ? ? ?return null; ? } //創建Resource對象 r = new Resources(assets, metrics, getConfiguration(), compInfo);
因此,只要將插件apk的路徑加入到AssetManager中,便能夠實現對插件資源的訪問。
具體實現時,由于AssetManager并不是一個public的類,需要通過反射去創建,并且部分Rom對創建的Resource類進行了修改,所以需要考慮不同Rom的兼容性。
資源路徑的處理
和代碼加載相似,插件和主工程的資源關系也有兩種處理方式
合并式:addAssetPath時加入所有插件和主工程的路徑
獨立式:各個插件只添加自己apk路徑
合并式由于AssetManager中加入了所有插件和主工程的路徑,因此生成的Resource可以同時訪問插件和主工程的資源。但是由于主工程和各個插件都是獨立編譯的,生成的資源id會存在相同的情況,在訪問時會產生資源沖突。
獨立式時,各個插件的資源是互相隔離的,不過如果想要實現資源的共享,必須拿到對應的Resource對象。
Context的處理
通常我們通過Context對象訪問資源,光創建出Resource對象還不夠,因此還需要一些額外的工作。 對資源訪問的不同實現方式也需要不同的額外工作。以VirtualAPK的處理方式為例
第一步:創建Resource
if (Constants.COMBINE_RESOURCES) { ? ?//插件和主工程資源合并時需要hook住主工程的資源 ? ?Resources resources = ResourcesManager.createResources(context, apk.getAbsolutePath()); ? ?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()); }
第二步:hook主工程的Resource
對于合并式的資源訪問方式,需要替換主工程的Resource,下面是具體替換的代碼。
public static void hookResources(Context base, Resources resources) { ? ?try { ? ? ? ? ? ?ReflectUtil.setField(base.getClass(), base, "mResources", resources); ? ? ? ? ? ?Object loadedApk = ReflectUtil.getPackageInfo(base); ? ? ? ? ? ?ReflectUtil.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了幾個地方,包括以下幾個hook點
替換了主工程context中LoadedApk的mResource對象
將新的Resource添加到主工程ActivityThread的mResourceManager中,并且根據Android版本做了不同處理
第三步:關聯resource和Activity
Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent); activity.setIntent(intent); //設置Activity的mResources屬性,Activity中訪問資源時都通過mResources ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources());
上述代碼是在Activity創建時被調用的(后面會介紹如何hook Activity的創建過程),在activity被構造出來后,需要替換其中的mResources為插件的Resource。由于獨立式時主工程的Resource不能訪問插件的資源,所以如果不做替換,會產生資源訪問錯誤。
做完以上工作后,則可以在插件的Activity中放心的使用setContentView,inflater等方法加載布局了。
資源沖突
合并式的資源處理方式,會引入資源沖突,原因在于不同插件中的資源id可能相同,所以解決方法就是使得不同的插件資源擁有不同的資源id。
資源id是由8位16進制數表示,表示為0xPPTTNNNN。PP段用來區分包空間,默認只區分了應用資源和系統資源,TT段為資源類型,NNNN段在同一個APK中從0000遞增。如下表所示
所以思路是修改資源ID的PP段,對于不同的插件使用不同的PP段,從而區分不同插件的資源。具體實現方式有兩種
修改aapt源碼,編譯期修改PP段。
修改resources.arsc文件,該文件列出了資源id到具體資源路徑的映射。
具體實現可以分別參考Atlas框架和Small框架。推薦第二種方式,不用入侵原有的編譯流程。
三、四大組件支持
Android開發中有一些特殊的類,是由系統創建的,并且由系統管理生命周期。如常用的四大組件,Activity,Service,BroadcastReceiver和ContentProvider。 僅僅構造出這些類的實例是沒用的,還需要管理組件的生命周期。其中以Activity最為復雜,不同框架采用的方法也不盡相同。下面以Activity為例詳細介紹插件化如何支持組件生命周期的管理。 大致分為兩種方式:
ProxyActivity代理
預埋StubActivity,hook系統啟動Activity的過程
3.1 ProxyActivity代理
ProxyActivity代理的方式最早是由dynamic-load-apk提出的,其思想很簡單,在主工程中放一個ProxyActivy,啟動插件中的Activity時會先啟動ProxyActivity,在ProxyActivity中創建插件Activity,并同步生命周期。下圖展示了啟動插件Activity的過程。
首先需要通過統一的入口(如圖中的PluginManager)啟動插件Activity,其內部會將啟動的插件Activity信息保存下來,并將intent替換為啟動ProxyActivity的intent。
ProxyActivity根據插件的信息拿到該插件的ClassLoader和Resource,通過反射創建PluginActivity并調用其onCreate方法。
PluginActivty調用的setContentView被重寫了,會去調用ProxyActivty的setContentView。由于ProxyActivity重寫了getResource返回的是插件的Resource,所以setContentView能夠訪問到插件中的資源。同樣findViewById也是調用ProxyActivity的。
ProxyActivity中的其他生命周期回調函數中調用相應PluginActivity的生命周期。
代理方式的關鍵總結起來有下面兩點:
ProxyActivity中需要重寫getResouces,getAssets,getClassLoader方法返回插件的相應對象。生命周期函數以及和用戶交互相關函數,如onResume,onStop,onBackPressedon,KeyUponWindow,FocusChanged等需要轉發給插件。
PluginActivity中所有調用context的相關的方法,如setContentView,getLayoutInflater,getSystemService等都需要調用ProxyActivity的相應方法。
該方式有幾個明顯缺點:
插件中的Activity必須繼承PluginActivity,開發侵入性強。
如果想支持Activity的singleTask,singleInstance等launchMode時,需要自己管理Activity棧,實現起來很繁瑣。
插件中需要小心處理Context,容易出錯。
如果想把之前的模塊改造成插件需要很多額外的工作。
該方式雖然能夠很好的實現啟動插件Activity的目的,但是由于開發式侵入性很強,dynamic-load-apk之后的插件化方案很少繼續使用該方式,而是通過hook系統啟動Activity的過程,讓啟動插件中的Activity像啟動主工程的Activity一樣簡單。
3.2 hook方式
在介紹hook方式之前,先用一張圖簡要的介紹下系統是如何啟動一個Activity的。
上圖列出的是啟動一個Activity的主要過程,具體步驟如下:
Activity1調用startActivity,實際會調用Instrumentation類的execStartActivity方法,Instrumentation是系統用來監控Activity運行的一個類,Activity的整個生命周期都有它的影子。
通過跨進程的binder調用,進入到ActivityManagerService中,其內部會處理Activity棧。之后又通過跨進程調用進入到Activity2所在的進程中。
ApplicationThread是一個binder對象,其運行在binder線程池中,內部包含一個H類,該類繼承于類Handler。ApplicationThread將啟動Activity2的信息通過H對象發送給主線程。
主線程拿到Activity2的信息后,調用Instrumentation類的newActivity方法,其內通過ClassLoader創建Activity2實例。
下面介紹如何通過hook的方式啟動插件中的Activity,需要解決以下兩個問題
插件中的Activity沒有在AndroidManifest中注冊,如何繞過檢測。
如何構造Activity實例,同步生命周期
解決方法有很多種,以VirtualAPK為例,核心思路如下:
先在Manifest中預埋StubActivity,啟動時hook上圖第1步,將Intent替換成StubActivity。
hook第10步,通過插件的ClassLoader反射創建插件Activity\
之后Activity的所有生命周期回調都會通知給插件Activity
下面具體分析整個過程涉及到的代碼:
替換系統Instrumentation
VirtualAPK在初始化時會調用hookInstrumentationAndHandler,該方法hook了系統的Instrumentaiton類,由上文可知該類和Activity的啟動息息相關。
private void hookInstrumentationAndHandler() { ? ?try { ? ? ? ? ? ?//獲取Instrumentation對象 ? ? ? ?Instrumentation baseInstrumentation = ReflectUtil.getInstrumentation(this.mContext); ? ? ? ? ? ?? ? ?//構造自定義的VAInstrumentation ? ? ? ?final VAInstrumentation instrumentation = new VAInstrumentation(this, baseInstrumentation); ? ? ? ? ?? ? ?? ? ? ?//設置ActivityThread的mInstrumentation和mCallBack ? ? ? ?Object activityThread = ReflectUtil.getActivityThread(this.mContext); ? ? ? ?ReflectUtil.setInstrumentation(activityThread, instrumentation); ? ? ? ?ReflectUtil.setHandlerCallback(this.mContext, instrumentation); ? ? ? ? ?this.mInstrumentation = instrumentation; ? ?} catch (Exception e) { ? ? ? ?e.printStackTrace(); ? ?} }
該段代碼將主線程中的Instrumentation對象替換成了自定義的VAInstrumentation類。在啟動和創建插件activity時,該類都會偷偷做一些手腳。
hook activity啟動過程
VAInstrumentation類重寫了execStartActivity方法,圖 3.2中的第一步。
public ActivityResult execStartActivity( ? ?//省略了無關參數 ? ?Intent intent) { //轉換隱式intent ? ?mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent); ? ?if (intent.getComponent() != null) { ? ? ? ?//替換intent中啟動Activity為StubActivity ? ? ? ?this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent); ? ?} ? ? ? ? ? ?//調用父類啟動Activity的方法} public void markIntentIfNeeded(Intent intent) { ? ?if (intent.getComponent() == null) { ? ? ? ? ? ?return; ? ?} ? ?String targetPackageName = intent.getComponent().getPackageName(); ? ?String targetClassName = intent.getComponent().getClassName(); ? ?// search map and return specific launchmode stub activity ? ?if (!targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) != null) { ? ? ? ?intent.putExtra(Constants.KEY_IS_PLUGIN, true); ? ? ? ?intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName); ? ? ? ?intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName); ? ? ? ?dispatchStubActivity(intent); ? ?} }
execStartActivity中會先去處理隱式intent,如果該隱式intent匹配到了插件中的Activity,將其轉換成顯式。之后通過markIntentIfNeeded將待啟動的的插件Activity替換成了預先在AndroidManifest中占坑的StubActivity,并將插件Activity的信息保存到該intent中。其中有個dispatchStubActivity函數,會根據Activity的launchMode選擇具體啟動哪個StubActivity。VirtualAPK為了支持Activity的launchMode在主工程的AndroidManifest中對于每種啟動模式的Activity都預埋了多個坑位。
hook Activity的創建過程
上一步欺騙了系統,讓系統以為自己啟動的是一個正常的Activity。當來到圖 3.2的第10步時,再將插件的Activity換回來。此時調用的是VAInstrumentation類的newActivity方法。
@Overridepublic Activity newActivity(ClassLoader cl, String className, Intent intent){ ? ?try { ? ? ? ?cl.loadClass(className); ? ?} catch (ClassNotFoundException e) { ? ? ? ?//通過LoadedPlugin可以獲取插件的ClassLoader和Resource ? ? ? ?LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent); ? ? ? ? ? ? ? ?//獲取插件的主Activity ? ? ? ?String targetClassName = PluginUtil.getTargetActivity(intent); ? ? ? ? ? ? ? ?if (targetClassName != null) { ? ? ? ?? ? ? ? ? ?//傳入插件的ClassLoader構造插件Activity ? ? ? ? ? ?Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent); ? ? ? ? ? ?activity.setIntent(intent); ? ? ? ? ? ? ? ? ? ?//設置插件的Resource,從而可以支持插件中資源的訪問 ? ? ? ? ? ?try { ? ? ? ? ? ? ? ?ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources()); ? ? ? ? ? ?} catch (Exception ignored) { ? ? ? ? ? ? ? ? ? ?? ? ? ? ? ? ? ?// ignored. ? ? ? ? ? ?} ? ? ? ? ? ?return activity; ? ? ? ?} ? ?} ? ?return mBase.newActivity(cl, className, intent); }
由于AndroidManifest中預埋的StubActivity并沒有具體的實現類,所以此時會發生ClassNotFoundException。之后在處理異常時取出插件Activity的信息,通過插件的ClassLoader反射構造插件的Activity。
一些額外操作
插件Activity構造出來后,為了能夠保證其正常運行還要做些額外的工作。VAInstrumentation類在圖3.2中的第11步中也做了一些處理。
@Overridepublic void callActivityOnCreate(Activity activity, Bundle icicle) { ? ?final Intent intent = activity.getIntent(); ? ? ? ?if (PluginUtil.isIntentFromPlugin(intent)) { ? ? ? ?Context base = activity.getBaseContext(); ? ? ? ? ? ? ? ?try { ? ? ? ? ? ?LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent); ? ? ? ? ? ?ReflectUtil.setField(base.getClass(), base, "mResources", plugin.getResources()); ? ? ? ? ? ?ReflectUtil.setField(ContextWrapper.class, activity, "mBase", plugin.getPluginContext()); ? ? ? ? ? ?ReflectUtil.setField(Activity.class, activity, "mApplication", plugin.getApplication()); ? ? ? ? ? ?ReflectUtil.setFieldNoException(ContextThemeWrapper.class, activity, "mBase", plugin.getPluginContext()); ? ? ? ? ? ? ? ?// set screenOrientation ? ? ? ? ? ?ActivityInfo activityInfo = plugin.getActivityInfo(PluginUtil.getComponent(intent)); ? ?? ? ? ? ? ?if (activityInfo.screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) { ? ? ? ? ? ? ? ?activity.setRequestedOrientation(activityInfo.screenOrientation); ? ? ? ? ? ?} ? ? ? ?} catch (Exception e) { ? ? ? ? ? ?e.printStackTrace(); ? ? ? ?} ? ?} ? ?mBase.callActivityOnCreate(activity, icicle); }
這段代碼主要是將Activity中的Resource,Context等對象替換成了插件的相應對象,保證插件Activity在調用涉及到Context的方法時能夠正確運行。
經過上述步驟后,便實現了插件Activity的啟動,并且該插件Activity中并不需要什么額外的處理,和常規的Activity一樣。那問題來了,之后的onResume,onStop等生命周期怎么辦呢?答案是所有和Activity相關的生命周期函數,系統都會調用插件中的Activity。原因在于AMS在處理Activity時,通過一個token表示具體Activity對象,而這個token正是和啟動Activity時創建的對象對應的,而這個Activity被我們替換成了插件中的Activity,所以之后AMS的所有調用都會傳給插件中的Activity。
小結
VirtualAPK通過替換了系統的Instrumentation,hook了Activity的啟動和創建,省去了手動管理插件Activity生命周期的繁瑣,讓插件Activity像正常的Activity一樣被系統管理,并且插件Activity在開發時和常規一樣,即能獨立運行又能作為插件被主工程調用。
其他插件框架在處理Activity時思想大都差不多,無非是這兩種方式之一或者兩者的結合。在hook時,不同的框架可能會選擇不同的hook點。如360的RePlugin框架選擇hook了系統的ClassLoader,即圖3.2中構造Activity2的ClassLoader,在判斷出待啟動的Activity是插件中的時,會調用插件的ClassLoader構造相應對象。另外RePlugin為了系統穩定性,選擇了盡量少的hook,因此它并沒有選擇hook系統的startActivity方法來替換intent,而是通過重寫Activity的startActivity,因此其插件Activity是需要繼承一個類似PluginActivity的基類的。不過RePlugin提供了一個Gradle插件將插件中的Activity的基類換成了PluginActivity,用戶在開發插件Activity時也是沒有感知的。
3.3 其他組件
四大組件中Activity的支持是最復雜的,其他組件的實現原理要簡單很多,簡要概括如下
Service:Service和Activity的差別在于,Activity的生命周期是由用戶交互決定的,而Service的生命周期是我們通過代碼主動調用的,且Service實例和manifest中注冊的是一一對應的。實現Service插件化的思路是通過在manifest中預埋StubService,hook系統startService等調用替換啟動的Service,之后在StubService中創建插件Service,并手動管理其生命周期。
BroadCastReceiver:解析插件的manifest,將靜態注冊的廣播轉為動態注冊。
ContentProvider:類似于Service的方式,對插件ContentProvider的所有調用都會通過一個在manifest中占坑的ContentProvider分發。
四、發展方向
通過對插件化技術的學習,可以看出目前插件化技術的兩個發展方向
結合組件化技術,成為一個中大型app的基礎框架
以Small和阿里的Atlas為代表,利用了插件化技術對復雜工程的模塊進行解耦,將app分成主工程和多個插件模塊。主工程在運行期間動態加載相應模塊的插件運行,并負責插件模塊的管理工作。各個插件可以獨立開發和運行,也可以依賴主工程或者其他插件。下面是基于Atlas的手淘app的框架圖
其中的獨立bundle即是一個插件,手淘中的首頁,詳情頁,掃碼,支付等都做成了單獨的bundle,并且首頁bundle還可以依賴于定位bundle。而主工程中則包含了各種基礎功能庫供各個bundle調用,并且包含了對bundle的安裝,運行,版本管理,安全校驗等運行期的管理工作。
組件化技術是利用gradle腳本實現的編譯期的功能解耦,而Atlas是利用插件化技術實現了一套運行期的功能解耦,所以其也號稱是動態組件化技術。
app沙盒系統,完全模擬app的運行環境
以VirtualAPP為代表,在應用層構建了一個虛擬的app運行環境,實現了免安裝運行apk,應用雙開等黑科技。另外作為應用開發者也需要注意我們的應用可能會運行在一個虛擬的環境下,對于支付,登錄等功能要特別注意其安全性。
最后用VirtualAPP的作者Lody的一句話結束本篇文章,相信插件化技術還會繼續發展壯大下去。
“插件化技術的成熟程度雖然在最近幾年呈上升趨勢,但是總體而言仍然處于初、中級階段。?
App沙盒技術的出現就是插件化發展的創新和第一階段的產物。在未來,我相信很多插件化技?
術會被更多的應用,如果插件化穩定到了一定的程度,甚至可以顛覆App開發的方式。”
參考?
1.Android插件化:從入門到放棄(http://www.infoq.com/cn/articles/android-plug-ins-from-entry-to-give-up)
2.Android apk動態加載機制的研究?
(http://blog.csdn.net/singwhatiwanna/article/details/22597587)
3.Android插件化原理解析系列文章?
(http://weishu.me/2016/01/28/understand-plugin-framework-overview/)
4.?深度 | 滴滴插件化方案 VirtualApk 源碼解析?
5.VirtualAPK資源加載原理解析?
(https://www.notion.so/VirtualAPK-1fce1a910c424937acde9528d2acd537)
http://itindex.net/detail/57916-android-%E6%8F%92%E4%BB%B6-%E6%8A%80%E6%9C%AF
總結
以上是生活随笔為你收集整理的《Android插件化技术——原理篇》的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: LBE平行空间的技术实现
- 下一篇: android sina oauth2.