Shadow解決Activity等組件生命週期的方法解析

明確問題

每一個Android插件框架要解決的首要問題都是Activity的生命週期問題。Activity表明了Service等其餘須要註冊的組件。不一樣是插件框架解決這個問題的前提也不徹底同樣。而咱們的業務要求比較苛刻,再加上Android 9.0的非公開API限制,因此由這些前提要求:html

  1. 插件代碼也要能正常編譯安裝運行。
  2. 插件代碼都是現有業務代碼,不能由於接入插件框架而須要修改代碼(即須要插件框架無代碼侵入性)。
  3. 在宿主的AndroidManifest.xml中只能註冊有限數量(大約10個)的組件。宿主AndroidManifest過大會使宿主安裝變慢,跨進程通訊出錯。
  4. 不能使用非公開API。

大方向的選擇

其實咱們早就在用一款也是基於代理組件轉調插件組件的插件框架了。只不過這款插件框架用到了大量反射使用私有API,眼看着是不可能再Android 9.0上繼續使用了。咱們也調研了外界口碑最好的RePlugin。因此大概就這兩種方向,一是用代理Activity做爲殼子註冊在宿主中真正運行起來,而後讓它持有插件Activity,想辦法在收到系統的生命週期方法調用時轉調插件Activity的對應生命週期方法。二是Hack修改宿主PathClassLoader,讓它能在收到系統查詢AndroidManifest中註冊的Activity的類時返回插件的Activity類。android

方法二就是RePlugin的關鍵技術。它利用了JVM的特性。我也不太確定這算不算是bug,總之ClassLoader的loadClass方法返回的實際類能夠和它被要求加載的類名字不同。舉個例子,宿主的AndroidManifest.xml註冊一個Activity名叫A,插件裏有一個Activity名叫B。宿主代碼或者apk中最終是沒有A這個類的,只有在AndroidManifest中註冊的一個名字而已。當想要加載插件Activity B時,就發出一個啓動Activity A的Intent。系統收到這個Intent後會檢查宿主安裝的AndroidManifest信息,從中肯定A是哪一個apk安裝的,就會找到宿主的PathClassLoader。而後系統就會試圖從PathClassLoader中加載A這個類,而後做爲Activity類型的對象使用(這很正常)。因此若是咱們把宿主的PathClassLoader給Hack了,控制它的加載邏輯,讓它收到這個加載調用時實際返回的是插件Activity B的類。因爲B也真的是Activity的子類,因此係統拿回去看成Activity類型使用沒有任何問題。這裏再擴展一下,若是類C繼承自類A,在加載C時也會去加載A,若是這時拿B當A返回的話,C收到B以後是會發現B的名字不是A而出錯的。關於RePlugin這段關鍵技術的實現,當時調研時就發現實現的有些麻煩了。RePlugin選擇複製一個PathClassLoader,而後替換系統持有的PathClassLoader。因此複製PathClassLoader須要反射使用PathClassLoader的私有API,拿出來它裏面的數據,替換系統持有的PathClassLoader也要反射修改私有API。咱們當時已經實現了「全動態插件框架」,其中代理殼子Activity的動態化使用的方法也能解決這個問題,咱們的選擇是在宿主PathClassLoader上給它加一個parent ClassLoader。由於PathClassLoader也是一個有正常「雙親委派」邏輯的ClassLoader,它加載什麼類都會先問本身parent ClassLoader先加載。因此咱們加上去的這個parent ClassLoader也能完成RePlugin想要作的事。不過咱們用它的目的是不但願殼子Activity打包在宿主佔用宿主不少方法數,還不能更新。這一點之後可能再單獨講。關於這個替換實現,最近給RePlugin提了一個PR:github.com/Qihoo360/Re… ,有興趣的同窗能夠看一下。git

RePlugin的這種方案還有一點很是不適合咱們的業務,就是宿主AndroidManifest中註冊的「坑位」Activity,就是上面舉例的Activity A,是不能同時供多個插件Activity使用的。就是我不能在宿主AndroidManifest中註冊一個Activity A,而後讓它同時支持插件Activity B和C。這是由於ClassLoader在loadClass的時候,收到的參數只有一個A的類名,咱們沒有辦法傳遞更多信息,讓ClassLoader能在這個loadClass的調用中區分出應該返回B仍是應該返回C。因此這種方案須要在宿主中註冊大量Activity,這對於咱們的宿主來講是不可接受的。而方法一是用代理Activity持有插件Activity轉調的方案,就能夠在啓動代理Activity時經過Intent傳遞不少參數,代理Activity經過Intent中的參數就能決定該構造一個B仍是一個C。這就使得這種方案下殼子是可複用的。github

