筆記,參考7.2中所列參考文章所寫,DEMO地址在PluginTestDemoApplication
1.綜述
2015年是Android外掛化技術突飛猛進的一年,隨著業務的發展各大廠商都碰到了Android Native平臺的瓶頸。 從技術上講,業務邏輯的複雜導致程式碼量急劇膨脹,各大廠商陸續出到65535方法數的天花板;同時,運營為王的時代對於模組熱更新提出了更高的要求。 在業務層面上,功能模組的解耦以及維護團隊的分離也是大勢所趨;各個團隊維護著同一個App的不同模組,如果每個模組升級新功能都需要對整個app進行升級,那麼釋出流程不僅複雜而且效率低下;在講究小步快跑和持續迭代的移動網際網路必將遭到淘汰。 H5和Hybird可以解決這些問題,但是始終比不上native的使用者體驗;於是,國外的FaceBook推出了react-native;而國內各大廠商幾乎都選擇純native的外掛化技術。可以說,Android的未來必將是react-native和外掛化的天下。 react-native資料很多,但是講述外掛化的卻鳳毛菱角;
外掛化技術聽起來高深莫測,實際上要解決的就是兩個問題:
- 1.程式碼載入
- 2.資源載入
1.1 需要解決的問題
-
1.程式碼的載入
類的載入可以使用Java的ClassLoader機制,但是對於Android來說,並不是說類載入進來就可以用了,很多元件都是有“生命”的;因此對於這些有血有肉的類,必須給它們注入活力,也就是所謂的元件生命週期管理;
另外,如何管理載入進來的類也是一個問題。假設多個外掛依賴了相同的類,是抽取公共依賴進行管理還是外掛單獨依賴?這就是ClassLoader的管理問題;
-
2.資源的載入
資源載入方案大家使用的原理都差不多,都是用AssetManager的隱藏方法addAssetPath;但是,不同外掛的資源如何管理?是公用一套資源還是外掛獨立資源?共用資源如何避免資源衝突?對於資源載入,有的方案共用一套資源並採用資源分段機制解決衝突(要麼修改aapt要麼新增編譯外掛);有的方案選擇獨立資源,不同外掛管理自己的資源。
1.2 Android中載入類 和 Java載入類的區別
Android中許多元件類(如Activity、Service等)是需要在Manifest檔案裡面註冊後才能工作的(系統會檢查該元件有沒有註冊),所以即使動態載入了一個新的元件類進來,沒有註冊的話還是無法工作;
Res資源是Android開發中經常用到的,而Android是把這些資源用對應的R.id註冊好,執行時通過這些ID從Resource例項中獲取對應的資源。如果是執行時動態載入進來的新類,那類裡面用到R.id的地方將會丟擲找不到資源或者用錯資源的異常,因為新類的資源ID根本和現有的Resource例項中儲存的資源ID對不上;
2.實現思路
目前國內開源的較成熟的外掛方案有DL和DroidPlugin;但是DL方案僅僅對Framework的表層做了處理,嚴重依賴that語法,編寫外掛程式碼和主程式程式碼需單獨區分;而DroidPlugin通過Hook增強了Framework層的很多系統服務,開發外掛就跟開發獨立app差不多;就拿Activity生命週期的管理來說,DL的代理方式就像是牽線木偶,外掛只不過是操縱傀儡而已;而DroidPlugin則是借屍還魂,外掛是有血有肉的系統管理的真正元件;DroidPlugin Hook了系統幾乎所有的Sevice,欺騙了大部分的系統API;掌握這個Hook過程需要掌握很多系統原理,因此學習DroidPlugin對於整個Android FrameWork層大有裨益。
以DroidPlugin為例的方式
-
1.構造DexClassLoader : 既然我們知道如果想啟動外掛apk就需一個Classloader,那麼我們換一種想法,能不能我們將我們的外掛apk的資訊告訴系統的這個Classloader,然後讓系統的Classloader來幫我們載入及建立呢?答案是肯定之前我們說過講過android中的Classloader主要分析PathClassLoader和DexClassLoader,系統通過PathClassLoader來載入系統類和主dex中的類。而DexClassLoader則用於載入其他dex檔案中的類。他們都是繼承自BaseDexClassLoader。(如果沒有看過的建議先看看 外掛化知識詳細分解及原理 之ClassLoader及dex載入過程)
-
2.拿到宿主apk裡ClassLoader中的pathList物件和我們Classloader的pathList,進行合併
- 2.1 通過PathClassLoader拿到宿主應用的dexPathList,通過DexClassLoader拿到外掛的dexPathList。
- 2.2 拿到宿主和外掛的dex列表
- 2.3 將宿主和外掛的dexPathList合併
-
3.hook住startActivity方法,使用佔坑的方式,也就是說我們可以提前在AndroidManifest中固定寫死一個Activity,這個Activity只不過是一個傀儡,我們在啟動我們外掛apk的時候使用它去系統層校檢合法性,然後等真正建立Activity的時候再通過hook思想攔截Activity的建立方法,提前將資訊更換回來建立真正的外掛apk。
- 3.1 通過反射,拿到AMS的代理物件
- 3.2 自定義InvocationHandler攔截startActivty方法
- 3.3 帶系統檢查完Activity合法性後重新執行原Activity
3.準備工作
3.1 理解應用的啟動過程
下面是大致的流程,詳細內容可以看Activity的筆記或者羅昇陽的《Android原始碼分析》
應用程式是由Launcher啟動起來的,其實Launcher本身也是一個應用程式。
啟動一個應用時會呼叫 startActivitySafely
-> startActivity
-> startActivityForResult
最終會呼叫Instrumentation
的execStartActivity
方法。
在execStartActivity
中會呼叫ActivityManagerNative.getDefault().startActivity
,實際上就是呼叫了AMS的startActivity方法。
在execStartActivity
中還會呼叫checkStartActivityResult
方法對start的結果進行檢查。
AMS的startActivity
->startActivityAsUser
->mActivityStarter.startActivityMayWait
,最終呼叫 ActivityStackSupervisor
的realStartActivityLocked
方法。
在realStartActivityLocked
方法中,會呼叫ActivityThread
的scheduleLaunchActivity
方法。
在 scheduleLaunchActivity
方法中 有一行程式碼 sendMessage(H.LAUNCH_ACTIVITY, r);
在ActivityThread
內部的H
類中呼叫
public void handleMessage(Message msg) {
if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
switch (msg.what) {
case LAUNCH_ACTIVITY: {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
} break;
}
複製程式碼
在handleLaunchActivity
->performLaunchActivity
在 performLaunchActivity
中通過Instrumentation.newActivity
建立Activity,並建立Application和Context
3.2 理解Activity和Service的啟動過程
在performLaunchActivity
方法中
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
複製程式碼
實際是通過r.packageInfo這個LoadedApk型別的物件獲得。
LoadedApk儲存了Apk檔案的相關資訊。
在H
類裡通過getPackageInfoNoCheck
方法獲得LoadedApk,針對程式碼的資訊會存到mPackages
表裡
apk被安裝之後,apk的檔案程式碼以及資源會被系統存放在固定的目錄比如/data/app/package_name/base.apk)中,系統在進行類載入的時候,會自動去這一個或者幾個特定的路徑來尋找這個類。(可以在Android Studio的Device File Explorer去檢視)
在startActivityLocked方法內部進行了一系列重要的檢查:比如許可權檢查,Activity的exported屬性檢查等等;我們上文所述的,啟動沒有在Manifestfest中顯示宣告的Activity拋異常也是這裡發生的:
3.3 理解DexClassLoader 和 PathClassLoader
3.3.1 DexClassLoader
建構函式:
public DexClassLoader (String dexPath, String dexOutputDir, String libPath, ClassLoader parent)
複製程式碼
dexPath:dex檔案路徑列表,多個路徑使用”:”分隔
dexOutputDir:經過優化的dex檔案(odex)檔案輸出目錄
libPath:動態庫路徑(將被新增到app動態庫搜尋路徑列表中)
parent:這是一個ClassLoader,這個引數的主要作用是保留java中ClassLoader的委託機制(優先父類載入器載入classes,由上而下的載入機制,防止重複載入類位元組碼)
複製程式碼
DexClassLoader是一個可以從包含classes.dex實體的.jar或.apk檔案中載入classes的類載入器。可以用於實現dex的動態載入、程式碼熱更新等等。這個類載入器必須要一個app的私有、可寫目錄來快取經過優化的classes(odex檔案)
3.3.2 PathClassLoader
PathClassLoader提供兩個常用構造方法
public PathClassLoader (String path, ClassLoader parent)
1
public PathClassLoader (String path, String libPath, ClassLoader parent)
複製程式碼
path:檔案或者目錄的列表
libPath:包含lib庫的目錄列表
parent:父類載入器
複製程式碼
3.3.3區別
- DexClassLoader:能夠載入未安裝的jar\apk dex。
- PathClassLoader:Android系統通過PathClassLoader來載入系統類和主dex中的類。
參考:
3.4 理解反射和Hook
首先我們得找到被Hook的物件,我稱之為Hook點;什麼樣的物件比較好Hook呢?自然是容易找到的物件。什麼樣的物件容易找到?靜態變數和單例;在一個程式之內,靜態變數和單例變數是相對不容易發生變化的,因此非常容易定位,而普通的物件則要麼無法標誌,要麼容易改變。我們根據這個原則找到所謂的Hook點。
-
1.尋找Hook點,原則是靜態變數或者單例物件,儘量Hook pulic的物件和方法,非public不保證每個版本都一樣,需要適配。
-
2.選擇合適的代理方式,如果是介面可以用動態代理;如果是類可以手動寫代理也可以使用cglib。
-
3.偷樑換柱——用代理物件替換原始物件
4.其他
4.1 熱門熱修復框架
目前比較有名的外掛化框架: 任玉剛的:dynamic-load-apk,這個專案使用的是一種代理的方式去實現 https://github.com/singwhatiwanna/dynamic-load-apk
360的:DroidPlugin,這個專案是通過hook系統類來實現 https://github.com/Qihoo360/DroidPlugin
目前比較火的熱修復框架: 阿里的:andfix,用於對方法的修復,可以立即生效,不支援資源及類替換 https://github.com/alibaba/AndFix
騰訊的:tinker,除了不支援立即生效,全部支援 https://github.com/Tencent/tinker
美團的:robust,不開源
7.2 參考文章
總述
Hook
生命週期管理
- Android 外掛化原理解析——Activity生命週期管理
- Android外掛化原理解析——廣播的管理
- Android 外掛化原理解析——Service的外掛化
- Android外掛化原理解析——ContentProvider的外掛化
外掛的載入
下面這幾篇文章是參考上面的文章所寫,主要參考上面的幾篇文章,下面的文章作為補充