Android插件化原理探究

Android插件化原理探究

一、簡介

android動(dòng)態(tài)加載插件機(jī)制一直以來就是探索的熱門領(lǐng)域,各種動(dòng)態(tài)加載框架層出不窮,動(dòng)態(tài)插件機(jī)制能有效解決一些線上bug進(jìn)而避免頻繁的版本發(fā)布。本文一不對當(dāng)前流行的框架進(jìn)行探討(如果有需要人家已經(jīng)開源),二不追求去實(shí)現(xiàn)這么一個(gè)完整的動(dòng)態(tài)加載框架(這一般都是大廠所為,耗時(shí)耗力,而且如果真有機(jī)會(huì)去實(shí)現(xiàn),熟知原理就會(huì)有方案可尋),只是總結(jié)下相關(guān)原理,這樣不僅對動(dòng)態(tài)加載有一定的認(rèn)知,而且對理解Android系統(tǒng)大有裨益。

二、熱修復(fù)與插件化

通常談到插件化的時(shí)候緊密相連的就事熱修復(fù)。這里對二者進(jìn)行一些小小的區(qū)分。熱修復(fù)一般是修復(fù)出現(xiàn)bug的類或者方法,該技術(shù)一般需要能做到替換原有類或者方法的目的;而插件化可以認(rèn)為比熱修復(fù)稍簡單些,該技術(shù)一般只需要加載一個(gè)獨(dú)立的插件化的apk即可,而不必進(jìn)行原有類的替換,能滿足動(dòng)態(tài)添加的功能,這對一個(gè)超級app的開發(fā)有很大意義。

如果從java層面入手,二者本質(zhì)上都會(huì)涉及到對類加載的操作、對資源加載的操作等。而本文就從java層面總結(jié)下插件化相關(guān)的一些技術(shù)原理,而不是native hook層次方面的原理(這個(gè)需要深入了解native層面下的東西)。

三、插件化要解決的問題

(1)代碼的加載。

宿主app如何加載插件apk?因?yàn)閏lass loader是全局唯一的,在宿主啟動(dòng)的時(shí)候就已經(jīng)知道該加載宿主的apk了,然而插件的apk如何加載?

(2)組件生命周期的管理

對于普通的類加載,我們很容易借助于class loader完成,但是對于android中的activity等組件來說,這還遠(yuǎn)遠(yuǎn)滿足不了需求,首先android拒絕加載沒有在manifest文件注冊的activity,而插件中apk的組件顯然不會(huì)在宿主manifest文件中注冊;其次類似于activity等組件的聲明周期怎么維持?

(3)資源的加載

如何進(jìn)行資源的加載?這里的資源是指插件apk中的資源。因?yàn)樗拗黠@然對插件中的資源無感知,當(dāng)我們應(yīng)用R.xx.id進(jìn)行資源查找時(shí),必然會(huì)報(bào)錯(cuò)。

四、問題解決原理

1、代碼的加載

熟悉java的一般都會(huì)知道代碼的加載最終是由ClassLoader進(jìn)行加載的,java中的classloader采用的是雙親委派模型,即首先會(huì)委托給父加載器進(jìn)行加載,父加載器加載不了再自己去加載。java類加載器的這個(gè)邏輯有助于我們實(shí)現(xiàn)對插件apk的加載。

在android中加載類的class loader是DexClassLoader,因此首先我們要做到如何能加載插件apk(因?yàn)椴寮pk可能存在任何地方,宿主ClassLoader顯然不會(huì)知道他在那里,加載就更無從談起了)。

(1)自定義ClassLoader

這個(gè)原理就是要替換掉宿主的ClassLoader,采用我們自己的ClassLoader,進(jìn)而實(shí)現(xiàn)加載我們插件apk的目的。這里想要替換一般會(huì)hook掉系統(tǒng)中的緩存的ClassLoader,進(jìn)而截?cái)噙@個(gè)過程。這樣我們自己的classloader就可以實(shí)現(xiàn)針對插件apk進(jìn)行加載。不過這種方法涉及到大量的對系統(tǒng)代碼的反射操作,因?yàn)橄胍獦?gòu)建出可用的classloader,必須要確保合理的入?yún)?。所以難度實(shí)現(xiàn)起來較大。對于剛剛提到的hook點(diǎn),這里可以給出一段代碼:

public?final?LoadedApk?getPackageInfo(String?packageName,?CompatibilityInfo?compatInfo,? int?flags,?int?userId)?{??

final?boolean?differentUser?=?(UserHandle.myUserId()?!=?userId);??

synchronized?(mResourcesManager)?{??

????????????WeakReference?ref;??

if?(differentUser)?{??

//?Caching?not?supported?across?users??

ref?=null;??

}else?if?((flags?&?Context.CONTEXT_INCLUDE_CODE)?!=?0)?{??

????????????????ref?=?mPackages.get(packageName);??

}else?{??

????????????????ref?=?mResourcePackages.get(packageName);??

????????????}??

LoadedApk?packageInfo?=?ref?!=null???ref.get()?:?null;??

//Slog.i(TAG,?"getPackageInfo?"?+?packageName?+?":?"?+?packageInfo);??

//if?(packageInfo?!=?null)?Slog.i(TAG,?"isUptoDate?"?+?packageInfo.mResDir??

//????????+?":?"?+?packageInfo.mResources.getAssets().isUpToDate());??

if?(packageInfo?!=?null?&&?(packageInfo.mResources?==?null??

????????????????????||?packageInfo.mResources.getAssets().isUpToDate()))?{??

if?(packageInfo.isSecurityViolation()??

&&?(flags&Context.CONTEXT_IGNORE_SECURITY)?==0)?{??

throw?new?SecurityException(??

"Requesting?code?from?"?+?packageName??

+"?to?be?run?in?process?"? ?+?mBoundApplication.processName??

+"/"?+?mBoundApplication.appInfo.uid);? }??

return?packageInfo;? ?}? }??

ApplicationInfo?ai?=null;??

try?{??

????????????ai?=?getPackageManager().getApplicationInfo(packageName,??

????????????????????PackageManager.GET_SHARED_LIBRARY_FILES,?userId);??

}catch?(RemoteException?e)?{??

//?Ignore??

????????}??

if?(ai?!=?null)?{??

return?getPackageInfo(ai,?compatInfo,?flags);? }??

return?null;? ?}??

