寫在前面
==如果時(shí)間有限可以直接跳到最下面的 核心問題==
* 插件化現(xiàn)狀
插件化目前的處境肯定是大不如前午笛,由于android系統(tǒng)逐步完善收緊各種黑科技很難再爆發(fā)搜骡,各個(gè)插件化逐步從爆發(fā)大量黑科技到追求穩(wěn)定性,再加之小程序的產(chǎn)生。讓大廠很多合作直接使用小程序察绷,而不再使用插件化.
不過對(duì)于中小公司沒有小程序能力的炮叶,插件化不失為一種比較好的動(dòng)態(tài)化方案。
*為什么閱讀RePlugin的源碼
對(duì)比了VirtualApk和RePlugin贷揽,選擇閱讀RePlugin的源碼是因?yàn)樘男Γ萊ePlugin 有更大的概率還在維護(hù)中,而且wiki中有已經(jīng)整理好的原理性的文章禽绪,方便閱讀
為什么不選擇閱讀VirtualApp的源碼腐晾?雖然他是第三代插件化框架,但是目前收費(fèi)的丐一,閱讀源碼還是希望使用在項(xiàng)目中藻糖。有時(shí)間可以閱讀以下VirtualApp的 免費(fèi)版本
插件化無疑就是在解決,如何讓宿主app使用到 插件的 類库车,資源巨柒。這樣的問題..(核心問題部分中有回答)我們本著這個(gè)核心思想去閱讀源碼應(yīng)該會(huì)有更好的效果
* RePlugin解決了什么問題?
RePlugin解決的是各個(gè)功能模塊能獨(dú)立升級(jí)柠衍,又能需要和宿主洋满、插件之間有一定交互和耦合(所以開發(fā)需要按照一定的規(guī)則)。有別與類似 VirtualApk 這種雙開類型的插件化框架(可以將任一APP作為插件)
感嘆
RePlugin應(yīng)該是我現(xiàn)在閱讀的除了Android源碼之外最復(fù)雜的源碼了珍坊,不過確實(shí)寫得挺好的牺勾,有很多地方值得學(xué)習(xí),尤其是在程序的健壯性和兼容性方面阵漏。
版本
v2.3.3
參考
- 配合RePluginDebug效果更棒偶
這個(gè)是我自己閱讀是使用的工程驻民,包含了很多注釋。 -
PluginDemo
這個(gè)是我閱讀完RePlugin以后 字節(jié)寫的插件化demo履怯,只有最核心的功能實(shí)現(xiàn)回还,有助于理解原理 - 原理解析中相關(guān)內(nèi)容
- 唯一插件化RePlugin源碼及原理深度剖析 系列文章
- 《RePlugin全面解析 系列文章》
-
RePlugin核心類圖
綠色的部分是replugin-plugin-lib,藍(lán)色和紅色部分都是是replugin-host-lib包中的類叹洲,但是藍(lán)色部分是運(yùn)行在UI進(jìn)程中柠硕,而紅色部分是運(yùn)行在Persistent進(jìn)程中。 - RePlugin實(shí)現(xiàn)host和plugin方式有兩種參考 合作插件
- 貴方集成host方式运提,合作方集成plugin
- 合作方提供aar蝗柔,貴方集成host和將aar打到plugin中
- RePlugin 的局限(2017.7.18年)
RePlugin原理簡介
Replugin的整體框架使用了Binder機(jī)制來進(jìn)行宿主和多插件之間交互通信和數(shù)據(jù)共享闻葵,這里如果了解android四大組件的運(yùn)行流程的話,看完了Replugin的源碼后會(huì)感覺非常像簡易ServiceManager和AMS的結(jié)構(gòu)癣丧。
Replugin默認(rèn)會(huì)使用一個(gè)常駐進(jìn)程作為Server端槽畔,其他插件進(jìn)程和宿主進(jìn)程全部屬于Client端。當(dāng)然如果修改不使用常駐進(jìn)程坎缭,那么宿主的主進(jìn)程將作為插件管理進(jìn)程,而不管是使用宿主進(jìn)程還是使用默認(rèn)的常駐進(jìn)程,Server端其實(shí)就是創(chuàng)建了一個(gè)運(yùn)行在該進(jìn)程中的Provider竟痰,通過Provider的query方法返回了Binder對(duì)象來實(shí)現(xiàn)多進(jìn)程直接的的溝通和數(shù)據(jù)共享,或者說是插件之間和宿主之間溝通和數(shù)據(jù)共享掏呼,插件的安裝坏快,卸載,更新憎夷,狀態(tài)判斷等全部都在這個(gè)Server端完成莽鸿。
其實(shí)Replugin還是使用的占坑的方式來實(shí)現(xiàn)的插件化,replugin-host-gradle這個(gè)gradle插件會(huì)在編譯的時(shí)候自動(dòng)將坑位信息生成在主工程的AndroidManifest.xml中拾给,Replugin的唯一hook點(diǎn)是hook了系統(tǒng)了ClassLoader祥得,當(dāng)啟動(dòng)四大組件的時(shí)候會(huì)通過Clent端發(fā)起遠(yuǎn)程調(diào)用去Server做一系列的事情,例如檢測(cè)插件是否安裝蒋得,安裝插件级及,提取優(yōu)化dex文件,分配坑位额衙,啟動(dòng)坑位饮焦,這樣可以欺騙系統(tǒng)達(dá)到不在AndroidManifest.xml注冊(cè)的效果,最后在Clent端加載要被啟動(dòng)的四大組件窍侧,因?yàn)橐呀?jīng)hook了系統(tǒng)的ClassLoader县踢,所以可以對(duì)系統(tǒng)的類加載過程進(jìn)行攔截,將之前分配的坑位信息替換成真正要啟動(dòng)的組件信息并使用與之對(duì)應(yīng)的ClassLoader來進(jìn)行類的加載伟件,從而啟動(dòng)未在AndroidManifest.xml中注冊(cè)的組件硼啤。
各個(gè)工程模塊職責(zé)簡要解析
- replugin-host-gradle :
主程序使用的Gradle插件,主要職責(zé)是在我們的主程序打包的過程中(編譯的過程中)動(dòng)態(tài)的修改AndroidManifest.xml的信息斧账,動(dòng)態(tài)的生成占位各種Activity谴返、provider和service的聲明。 - replugin-host-library :
這個(gè)庫是要由主程序依賴的其骄,也是Replugin的核心亏镰,它的主要職責(zé)是初始化Replugin的整體框架,整體框架使用了Binder機(jī)制來實(shí)現(xiàn)多進(jìn)程直接的的溝通和數(shù)據(jù)共享拯爽,或者說是插件之間和宿主之間溝通和數(shù)據(jù)共享,hook住ClassLoader钧忽,加載插件毯炮、啟動(dòng)插件逼肯、多插件的管理全部都與這個(gè)庫輔助 - replugin-plugin-gradle :
這個(gè)是插件工程使用的Gradle的插件,這個(gè)庫使用了Transfrom API和Javassist實(shí)現(xiàn)了編譯期間動(dòng)態(tài)的修改字節(jié)碼文件桃煎,主要是替換插件工程中的Activity的繼承全部替換成Replugin庫中定義的XXXActivity篮幢。動(dòng)態(tài)的將插件apk中調(diào)用LocalBroadcastManager的地方修改為Replugin中的PluginLocalBroadcastManager調(diào)用,動(dòng)態(tài)修改ContentResolver和ContentProviderClient的調(diào)用修改成Replugin調(diào)用为迈,動(dòng)態(tài)的修改插件工程中所有調(diào)用Resource.getIdentifier方法的地方三椿,將第三參數(shù)修改為插件工程的包名 - replugin-plugin-library :
這個(gè)庫是由插件工程依賴的,這個(gè)庫的主要目的是通過反射的方式來使用主程序中接口和功能葫辐,這個(gè)庫在出程序加載加載插件apk后會(huì)進(jìn)行初始化搜锰。
各模塊解析
replugin-host-gradle
主要職責(zé)
- 創(chuàng)建 rpShowPlugin... Task 用于將 插件信息寫入到 plugins-builtin.json 文件中
- 創(chuàng)建 rpGenerateHostConfig Task用于生產(chǎn) RePluginHostConfig.java 文件,文件內(nèi)容基本就是 用戶配置的信息(坑位信息耿战,進(jìn)程名稱等)
- 修改manifast.xml 植入占坑信息
replugin-host-library
各類職責(zé)
運(yùn)行于常駐進(jìn)程 (常駐進(jìn)程主要用于插件管理和Service(四大組件)維護(hù))
- PmHostSvc(binder對(duì)象):這個(gè)類可以理解成是我們的Server端蛋叼,它直接或間接參與了Server端要做的所有事情
- PluginServiceServer(binder對(duì)象):主要負(fù)責(zé)了對(duì)Service的提供和調(diào)度工作,例如startService剂陡、stopService狈涮、bindService、unbindService全部都由這個(gè)類管理
- PluginManagerServer(binder對(duì)象):掌管了所有對(duì)插件的的操作鸭栖,例如插件的安裝歌馍、加載、卸載晕鹊、更新等等
- Builder.PxAll : 緩存所有(各種類型)插件
運(yùn)行于ui進(jìn)程
- RePlugin:RePlugin的對(duì)外入口類 松却,宿主App可直接調(diào)用此類中的方法,來使用插件化的幾乎全部的邏輯捏题。
- IPC:用于“進(jìn)程間通信”的類玻褪。插件和宿主可使用此類來做一些跨進(jìn)程發(fā)送廣播、判斷進(jìn)程等工作公荧。
- PMF:框架和主程序接口代碼
- PmBase:具有很多重要的功能,例如:分配坑位带射、初始化插件信息、Clent端連接Server端循狰、加載插件窟社、更新插件、刪除插件绪钥、等等
- PluginProcessPer:它是一個(gè)Binder對(duì)象灿里,它代表了“當(dāng)前Clent端”,使用它來和Server端進(jìn)行通信
- LaunchModeStates:存儲(chǔ) LaunchMode + Theme -> 此種組合下的 ActivityState 狀態(tài)集合
- ActivityState:坑位與真實(shí)組件之間的對(duì)應(yīng)關(guān)系
- PluginContainers:用來管理Activity坑位信息的容器程腹,初始化了多種不同啟動(dòng)模式和樣式Activity的坑位信息匣吊。
- PluginCommImpl:負(fù)責(zé)宿主與插件、插件間的互通,很多對(duì)提供方法都經(jīng)過這里中轉(zhuǎn)或者最終調(diào)到這里
- PluginLibraryInternalProxy:Replugin框架中內(nèi)部邏輯使用的很多方法都在這里色鸳,包括插件中通過“反射”調(diào)用的內(nèi)部邏輯如PluginActivity類的調(diào)用社痛、Factory2等
- RePluginClassLoader:用于替代宿主原有PathClassLoader的工作
- PluginDexClassLoader:個(gè)用來加載插件apk的類
- PluginProcessMain:進(jìn)程管理類
- IPluginManagerServer(aidl文件):插件管理器。用來控制插件的安裝命雀、卸載蒜哀、獲取等。運(yùn)行在常駐進(jìn)程中
- IPluginHost(aidl文件):涉及到插件交互吏砂、運(yùn)行機(jī)制有關(guān)的管理器
- StubProcessManager:坑位進(jìn)程管理
- PluginManagerProxy:用于各進(jìn)程(包括常駐自己)緩存 PluginManagerServer 的Binder實(shí)現(xiàn)
- PluginContext:插件要用的 Context
- PluginApplicationClient: 一種能處理【插件】的Application的類
- RePluginInternal:主要功能是緩存了 Context(宿主Application) 對(duì)象撵儿,并對(duì)外提供
- PluginInfo:用來描述插件,通過解析json生成
- PluginProviderStub:用于客戶端進(jìn)程通過 ContentProvider 獲取常駐進(jìn)程 binder對(duì)象等操作
replugin-plugin-gradle
主要職責(zé)
- 創(chuàng)建調(diào)試用的各個(gè)task
- 強(qiáng)制停止宿主程序: rpForceStopHostApp
- 安裝插件到宿主并運(yùn)行(常用任務(wù)): rpInstallAndRunPluginDebug或rpInstallAndRunPluginRelease等
- 僅僅安裝插件到宿主: rpInstallPluginDebug或rpInstallPluginRelease等
- rpRestartHostApp
重啟宿主程序 - 僅僅運(yùn)行插件狐血,如果插件前面沒安裝淀歇,則執(zhí)行不成功:rpRunPluginDebug或rpRunPluginRelease等
- 啟動(dòng)宿主程序:rpStartHostApp
- 僅僅卸載插件,如果完全卸載氛雪,還需要執(zhí)行rpRestartHostApp任務(wù): rpUninstallPluginDebug或rpUninstallPluginRelease
- 使用了Transfrom API和Javassist實(shí)現(xiàn)了編譯期間動(dòng)態(tài)的修改字節(jié)碼文件房匆,主要是
- 替換插件工程中的Activity的繼承全部替換成Replugin庫中定義的XXXActivity(如PluginActivity)。
- 動(dòng)態(tài)的將插件apk中調(diào)用LocalBroadcastManager的地方修改為Replugin中的PluginLocalBroadcastManager調(diào)用报亩,(被修改的包含一些系統(tǒng)類 比如LocalBroadcastManager的sendBroadcastSync 就被替換了浴鸿。)
- 動(dòng)態(tài)修改ContentResolver和ContentProviderClient的調(diào)用修改成Replugin 自定義的調(diào)用調(diào)用,(被修改的包含一些系統(tǒng)類)
- 動(dòng)態(tài)的修改插件工程中所有調(diào)用Resource.getIdentifier方法的地方弦追,將第三個(gè)參數(shù)修改為插件工程的包名(被修改的包含一些系統(tǒng)類)
replugin-plugin-library
各類職責(zé)
- Entry:宿主框架最先調(diào)用的類 用于初始化框架和環(huán)境
- RePluginServiceManager:插件內(nèi)部向外提供服務(wù)的管理實(shí)現(xiàn)類
大體工作流程
- 宿主APP啟動(dòng)時(shí)加載插件(解析插件信息但是不適用)岳链,和緩存預(yù)埋坑位
- 在使用插件時(shí) 選擇合適坑位
閱讀要點(diǎn)
標(biāo)識(shí)解讀
- N1 : UI 進(jìn)程標(biāo)識(shí)
- P{n} : 自定義進(jìn)程標(biāo)識(shí)
- NR : launchMode為 Standard
- STP: launchMode為 LAUNCH_SINGLE_TOP
- ST: launchMode為 LAUNCH_SINGLE_TASK
- SI:launchMode為 LAUNCH_SINGLE_INSTANCE
- NTS :表示坑的 theme 為不透明
- TS:表示坑的 theme 為透明
- p_n插件:
- 純APP插件:
內(nèi)部存儲(chǔ)中各個(gè)文件夾的含義
- app_plugins_v3_libs:內(nèi)置插件等的 so文件存放目錄
- app_p_c:存放可以覆蓋更新的插件 so文件
- app_p_n:純"APK"插件的 so文件存放目錄?
閱讀時(shí)注意的點(diǎn)
- 每一個(gè)插件(Plugin)都由一個(gè)Loader劲件,每個(gè) Loader都由一個(gè) ComponentList
調(diào)用鏈
host初始化
- RePluginApplication.attachBaseContext
- RePlugin.App.attachBaseContext
- IPC.init() : 確認(rèn)常駐進(jìn)程名 和 當(dāng)前進(jìn)程是那種進(jìn)程(主進(jìn)程or常駐進(jìn)程or其他)
- PMF.init:
- PluginManager.init
- 初始化主線程handler
- 通過當(dāng)前進(jìn)程的名字 獲取 進(jìn)程對(duì)應(yīng)的 int值
- PmBase.<init> : (所有進(jìn)程都會(huì)調(diào)用)
- PluginProcessPer.<init> :
- PluginServiceServer.<init>
- PluginContainers.init() : 初始化坑位
- PluginCommImpl.<init>
- PluginLibraryInternalProxy.<init>
- PluginProcessPer.<init> :
- PmBase.init() : 判斷是否使用常駐進(jìn)程作為服務(wù)進(jìn)程掸哑,并對(duì)服務(wù)進(jìn)程和客戶進(jìn)程分別初始化
- PmBase.initForServer(常駐進(jìn)程中的操作) : 初始化服務(wù)進(jìn)程,最主要的操作就是 將所有插件信息緩存到 PmBase.mPlugins 數(shù)組中
- Builder.builder : 整理插件并緩存到 PxAll中
- PmBase.refreshPluginMap : 將插件信息全部緩存到 mPlugins 中
- PluginManagerProxy.load : 加載純 APP插件零远?
- PmBase.refreshPluginMap : 更新 mPlugins 中信息
- PmBase.initForClient(客戶端進(jìn)程中的操作):1. 鏈接常駐進(jìn)程苗分,2. 獲取插件信息
- PluginProcessMain.connectToHostSvc(): 連接常駐進(jìn)程(初始化用于通信的binder代理對(duì)象)
- PluginProviderStub.proxyFetchHostBinder : 獲取常駐進(jìn)程b PmHostSvc inder 對(duì)象
- IPluginHost.Stub.asInterface(binder) : 獲取PmHostSvc inder 對(duì)象的代理對(duì)象
- PluginManagerProxy.connectToServer : 初始化 PluginManagerProxy.sRemote 對(duì)象用于和常駐進(jìn)程通信
- PluginManagerProxy.syncRunningPlugins() : 和常駐進(jìn)程同步插件運(yùn)行列表
- PmBase.attach : 注冊(cè)該進(jìn)程信息到“插件管理進(jìn)程”中?
- PmBase.refreshPluginsFromHostSvc : 從常駐進(jìn)程獲取插件列表,將插件信息全部緩存到 mPlugins 中
- PluginProcessMain.connectToHostSvc(): 連接常駐進(jìn)程(初始化用于通信的binder代理對(duì)象)
- PluginTable.initPlugins : 創(chuàng)建一份 最新快照到 PluginTable.PLUGINS
- PmBase.initForServer(常駐進(jìn)程中的操作) : 初始化服務(wù)進(jìn)程,最主要的操作就是 將所有插件信息緩存到 PmBase.mPlugins 數(shù)組中
- PatchClassLoaderUtils.patch : hook App的classLoader 為 RePluginClassLoader
- PluginManager.init
- PMF.callAttach()
- PmBase.callAttach()
- Plugin.load(); 加載并啟動(dòng)插件
- Plugin.loadLocked() 加載插件
- Plugin.doLoad(): 加載插件信息、資源牵辣、Dex摔癣,并運(yùn)行Entry類
- Loader.loadDex():
- PackageManager.getPackageArchiveInfo : 獲取插件的 PackageInfo
- mPackageInfo.applicationInfo.sourceDir : 設(shè)置插件的路徑,這個(gè)地址后面再獲取插件的Resources對(duì)象時(shí)會(huì)用到(設(shè)置之前是空的)
- mPackageInfo.applicationInfo.publicSourceDir : 同上
- mPackageInfo.applicationInfo.nativeLibraryDir :設(shè)置 so文件存放 路徑 纬向,插件加載so時(shí)會(huì)使用
- pm.getResourcesForApplication : 創(chuàng)建插件使用的Resources對(duì)象
- RePlugin.getConfig().getCallbacks().createPluginClassLoader : 創(chuàng)建插件的ClassLoader
- new PluginContext : 創(chuàng)建插件使用的 Context (PluginContext)择浊,
- Loader.loadDex():
- Plugin.loadEntryLocked(): 會(huì)反射加載插件中的Entry類以初始化插件框架和環(huán)境
- Plugin.doLoad(): 加載插件信息、資源牵辣、Dex摔癣,并運(yùn)行Entry類
- Plugin.callApp(): 啟動(dòng)并初始化插件Application,
- Plugin.callAppLocked :
- PluginApplicationClient.getOrCreate : 創(chuàng)建插件的Application對(duì)象
- PluginApplicationClient.<init> :
- PluginApplicationClient.initCustom : 創(chuàng)建插件的Application對(duì)象
- PluginApplicationClient.callAttachBaseContext : 將插件使用的Context 通過Application傳給插件 (PluginContext)
- PluginApplicationClient.getOrCreate : 創(chuàng)建插件的Application對(duì)象
- Plugin.callAppLocked :
- Plugin.loadLocked() 加載插件
- Plugin.load(); 加載并啟動(dòng)插件
- PmBase.callAttach()
- RePlugin.App.attachBaseContext
- RePluginApplication.onCreate
- RePlugin.App.onCreate()
- PMF.callAppCreate
- PmBase.callAppCreate
- 常駐進(jìn)程:獲取cookie
- 其他進(jìn)程注冊(cè) 安裝插件和卸載插件的廣播
- PluginInfoUpdater.register():非常駐進(jìn)程注冊(cè)監(jiān)聽PluginInfo變化的廣播以接受來自常駐進(jìn)程的更新
- PmBase.callAppCreate
- PMF.callAppCreate
- RePlugin.App.onCreate()
宿主啟動(dòng)插件中某Activity流程
- RePlugin.startActivity
- Factory.startActivityWithNoInjectCN
- PluginCommImpl.startActivity
- PluginLibraryInternalProxy.startActivity(5個(gè)參數(shù))
- PluginCommImpl.loadPluginActivity : 找到坑位activity 并封裝為 ComponentName 再返回,這個(gè)過程中還會(huì)給Intent塞一下參數(shù)逾条,比如 插件名稱
- MP.startPluginProcess : 啟動(dòng)目標(biāo)進(jìn)程 并獲取PluginProcessPer的binder 代理對(duì)象 用于通信
- client.allocActivityContainer : 遠(yuǎn)程分配坑位并返回
- context.startActivity : 打開坑位Activity琢岩,這個(gè)時(shí)候App就要調(diào)用classLoader加載坑位Activity了,但是App的ClassLoader被我們hook成為了 RePluginClassLoader ,所以也就是使用 RePluginClassLoader 來加載 坑位activity师脂,下面我么那就繼續(xù)看這個(gè)流程
- RePluginClassLoader. loadClass : 記載類
- PMF.loadClass :
- PmBase.loadClass :
- PluginProcessPer.resolveActivityClass :
- PluginDexClassLoader.loadClass : 使用插件classLoader 加載類
- PluginProcessPer.resolveActivityClass :
- PmBase.loadClass :
- PMF.loadClass :
- PluginCommImpl.loadPluginActivity : 找到坑位activity 并封裝為 ComponentName 再返回,這個(gè)過程中還會(huì)給Intent塞一下參數(shù)逾条,比如 插件名稱
- PluginLibraryInternalProxy.startActivity(5個(gè)參數(shù))
- PluginCommImpl.startActivity
- Factory.startActivityWithNoInjectCN
==這樣就達(dá)成了偷梁換柱担孔,系統(tǒng)以為我們加載的是 坑位類江锨,但其實(shí)加載的是 插件目標(biāo)類。而且系統(tǒng)也會(huì)乖乖的替我們 管理 插件目標(biāo)類的 生命周期攒磨。太陰了....==
插件中啟動(dòng)activity
1. 插件中直接或者間接(使用activity中使用view.getContext)通過Activity.startActivity打開宿主Activity
因?yàn)樵诰幾g器 插件中Activity的父類都被改變?yōu)槔^承自 PluginActivity
等Replugin 提供的Activity泳桦,所以他們的 startActivity 都會(huì)以 PluginActivity等的 startActivity為起點(diǎn)
- PluginActivity.PluginActivity
- RePluginInternal.startActivity
- ProxyRePluginInternalVar.startActivity.call 汤徽;反射調(diào)用 宿主工程中的 com.qihoo360.i.Factory2.startActivity 方法
- PluginLibraryInternalProxy.startActivity(2個(gè)參數(shù)) :
- PluginLibraryInternalProxy.fetchPluginByPitActivity : 獲取插件名
- Factory.startActivityWithNoInjectCN :
- PluginCommImpl.startActivity:
- PluginLibraryInternalProxy.startActivity(5個(gè)參數(shù))
- PluginCommImpl.loadPluginActivity:執(zhí)行這個(gè)方法時(shí) 因?yàn)闆]有在插件中肯定找不到宿主的Activity 所以會(huì)直接返回false娩缰,然后一層層退出到 RePluginInternal 中
- PluginLibraryInternalProxy.startActivity(5個(gè)參數(shù))
- PluginCommImpl.startActivity:
- PluginLibraryInternalProxy.startActivity(2個(gè)參數(shù)) :
- ProxyRePluginInternalVar.startActivity.call 汤徽;反射調(diào)用 宿主工程中的 com.qihoo360.i.Factory2.startActivity 方法
- super.startActivity : 正常啟動(dòng)activity
- RePluginInternal.startActivity
2. 插件中使用Application的context打開Activity (==要走兩次PluginContext.startActivity==)
因?yàn)椴寮械腃ontext是在Plugin.callApp()
過程中傳遞過去的PluginContext,所以,這個(gè)流程會(huì)以PluginContext.startActivity(Intent intent)
為起點(diǎn)
- PluginContext.startActivity(Intent intent): 第一次 是替換intent中要打開的Activity為 坑位activity
- Factory2.startActivity(Context context, Intent intent) : 這次返回true
- PluginLibraryInternalProxy.startActivity(2個(gè)參數(shù)) :
- PluginLibraryInternalProxy.fetchPluginByPitActivity : 獲取插件名
- Factory.startActivityWithNoInjectCN :
- PluginCommImpl.startActivity:
- PluginLibraryInternalProxy.startActivity(5個(gè)參數(shù)) : 替換目標(biāo)為坑位Activity
- context.startActivity : 這里又調(diào)用了一次Context.startActivity,所以又回到了 PluginContext.startActivity中
- PluginLibraryInternalProxy.startActivity(5個(gè)參數(shù)) : 替換目標(biāo)為坑位Activity
- PluginCommImpl.startActivity:
- Factory.startActivityWithNoInjectCN :
- Factory2.startActivity(Context context, Intent intent) : 這次返回true
- PluginContext.startActivity(Intent intent): 第二次是打開坑位activity谒府,欺騙系統(tǒng)打開 目標(biāo)activity
- Factory2.startActivity : 這次返回false
- super.startActivity(intent) : 真正的打開activity
可以看到 插件中啟動(dòng)activity最終都走到了 PluginLibraryInternalProxy.startActivity(5個(gè)參數(shù))
這個(gè)方法
3. 插件中正常(走的是Activity的startActivity)打開插件中的Activity (==要走兩次Activity.startActivity==)
- PluginActivity.startActivity : 第一次
- Factory2.startActivity(Activity activity, Intent intent) : 反射調(diào)用 這次返回true
- PluginLibraryInternalProxy.startActivity(2個(gè)參數(shù)) :
- PluginLibraryInternalProxy.fetchPluginByPitActivity : 獲取插件名
- Factory.startActivityWithNoInjectCN :
- PluginCommImpl.startActivity:
- PluginLibraryInternalProxy.startActivity(5個(gè)參數(shù)): 替換目標(biāo)為坑位Activity
- context.startActivity : 這里的 context其實(shí)是 Activity
- PluginLibraryInternalProxy.startActivity(5個(gè)參數(shù)): 替換目標(biāo)為坑位Activity
- PluginCommImpl.startActivity:
- Factory.startActivityWithNoInjectCN :
- Factory2.startActivity(Activity activity, Intent intent) : 反射調(diào)用 這次返回true
- PluginActivity.startActivity : 第二次
- Factory2.startActivity(Activity activity, Intent intent) : 反射調(diào)用 這次返回false
- super.startActivity(intent) : 真正的打開activity
- Factory2.startActivity(Activity activity, Intent intent) : 反射調(diào)用 這次返回false
4. 插件中打開宿主中Activity
因?yàn)椴寮腸lassLoader 如果找不到類就會(huì)去 宿主中找拼坎,而且 宿主的Activity也已經(jīng)注冊(cè)了,所以直接打開就行
插件activity(繼承自PluginActivity) onCreate 流程
插件在編譯器會(huì)將自己的父類替換為RePlugin內(nèi)容提供的類如PluginActivity
- RePluginInternal.handleActivityCreateBefore : 對(duì)FragmentActivity做特殊處理
- super.onCreate() : PluginActivity 父類的 onCreate
- RePluginInternal.handleActivityCreate : 填充一下必要的東西完疫,比如 lable等泰鸡?
學(xué)到的
1. 如何避免資源id沖突
答:不同的插件設(shè)置不同的packageId(==范圍0x02 - 0x7e,0x01是系統(tǒng)的壳鹤,0x7f是宿主APP的==)盛龄,進(jìn)行區(qū)分
2. hook時(shí)機(jī)完美
感覺hook classLoader的時(shí)機(jī)非常完美,是在Application的attachBaseContext中進(jìn)行hook的 芳誓,這個(gè)時(shí)候是 Appcation剛創(chuàng)建完畢余舶,他的上一步就是創(chuàng)建ContextImpl并保存LoadedApk。感覺非常及時(shí)(锹淌,不過好像也不用這么早只要下個(gè)apk中的類是用自定義classLoader加載的就行匿值?)
3. RePlugin支持插件使用宿主的類
RePlugin 是每一個(gè)Plugin都會(huì)有一個(gè)獨(dú)立的ClassLoader(PluginDexClassLoader),會(huì)優(yōu)先是用自己的classLoader,如果自己找不到了才回去通過父類查找,這樣就支持在不同插件中使用路徑和名字完全相同的類
4. gradle plugin 寫得確實(shí)很優(yōu)雅赂摆,很多之前未見過的寫法挟憔,及gradle 版本兼容,值得學(xué)習(xí)
5. 可以通過gradle task 執(zhí)行adb命令 烟号,然后進(jìn)行一些操作
6. handler.postAtFrontOfQueue 這個(gè) api的意思是 發(fā)送一個(gè)message 而且放到隊(duì)列的最前面
7. 通過反射刪除一個(gè)成員的 finel
修飾符 绊谭,真的是厲害啊
/**
* 刪除final修飾符
* @param field
*/
public static void removeFieldFinalModifier(final Field field) {
// From Apache: FieldUtils.removeFinalModifier()
Validate.isTrue(field != null, "The field must not be null");
try {
if (Modifier.isFinal(field.getModifiers())) {//是否是final類型
// Do all JREs implement Field with a private ivar called "modifiers"?
final Field modifiersField = Field.class.getDeclaredField("modifiers");
final boolean doForceAccess = !modifiersField.isAccessible();
if (doForceAccess) {
modifiersField.setAccessible(true);
}
try {
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
} finally {
if (doForceAccess) {
modifiersField.setAccessible(false);
}
}
}
} catch (final NoSuchFieldException ignored) {
// The field class contains always a modifiers field
} catch (final IllegalAccessException ignored) {
// The modifiers field is made accessible
}
}
8. Intent的Component 可以用來傳遞打開Activity的源頭參考
9. DexClassLoader 的 optimizedDirectory 和 librarySearchPath 只需要我們指定,不用我們自己去創(chuàng)建汪拥,并解析apk
==核心問題==
1. 宿主如何加載插件的類和so文件达传?
答:首先明確類和so文件都是通過ClassLoader進(jìn)行加載的,RePlugin中 有兩個(gè)ClassLoader 一個(gè)是宿主的 RePluginClassLoader
他是hook了App的原始 ClassLoader喷楣,一個(gè)是加載插件Class的 PluginDexClassLoader
趟大。當(dāng)宿主通過RePluginClassLoader
加載一個(gè)插件里的類時(shí),它先會(huì)去使用插件的PluginDexClassLoader
去加載铣焊,如果找到了就直接返回逊朽,如果找不到才會(huì)去自己進(jìn)行加載。具體的可以跟著上面 《宿主啟動(dòng)插件中某Activity流程》走一遍就知道了曲伊。
至于為什么 DexClassLoader叽讳,其實(shí)就是因?yàn)?DexClassLoader 在初始化的時(shí)候可以傳入一個(gè)已經(jīng)優(yōu)化過的dex文件路徑追他,就可以加載它。 可以動(dòng)態(tài)化可以參考
2. 插件中的資源是如何找到并加載的岛蚤? 以layout為例
2.1 插件Activity加載自己的layout文件 (比如:demo1插件的 MainActivity 加載 自己的 R.layout.main layout)
首先Activity在創(chuàng)建的時(shí)候會(huì)創(chuàng)建一個(gè) PhoneWindow 邑狸,PhoneWindow在創(chuàng)建的時(shí)候回創(chuàng)建一個(gè) LayoutInflater,這個(gè)過程中都傳遞了一個(gè)Context涤妒,LayoutInflater 會(huì)將這個(gè)Context記錄下來也就是mContext
,這個(gè)Context其實(shí)就是 Activity 的Context单雾。 setContentView( R.layout.main) 最后會(huì)調(diào)用到 LayoutInflater.infalte()方法,這個(gè)時(shí)候 就會(huì)通過mContext.getResources()
獲取 Resources 對(duì)象她紫,期間會(huì)調(diào)用到Activity的mBase.getResources
方法硅堆,最終會(huì)調(diào)用到ContextImpl的 getResources()
方法。
==2.2.1 系統(tǒng)正常啟動(dòng)apk的情況下==
上面提到Resources的獲取最終是通過ContextImpl.getResources()
方法獲取贿讹,而ContextImpl中的mResources
對(duì)象是在構(gòu)造方法中通過LoadedApk.getResources()
方法初始化的如下:
public Resources getResources(ActivityThread mainThread) {
if (mResources == null) {
//通過ApplicationInfo中的 一些文件夾創(chuàng)建 Resources
mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, null, this);
}
return mResources;
}
可以看到創(chuàng)建 Resources 的過程中使用到了 mResDir
(apk文件路徑渐逃,在RePlugin中就是插件的路徑) 等這些參數(shù),下面我們先看一下 系統(tǒng)正常啟動(dòng)apk的情況下 mResDir
等字段是哪里進(jìn)程賦值的民褂。
我們都知道在系統(tǒng)啟動(dòng)apk的過程中會(huì)通過zygote孵化一個(gè)新的進(jìn)程用于這個(gè)APK的運(yùn)行茄菊,當(dāng)新的進(jìn)程創(chuàng)建完畢需要將Application和這個(gè)進(jìn)程綁定的時(shí)候系統(tǒng)會(huì)調(diào)用ActivityThread.handleBindApplication
,我們就從這里還是看
1.1 ActivityThread.handleBindApplication
private void handleBindApplication(AppBindData data) {
....
InstrumentationInfo ii = null;
try {
//通過 PackageManagerService 解析Apk獲取 apk的一些基本信息
ii = appContext.getPackageManager().
getInstrumentationInfo(data.instrumentationName, 0);
} catch (PackageManager.NameNotFoundException e) {
}
....
//創(chuàng)建 ApplicationInfo 用于記錄APP的基本信息 如赊堪,包名面殖,apk路徑等
ApplicationInfo instrApp = new ApplicationInfo();
instrApp.packageName = ii.packageName;
instrApp.sourceDir = ii.sourceDir;
instrApp.publicSourceDir = ii.publicSourceDir;
instrApp.splitSourceDirs = ii.splitSourceDirs;
instrApp.splitPublicSourceDirs = ii.splitPublicSourceDirs;
instrApp.dataDir = ii.dataDir;
instrApp.nativeLibraryDir = ii.nativeLibraryDir;
//這里創(chuàng)建 LoadedApk 并通過 instrApp記錄的一些信息做一些初始化 詳見【1.2】
LoadedApk pi = getPackageInfo(instrApp, data.compatInfo,
appContext.getClassLoader(), false, true, false);
....
// 此處data.info是指LoadedApk, 通過反射創(chuàng)建目標(biāo)應(yīng)用Application對(duì)象
Application app = data.info.makeApplication(data.restrictedBackupMode, null);
}
1.2 ActivityThread.getPackageInfo
private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
boolean registerPackage) {
//創(chuàng)建LoadedApk對(duì)象 詳見【1.3】
packageInfo =
new LoadedApk(this, aInfo, compatInfo, baseLoader,
securityViolation, includeCode &&
(aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);
}
1.3 LoadedApk<init>
public LoadedApk(ActivityThread activityThread, ApplicationInfo aInfo,
CompatibilityInfo compatInfo, ClassLoader baseLoader,
boolean securityViolation, boolean includeCode, boolean registerPackage) {
final int myUid = Process.myUid();
aInfo = adjustNativeLibraryPaths(aInfo);
//ActivityThread對(duì)象
mActivityThread = activityThread;
mApplicationInfo = aInfo;
mPackageName = aInfo.packageName;
mAppDir = aInfo.sourceDir;
mResDir = aInfo.uid == myUid ? aInfo.sourceDir : aInfo.publicSourceDir;
mSplitAppDirs = aInfo.splitSourceDirs;
mSplitResDirs = aInfo.uid == myUid ? aInfo.splitSourceDirs : aInfo.splitPublicSourceDirs;
mOverlayDirs = aInfo.resourceDirs;
mSharedLibraries = aInfo.sharedLibraryFiles;
mDataDir = aInfo.dataDir;
mDataDirFile = mDataDir != null ? new File(mDataDir) : null;
mLibDir = aInfo.nativeLibraryDir;
mBaseClassLoader = baseLoader;
mSecurityViolation = securityViolation;
mIncludeCode = includeCode;
mRegisterPackage = registerPackage;
mDisplayAdjustments.setCompatibilityInfo(compatInfo);
}
從上面的分析得到,系統(tǒng)正常啟動(dòng)Apk的情況下雹食,系統(tǒng)會(huì)在Application創(chuàng)建之前就將 mResDir
等信息就賦值給了LoadedApk
,后面我們調(diào)用getResources
就會(huì)拿到正確的Resources
對(duì)象
==2.2.2 看完了正常情況下的畜普,那么RePlugin插件中的Activity是如何正常使用setContentView
的呢?==
在上面的描述中我們已經(jīng)知道在Activity中獲取Resources對(duì)象會(huì)通過mBase.getResources()
來獲取而且在分replugin-plugin-gradle
的時(shí)候我們知道插件在編譯器會(huì)將期繼承的Activity替換為PluginActivity
等Replugin內(nèi)部提供的Activity群叶,那么我們來看一下 PluginActivity
中有什么玄機(jī)嗎吃挑?
果真在 PluginActivity
中通過如下調(diào)用鏈將Activity的mBase替換成了PluginContext
對(duì)象,所以在Activity中獲取Resources對(duì)象最終會(huì)走到PluginContext.getResource
- PluginActivity.attachBaseContext
- RePluginInternal.createActivityContext
- ProxyRePluginInternalVar.createActivityContext.call
- RePluginInternal.createActivityContext
PluginContext.getResource
方法如下
public Resources getResources() {
if (mNewResources != null) {
return mNewResources;
}
return super.getResources();
}
他只是返回了mNewResources
,mNewResources
是在PluginContext
的構(gòu)造犯法中賦值的街立,PluginContext
是通過如下調(diào)用鏈創(chuàng)建的(==具體可以看調(diào)用鏈中的內(nèi)容==)
- Plugin.load(); 加載并啟動(dòng)插件
- Plugin.loadLocked() 加載插件
- Plugin.doLoad(): 加載插件信息舶衬、資源、Dex赎离,并運(yùn)行Entry類
- Loader.loadDex():
- PackageManager.getPackageArchiveInfo : 獲取插件的 PackageInfo
- mPackageInfo.applicationInfo.sourceDir : 設(shè)置插件的路徑逛犹,這個(gè)地址后面再獲取插件的Resources對(duì)象時(shí)會(huì)用到(設(shè)置之前是空的)
- pm.getResourcesForApplication : 創(chuàng)建插件使用的Resources對(duì)象
- RePlugin.getConfig().getCallbacks().createPluginClassLoader : 創(chuàng)建插件的ClassLoader
- new PluginContext : 創(chuàng)建插件使用的 Context (PluginContext),
- Loader.loadDex():
- Plugin.doLoad(): 加載插件信息舶衬、資源、Dex赎离,并運(yùn)行Entry類
- Plugin.loadLocked() 加載插件
==2.2.3:總結(jié)==
可以看出來系統(tǒng)啟動(dòng)APP和我們動(dòng)態(tài)加載插件完全是不一樣的思路梁剔。
2.2 插件中使用其他插件的layout文件
通過如下調(diào)用鏈就可以獲取到具體的View
- RePlugin.fetchViewByLayoutName
- RePluginCompat.fetchViewByLayoutName
- RePlugin.fetchContext : 加載插件虽画,并獲取插件自身的Context對(duì)象,以獲取資源等信息
- RePluginCompat.fetchResourceIdByName : 要注意一下的是 在插件編譯期 這個(gè)方法最后調(diào)用的 Resources.getIdentifier 的第三個(gè)參數(shù)被替換成了 插件的包名荣病,至于為什么現(xiàn)在還不知道(==關(guān)注問題中的第八個(gè)码撰,會(huì)后會(huì)在哪里解答==)
- RePlugin.fetchPackageInfo : 獲取 插件對(duì)應(yīng)的 PackageInfo
- RePlugin.fetchResources : 加載插件,并獲取插件的資源信息
- Resources.getIdentifier : 獲取資源id个盆,
- LayoutInflater.from(context).inflate : 填充為View
- RePluginCompat.fetchViewByLayoutName
3. 插件中的so文件是如何加載的脖岛?
3. RePlugin中的核心
- ClassLoader : DexClassLoader 和 RePluginClassLoader
- Context:PluginContext
問答
1. ==RePlugin是使用DexClassLoader加載自定義路徑下的dex嗎朵栖?==
答:是的,在Android中 DexClassLoader 總是動(dòng)態(tài)話的不二選擇柴梆,只不過 RePlugin中 有兩個(gè)ClassLoader 一個(gè)是宿主的 RePluginClassLoader
他是hook了App的原始 ClassLoader陨溅,一個(gè)是加載插件Class的 PluginDexClassLoader
。當(dāng)宿主通過RePluginClassLoader
加載一個(gè)插件里的類時(shí)绍在,它先會(huì)去使用插件的PluginDexClassLoader
去加載门扇,如果找到了就直接返回,如果找不到才會(huì)去自己進(jìn)行加載揣苏。
至于為什么 DexClassLoader悯嗓,其實(shí)就是因?yàn)?DexClassLoader 在初始化的時(shí)候可以傳入一個(gè)已經(jīng)優(yōu)化過的dex文件路徑,就可以加載它卸察。 可以動(dòng)態(tài)化可以參考
2. ProcessPitProviderPersist這個(gè)provider對(duì)外提供binder然后進(jìn)行通信,這樣做不會(huì)有安全問題嗎铅祸?
3. replugin-host-lib中 manifest中 配置的爆紅的 四大組件是干嘛的坑质?
4. RePlugin.attachBaseContext
方法中有提到 HostConfigHelper.init();
需要在IPC.init
只有進(jìn)行,那是不是說常駐進(jìn)程名肯定就是獨(dú)立進(jìn)程临梗?配置了也沒用涡扼?
答:有用的,不知道為啥會(huì)有那句注釋
5. PluginManagerProxy.connectToServer()是在干啥盟庞?
答:通過 binder 獲取到 PluginManagerServer.Stub 對(duì)象也就是 sRemote
6. StubProcessManager.schedulePluginProcessLoop這是在干啥吃沪?
答:應(yīng)該是在回收無用進(jìn)程
7. Plugin.attach 中的parent參數(shù)是已經(jīng)被 hook的 classLoader了嗎?
答:這個(gè)是沒有被 hook過的 什猖,因?yàn)檫@個(gè) 是在PmBase中初始化的票彪,PmBase這個(gè)類是在 hook之前加載的
8. ==replugin-plugin-gradle中為什么要替換 getIdentifier 的三個(gè)參數(shù)為當(dāng)前 插件包名?不替換行不行不狮?==
答:難道這個(gè)參數(shù)在解析.resc文件時(shí)會(huì)用到降铸,記得在ResGuard中就有解析packageName的時(shí)候,應(yīng)該是這樣的摇零,也不對(duì)啊推掸,它傳的的是調(diào)用方的包名...搞不懂
9. com.qihoo360.replugin.Entry 這個(gè)類在哪里?里面的 create 干了些啥驻仅?在Loader.loadEntryMethod3 方法中有使用到谅畅?
答:這個(gè)類位于 replugin-plugin-lib中,crate是宿主框架最先調(diào)用的類 用于初始化插件框架和環(huán)境
10. 動(dòng)態(tài)類是干啥的噪服? RePlugin.registerHookingClass 中會(huì)注冊(cè)毡泻?
答:在加載插件類的時(shí)候會(huì)用到 具體使用位置是 PmBase.loadClass,作用是作為真實(shí)類加載之前的 中介類,具體能干啥 還不太清楚芯咧,不過看描述很強(qiáng)大的感覺
11. ==PluginLibraryInternalProxy.startActivity 不是只是打開坑位Activity么牙捉?插件的Activity怎么顯示的竹揍?也就是Android 系統(tǒng)怎么被騙==了?
答:整體調(diào)用流程如下:
- Pmbase根據(jù)Intent找到對(duì)應(yīng)的插件
- 分配坑位Activity邪铲,與插件中的Activity建立一對(duì)一的關(guān)系并保存在PluginContainer中
- 讓系統(tǒng)啟動(dòng)坑位Activity芬位,因?yàn)樗窃贛anifest中注冊(cè)過的
- Android系統(tǒng)會(huì)嘗試使用RepluginClassLoader加載坑位Activity的Class對(duì)象
- RepluginClassLoader 通過建立的對(duì)應(yīng)關(guān)系找到插件Activity,并使用PluginDexClassLoader 加載插件Activity 的Class對(duì)象并返回
- Android系統(tǒng)就使用這個(gè)插件中的Activity的Class對(duì)象來運(yùn)行生命周期函數(shù)
- 讓系統(tǒng)以為是自己的classLoader加載的類但是其實(shí)是使用插件ClassLoader加載的然后給到系統(tǒng)带到,這一招偷梁換柱 真的是高啊 昧碉。到此 Android系統(tǒng)就被 騙啦
==這樣貍貓換太子也太6了== - 參考:Replugin 全面解析 (2)
12. 常駐進(jìn)程什么時(shí)候啟動(dòng)的?
答:是在ui進(jìn)程啟動(dòng)的過程中 通過 PluginProcessMain.connectToHostSvc
這個(gè)方法觸發(fā) ProcessPitProviderPersist
(運(yùn)行在常駐進(jìn)程)這個(gè)內(nèi)容提供者初始話而啟動(dòng)的
13. RePlugin是如何避免資源沖突的揽惹?
答:Replugin中宿主和插件被饿,插件和插件之間不會(huì)存在 資源沖突,因?yàn)?他們的資源壓根就不會(huì)合并搪搏。