Android 外掛化原理入門筆記

李納斯小盒發表於2018-05-21

Android開發筆記 onGithub

筆記,參考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

最終會呼叫InstrumentationexecStartActivity方法。

execStartActivity中會呼叫ActivityManagerNative.getDefault().startActivity ,實際上就是呼叫了AMS的startActivity方法。

execStartActivity中還會呼叫checkStartActivityResult方法對start的結果進行檢查。

AMS的startActivity->startActivityAsUser->mActivityStarter.startActivityMayWait,最終呼叫 ActivityStackSupervisorrealStartActivityLocked方法。

realStartActivityLocked方法中,會呼叫ActivityThreadscheduleLaunchActivity方法。

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點。


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

生命週期管理

外掛的載入

下面這幾篇文章是參考上面的文章所寫,主要參考上面的幾篇文章,下面的文章作為補充

相關文章