上述代碼實(shí)質(zhì)上是獲取緩存的package info,返回的對象類型是LoadApk,LoadApk中包含了package信息,而package信息中就包含了classloader信息。具體可以自行查看。

(2)委托加載

這中方案的原理不在追求替換掉系統(tǒng)的classloader,而是選擇了一個(gè)相對妥協(xié)的方案,即依然運(yùn)行系統(tǒng)的classloader去進(jìn)行類加載,那么此時(shí)又如何做到加載我們插件apk的呢?

這個(gè)就又涉及到android系統(tǒng)類加載的過程了,上文提到android提供了DexClassLoader進(jìn)行類加載,實(shí)際上他繼承于BaseDexClassLoader,在其findClass(類加載器建議通過該方法查找class)中有一端下面代碼(源碼地址參見:http://androidxref.com/6.0.1_r10/xref/libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java):

Override??

protected?Class?findClass(String?name)?throws?ClassNotFoundException?{??

List?suppressedExceptions?=new?ArrayList();??

????????Class?c?=?pathList.findClass(name,?suppressedExceptions);??

if?(c?==?null)?{??

ClassNotFoundException?cnfe?=new?ClassNotFoundException("Didn't?find?class?\""?+?name?+?"\"?on?path:?"?+?pathList);??

for?(Throwable?t?:?suppressedExceptions)?{??

????????????????cnfe.addSuppressed(t);??

????????????}??

throw?cnfe;??

????????}??

return?c;??

????}??

從代碼中可以看出,這里主要通過pathList完成了類的查找。pathList類型是DexPathList,查找代碼(http://androidxref.com/6.0.1_r10/xref/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java),實(shí)現(xiàn)如下:

public?Class?findClass(String?name,?List?suppressed)?{??

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;? ? ? }??

從代碼中可以看出,這里findClass會(huì)遍歷dexElements數(shù)組完成最終的加載,對!這就是這種方案的突破口!我們只需要將我們插件的dex插入到這個(gè)數(shù)組中就可以實(shí)現(xiàn)對我們插件apk的加載(通過反射去構(gòu)建dexElements,將宿主和插件apk的dex一起copy進(jìn)去即可)!

至此,上面兩種方式可以實(shí)現(xiàn)對插件apk的加載。

(3)生命周期的管理

對于普通的類加載,我們很容易借助于class loader完成,但是對于android中的activity等組件來說,這還遠(yuǎn)遠(yuǎn)滿足不了需求,因?yàn)閍ndroid拒絕加載沒有在manifest文件注冊的activity,而插件中apk的組件顯然不會(huì)再宿主manifest文件中注冊,因此這是首要面臨的問題。下面給出兩種流行的方案。

4、生命周期的管理

對于生命周期的管理,目前我得知有兩種方案

(1)hook機(jī)制。

類似于解決classloader加載class類一樣,hook住關(guān)鍵點(diǎn),進(jìn)而達(dá)到欺騙Framework的目的。這里首先會(huì)在manifest中注冊代理activity(此處暫時(shí)稱為ProxyActivity),在跳轉(zhuǎn)的時(shí)候跳轉(zhuǎn)的指向的是ProxyActivity,而在真正launch的時(shí)候launch的是真正的目標(biāo)Actitity(此處暫時(shí)稱為TargetActivity)。話是這么說,但是如何做到無縫替換?其實(shí)這種方案就是從系統(tǒng)源碼入手,在startActivity開始,到進(jìn)入system_server進(jìn)程ActivityManagerService之前保持目標(biāo)activity指向ProxyActivity,以解決activity在manifet中未注冊問題;在從ActivityManagerService進(jìn)程回到當(dāng)前啟動(dòng)進(jìn)程時(shí)用TargetActivity替換掉ProxyActivity已達(dá)到欺騙AMS的目的,具體可查閱相關(guān)資料。

這種做法可以使activity有完整的生命周期,因?yàn)槎际怯蠥MS再進(jìn)行管理。

(2)代理機(jī)制

這種做法在ProxyActivity的注冊上與上述方法一致,但在聲明周期的管理上則是有動(dòng)態(tài)加載框架實(shí)現(xiàn)了一系列代理目標(biāo)組件的生命周期接口來完成的。當(dāng)啟動(dòng)目標(biāo)Activity的時(shí)候,實(shí)際上是有代理Activity完全代理了其聲明周期。

3、資源的加載實(shí)現(xiàn)原理(取自DL框架對于資源加載原理描述)

加載的方法是通過反射,通過調(diào)用AssetManager中的addAssetPath方法,我們可以將一個(gè)apk中的資源加載到Resources中,由于addAssetPath是隱藏api我們無法直接調(diào)用,所以只能通過反射,其入?yún)鬟f的路徑可以是zip文件也可以是一個(gè)資源目錄,而apk就是一個(gè)zip,所以直接將apk的路徑傳給它,資源就加載到AssetManager中了,然后再通過AssetManager來創(chuàng)建一個(gè)新的Resources對象,這個(gè)對象就是我們可以使用的apk中的資源了,這樣我們的問題就解決了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容