還有一點就是咱們在舊框架上就已經設計了「全動態插件框架」,因此基於方法一的方向上開發新插件框架,咱們能夠不修改宿主的任何代碼,不跟宿主版本就能更新插件框架。關於這一點,後續文章再解析。編程

因此咱們探索的方向就這樣肯定在方法一這個方向上了。bash

舊框架爲何要用反射、要用私有API?

咱們的舊框架就是代理Activity轉調插件Activity的方案,市面上還有不少插件框架也是這種方案。你們只是在實現轉調的手段上不同。這裏就不深刻分析它們都是怎麼實現的了,總之就是一個目的,讓插件Activity能收到生命週期回調。咱們的舊框架和其餘一些框架有一種這樣的方案,就是讓殼子Activity直接反射調用插件Activity對應的生命週期方法。這樣作要解決一個額外的問題。框架

Activity被系統構造出實例以後,並非直接調用onCreate方法的。首先會調用它的attach方法。attach方法實際上就是Activity的初始化方法,系統經過這個方法向Activity注入一些私有變量,好比Window、ActivityThread等等。插件Activity因爲是咱們殼子Activity本身new出來的,因此係統不會調用插件Activity的attach方法初始化它。Activity若是沒有初始化就被調用了onCreate會有什麼問題呢?咱們前面說了咱們的一個前提是插件Activity要求也要能正常編譯安裝運行,因此插件Activity的onCreate方法裏必定寫了super.onCreate()調用。咱們還要求對插件代碼無侵入性,因此也不能在這個調用外面包一層「if (不是插件模式)"。那麼在插件環境下,這個super.onCreate()就必定會執行。Activity基類的onCreate方法就會使用那些應該初始化過的私有變量,可是如今它們沒有初始化。因此這一類插件框架方案就要解決這個問題。因此要麼是反射調用attach方法,傳入從殼子Activity拿到的私有變量,好比說反射出殼子Activity的ActivityThread對象,傳給插件Activity的attach方法。要麼就乾脆直接用反射枚舉讀寫殼子Activity和插件Activity的私有變量,把它們寫成同樣的完成這個初始化。因此這就是爲何舊框架須要使用反射和私有API。ide

Shadow如何解決的插件Activity生命週期問題

實際上經過前面的分析,咱們發現其實根本不須要插件Activity執行super.onCreate()方法。明確了這個方案本來的目的,就是在宿主中註冊並啓動一個殼子Activity,這個殼子Activity什麼都不本身作,想辦法讓插件Activity的各個生命週期方法實現代碼成爲殼子Activity的各個生命週期實現方法的代碼。所以咱們根本不須要插件Activity是一個系統Activity的子類。咱們只是由於須要插件Activity還能正常安裝運行,才致使它是一個真正的系統Activity子類的。spa

咱們也知道若是不要求對插件代碼無侵入性,也不要求插件能獨立安裝運行,其實是能夠把讓插件Activity不用繼承系統Activity了,就簡單繼承一個普通類就好了。這個普通類上定義一些跟系統Activity類同樣的生命週期方法,實現成空實現,而後這些生命週期方法能夠設置成public的,這樣殼子Activity以這個普通類類型持有插件Activity就能夠直接調用插件Activity的生命週期方法了。這樣實現既不用反射也不用私有API。插件

而咱們其實是不須要插件的apk能獨立安裝運行的,咱們但願插件能獨立安裝運行的本質目的是節省人力,不要維護兩套代碼。因此看起來,這裏只要引入AOP手段,經過AOP編程修改插件Activity的父類,把插件Activity的父類從系統Activity改爲咱們想要的普通類就好了。

咱們先不說這個AOP手段怎麼實現,由於問題還沒完全搞清。咱們是真的想讓A持有B,A收到什麼調用就轉調B的什麼調用嗎?這是咱們的真正目的嗎?不是的。單純的只是讓插件Activity中的super.onCreate()調用失效,並不完美。由於這跟插件Activity正常安裝運行時還有點不同。如今插件Activity的onCreate方法的代碼就至關因而殼子Activity的onCreate方法的代碼的一部分了。好比:

class ShadowActivity {
    public void onCreate(Bundle savedInstanceState) {
    }
}

class PluginActivity extends ShadowActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        System.out.println("Hello World!");
    }
}

class ContainerActivity extends Activity {
    ShadowActivity pluginActivity;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        pluginActivity.onCreate(savedInstanceState);
    }
}
複製代碼

上面的ShadowActivity就是咱們前面說的普通類。仔細看一下是否是就至關於ContainerActivity本來就實現成這樣:

class ContainerActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        System.out.println("Hello World!");
    }
} 
複製代碼

只要PluginActivity是動態加載的,就至關於ContainerActivity的實現是動態的。可是若是本來PluginActivity的代碼是這樣的呢?

