framework外掛化技術-類載入

gogogo在掘金發表於2018-09-02

近兩年,Android的熱升級技術成為Android領域的一個熱點。由於快速迭代的需要,熱修復,外掛化的技術已經深入app及framework的各個研究領域。

背景技術

簡單的介紹一下熱修復技術的背景: 以往的app專案管理流程如下:

framework外掛化技術-類載入
以上流程有版本週期長,使用者安裝成本高,bug修復不及時使用者體驗差等諸多缺點。 為了改變這樣的現狀,各大網際網路公司為此投入了很多研究,熱修復技術應運而生,把更新以補丁的方式上傳到雲端,app從雲端直接下載補丁即時生效,流程如下:

framework外掛化技術-類載入
可見使用熱修復技術之後能夠實現使用者無感知的修復。 在Android的熱修復主要分三大領域: 程式碼修復,資源修復,so修復。
程式碼修復有兩大主要方案:

  1. 阿里系的底層替換方案
  2. 騰訊系的類載入方案

兩類方案的優劣:
底層替換方案限制多,但時效性最好,載入輕快,立即見效。
類載入方案時效性差,需要app的冷啟動才能見效,但是修復範廣,限制少。

關於底層替換方案,比較出色的應該是阿里的Sophix了。核心原理是替換java方法對應的底層虛擬機器的ArtMethod,達到即時修復的效果。這個不是本文介紹的重點,詳情大家可以參看《深入探索Android熱修復技術原理》一書。 而冷啟動的方式則是將要修改的程式碼打成dex通過插包或者是合併的方式打入dexElements裡。這種方式能夠突破底層的諸多限制,但是同樣也會碰到一些Android原有校驗規則的限制,比如:CLASS_ISPREVERIFIED問題。

framework特性外掛化

同樣的,在Android手機的framework層也遇到類似的問題。目前,各大手機廠商基本都會對Google的原生framework進行或多或少的定製。而如果framework的特性需要升級,以往的流程是:

framework外掛化技術-類載入

而framework層有很多特性,在framework層的客戶端,本質上是app依賴的一些系統級lib。為了縮短髮布週期,讓使用者更快的體驗到我們的新特性,我們也希望能夠使用熱升級技術,將特性lib從framework層脫離出來,成為一個獨立的個體存在:

framework外掛化技術-類載入
將系統的lib從系統中解耦,成為一個獨立於平臺的lib,將會帶來以下好處:

  1. 特性更新快,熱升級
  2. 跨平臺,不依賴於系統rom
  3. 向後相容

support包

Google的support包就是Google對framework向後相容的一個實現。將framework的部分特性抽離,做成support包的形式,單獨釋出,讓新特性得以向後相容,不依賴於系統rom,可以橫跨多個Android版本。主要的實現方式是將support包作為靜態jar一起打包至app,特性跟著app走而不跟隨系統:

framework外掛化技術-類載入
如上圖所示是AndroidStudio裡編譯生成的一個demo apk的apk結構,從圖中我們可以看到在apk生成的classes.dex裡已經包含了support包的各個類。support包已經成為了app的一部分。這樣的方式帶來的一個缺陷就是support包特性的更新必須依賴於app的更新。當然,我們也可以採取以上介紹的各種熱修復技術去更新support包特性。但是作為framework層,我們更希望去尋找一種更基礎的方案,讓特性以一種的新的形式去載入。為此,我們需要看一下Android的類載入。

類載入

我們都知道Java的類載入是通過ClassLoader來載入的。

framework外掛化技術-類載入
而ClassLoader的類載入又是雙親代理模式,也就是樹形結構。Android雖然對ClassLoader在具體的實現上有些改變,但是結構是不變的。

framework外掛化技術-類載入
而一般app的class關係樹如圖:

framework外掛化技術-類載入

預載入

BootClassLoader是所有classLoader的parent,載入的優先順序最高,負責載入一些需要預載入的類。類定義在 /libcore/ojluni/src/main/java/java/lang/ClassLoader.java

class BootClassLoader extends ClassLoader {

    private static BootClassLoader instance;

    public static synchronized BootClassLoader getInstance() {
        if (instance == null) {
            instance = new BootClassLoader();
        }

        return instance;
    }
    ...
}
複製程式碼

從以上程式碼可見,BootClassLoader是ClassLoader的內部類,訪問許可權是包內可見,所以可以知道僅僅只對部分系統開放。那麼BootClassLoader是在哪建立的,前面所說的預載入的類,又是在哪載入的?這個就要從系統啟動時的zygote程式的初始化說起了。
zygote程式初始化在/frameworks/base/core/java/com/android/internal/os/ZygoteInit.java執行,在該類的main函式裡會執行一些系統的zygote程式的初始化操作,並預載入操作:

public static void main(String argv[]) {
    ...
    preload(bootTimingsTraceLog);
    ...
}

複製程式碼

繼續看preload方法:

static void preload(TimingsTraceLog bootTimingsTraceLog) {
    ...
    preloadClasses();
    ...
}
複製程式碼

preload方法裡會執行preloadClass()方法進行類的預載入:

private static void preloadClasses() {
    ...
    InputStream is;
        try {
            is = new FileInputStream(PRELOADED_CLASSES);
        } catch (FileNotFoundException e) {
            Log.e(TAG, "Couldn't find " + PRELOADED_CLASSES + ".");
            return;
        }
    ...
    try {
            BufferedReader br
                = new BufferedReader(new InputStreamReader(is), 256);

            int count = 0;
            String line;
            while ((line = br.readLine()) != null) {
                // Skip comments and blank lines.
                line = line.trim();
                ...
                try {
                    ...
                    Class.forName(line, true, null);
                    ...
                } catch (ClassNotFoundException e) {
                    Log.w(TAG, "Class not found for preloading: " + line);
                } catch (UnsatisfiedLinkError e) {
                    Log.w(TAG, "Problem preloading " + line + ": " + e);
                } catch (Throwable t) {
                    ...
                }
            }
    ...
}
複製程式碼

可以看到,在preloadClass方法中,首先會去逐行讀取PRELOADED_CLASSES檔案,看下該檔案指向的路徑:

private static final String PRELOADED_CLASSES = "/system/etc/preloaded-classes";
複製程式碼

我們看下preload-classes檔案:

...
android.app.INotificationManager
android.app.INotificationManager$Stub
android.app.INotificationManager$Stub$Proxy
android.app.IProcessObserver
android.app.IProcessObserver$Stub
android.app.ISearchManager
android.app.ISearchManager$Stub
android.app.IServiceConnection
android.app.IServiceConnection$Stub
android.app.ITransientNotification
android.app.ITransientNotification$Stub
android.app.IUiAutomationConnection
...
複製程式碼

可以看到該檔案每一行基本都是framework的類,則Zygote程式通過BufferedReader逐行讀取檔案裡的每一個類,通過Class.forName方法載入到Zygote程式的記憶體中。這裡我們注意到,Class.forName的第三個傳參為null

Class.forName(line, true, null);
複製程式碼

那麼我們再來看/libcore/ojluni/src/main/java/java/lang/Class.java檔案:

public static Class<?> forName(String name, boolean initialize,
                                   ClassLoader loader)throws ClassNotFoundException {
        if (loader == null) {
            loader = BootClassLoader.getInstance();
        }
        ...
}
複製程式碼

可以看到第三個引數是ClassLoader,並且當傳參為null時,會構造BootClassLoader。到這裡我們可以看到,預載入的類,最終是會交給BootClassLoader來載入。當app執行時,如果需要用到系統的類時,則可以通過訪問他們父程式記憶體空間中在系統初始化時就已載入的類定義來訪問framework的類了,而不需要在使用到時才重新進行載入。並且系統公共的類定義只存在與zygote程式的記憶體當中,而不需要每個app程式載入一份,可以同時達到空間和時間上的節省。

外掛載入

從上述過程我們可以知道,framework裡的特性,都是在系統啟動時就通過BootClassLoader載入到zygote程式當中了,那麼如果我們需要更新那些特性,則需要更新系統配置,系統jar包,並且重啟系統。整個過程非常麻煩。如果我們希望將framework裡的特性抽取出來,作為一種可插拔式的外掛存在如何做到呢?上述ClassLoader的樹形關係則給了我們啟發:

framework外掛化技術-類載入
將app的classloader與BootClassLoader的直接父子關係切斷,中間加入為我們抽離出來的特性構建的CloassLoader作為app的父ClassLoader,而BootClassLoader則作為外掛CloassLoader的parent,當外掛不存在時,BootClassLoader仍然是app classloader的直接parent。這樣我們就可以實現外掛可插拔了,程式碼實現也很簡單:

public void addPluginLoader(Application app) {
        if(!checkToDownloadPlugin(app)) {
            return;
        }

        PathClassLoader parent = new PathClassLoader(mDexPath, app.getClassLoader().getParent());

        try {
            Field parentField = ClassLoader.class.getDeclaredField("parent");
            parentField.setAccessible(true);
            parentField.set(app.getClassLoader(), parent);
        } catch (NoSuchFieldException e) {
            ...
        } catch (IllegalAccessException e) {
            ...
        }
}
複製程式碼

其中的mDexPath則是外掛特性的路徑,此方法可放在Application的onCreate方法裡執行,實現外掛的載入。 一般來說,解除安裝外掛,不能影響到app的其他特性,所以在整體架構設計上我們還需要有一個介面作為沒有plugin的預設實現打包到apk當中。如下圖所示:

framework外掛化技術-類載入
我們可以將plugin的預設實現(在系統上沒有下載plugin實現時)以靜態lib的方式作為plugin的interface與app一起編譯打包,同樣的,plugin載入器(plugin loader)也作為靜態lib打包至app內部,其中的PluginApplication是一個繼承自Application的類,實現plugin包的載入。載入流程:

  1. 檢查當前是否存在plugin或plugin是否是最新,如果不是則從雲端下載,並校驗其安全性。
  2. 如果plugin已經ready,則構建plugin的ClassLoader並進行app classLoader的parent重定向。

例如plugin裡有一個叫PluginFeature的類,如果classLoader已經重定向好,則根據ClassLoader的雙親代理特性,雖然說interface裡也存在同樣的PluginFeature的類,但是interface是由app的ClassLoader載入的,由於plugin的ClassLoader優先順序更高,會去載入plugin的PluginFeature類,這樣,就可以達到介面與實現的分離。當我們需要更新特性時,只需要更新plugin,而不需要更新app。以達到特性更好的模組化開發,降低特性與app的耦合度。
當然這種方式,我們得保證對應用開放的介面不變,如果介面需要改變,應用還是需要更新自己的apk。 與Google提供的的support方案相比,這種動態載入的外掛化方案能夠給我們帶來以下好處:

  1. 在介面不變的情況下實現熱更新,不需要重新安裝apk。
  2. 系統可以只存在一份外掛,而不需要每個apk一份,節省rom空間。

當然,Android的plugin特性裡不僅僅只是程式碼,還有資源。我們知道已安裝的apk資源是在apk之間互相訪問的,但是我們下載的plugin並不希望在系統裡安裝,僅僅只是作為一個檔案存在於手機系統中,那plugin裡資源的載入如何實現呢?感興趣的同學可以看下下一篇:《framework外掛化技術-資源載入(免安裝)》

相關文章