class PluginActivity extends ShadowActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        savedInstanceState.clear();
        super.onCreate(savedInstanceState);
    }
}
複製代碼

顯然這種代碼在正常安裝運行時和插件環境運行時就不同了。由於變成了:

class ContainerActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        savedInstanceState.clear();
    }
}
複製代碼

這說明咱們要的不是插件Activity的super.onCreate()調用不執行,咱們是但願插件Activity的super.onCreate()可以直接指揮殼子Activity何時調用super.onCreate()。並且再想一想,這是否是繼承關係很像?假如PluginActivity是繼承自ContainerActivity的,運行時系統調用的是PluginActivity的實例,那麼PluginActivity的super.onCreate()就會直接指導ContainerActivity何時調用super.onCreate()了。因此咱們在這裏的真正需求是如何把本來的繼承關係用持有關係實現了。因此Shadow是這樣實現的:

class ShadowActivity {
    ContainerActivity containerActivity;
    public void onCreate(Bundle savedInstanceState) {
        containerActivity.superOnCreate(savedInstanceState);
    }
}

class PluginActivity extends ShadowActivity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        savedInstanceState.clear();
        super.onCreate(savedInstanceState);
    }
}

class ContainerActivity extends Activity {
    ShadowActivity pluginActivity;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        pluginActivity.onCreate(savedInstanceState);
    }

    public void superOnCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
}
複製代碼

仔細思考下如今的調用結果,是否是PluginActivity在正常安裝運行和插件環境下運行時行爲就一致了?

好了,如今就能夠考慮怎麼實施AOP手段將PluginActivity的父類從系統Activity改成ShadowActivity了。Shadow最終使用的是字節碼編輯手段。字節碼編輯能夠經過Android官方的構建過程當中的Transform API來實現,因此這個手段也是利用公開API實現的。關於字節碼編輯的細節之後的文章再講。總之,經過字節碼編輯,咱們能夠在不修改源碼的狀況下,將插件Activity的父類從系統Activity改成ShadowActivity。從而達到同一份源碼應用不一樣編譯選項生成不一樣的apk,一個能正常安裝運行,一個能在插件環境運行。

其實字節碼編輯並非達到咱們的目的的惟一手段。最初我並無一上來就用字節碼編輯實現,由於字節碼編輯在構建工程上要使用更多技巧,這些技巧後面也會分享。想達到這個AOP目的最簡單的方法是利用Java語言的基本特性。Java類在編譯時是沒有連接過程的,在Java類的字節碼中只記錄了依賴的其餘類的名字,在運行的時候纔會去問ClassLoader查找這些類的具體實現。因爲插件的ClassLoader就是咱們自行建立的,因此咱們徹底能夠在實現插件的ClassLoader時將它設計成不遵循「雙親委派」機制的ClassLoader。在插件Activity查找它的父類系統Activity時,咱們就給它返回一個假的系統Activity類。把ShadowActivity當作系統Activity返回給它就能夠了。這個代碼實現就比字節碼編輯簡單太多了。惋惜的是,Android的Java虛擬機並非一個標準的JVM。對於系統內置的類,好比系統Activity來講,它會直接使用預編譯好的Native實現。在有些手機上Debug模式沒有這一特性,上述方案就能正常運行。在Release模式下就會使用預編譯的Native實現,致使JVM崩潰。我想寫Android JVM虛擬機的人就沒想到會有人返回一個假的系統類吧,因此它甚至沒有拋出異常,出現了野指針。關於這一點,能夠參考這篇文章瞭解一些細節:mshockwave.blogspot.com/2016/03/int…

後續的開發也利用了字節碼編輯作了更多事情,那些事情是不能用ClassLoader技巧實現的。關於那些事情也在將來分享吧。

總結

因此,這篇文章講的東西能夠說就是Shadow實現零反射、無非公開API調用的關鍵了。怎麼樣?大家以爲是否是真的是一層窗戶紙,一捅就破了?解決問題的關鍵不是最後的代碼,而是最初的思想。「任何軟件工程遇到的問題均可以經過增長一箇中間層來解決」,Shadow在解決這個問題上加入的ShadowActivity就是這個中間層。經過這個中間層讓Android系統看不到插件,也讓插件看不到Android系統。這樣就不會打破Android系統的限制。反觀RePlugin的方案,在大方向上就將未安裝的Activity交給了系統,所以後面必然不得不繼續用Hack手段解決系統看到了未安裝的Activity的問題。好比系統爲插件Activity初始化的Context是以宿主的apk初始化的,插件框架就不得再也不去Hack修復。因此大方向的選擇很重要。

相關文章
相關標籤/搜索