這篇文章介紹一下Shadow的跨進程設計和插件Service的原理。一同講這兩部分是由於它們是相關的。這篇文章假設讀者對於Android的Service、Binder通訊沒有那麼瞭解,所以會說起一些可能對你來講有些簡單的內容。android
在Android系統中,應用能夠是多進程的。這在移動端操做系統中應該是很是高級的設計了,不少移動端操做系統、嵌入式系統都是不支持的。多進程程序給程序設計帶來了頗有優勢,也帶來更多的複雜性。git
Android系統中的「四大組件(Activity,Service,Receiver,Provider)」全都是能夠跨進程通訊的組件(下文中的組件指的都是這些組件)。每一個組件均可以在AndroidManifest中配置到一個指定的進程。Android系統其實不容許應用本身管理本身進程的生命週期。可是因爲咱們只要Start一個Intent啓動屬於那個進程的組件,就能啓動那個進程。再用Java通常的進程操做API,好比System.exit()
方法就能殺死一個進程。因爲這些很容易作到的特色,讓不少Android開發覺得本身能夠管理進程。實際上這樣理解Android進程是不對的。Android系統對進程的設計是這樣的,系統收到須要用到某個組件的請求時(好比start Activity或bind Service),就會檢查這個組件在AndroidManifest中註冊的進程是否已經啓動了。若是這個進程尚未啓動,系統就會首先啓動這個進程,而後構造一個應用註冊的Application對象,調用Application對象的attachBaseContext()
方法。而後構造全部註冊了應該在這個進程的ContentProvider,初始化它們,調用它們的onCreate()
方法。確保後面全部組件,包括尚未被調用的Application對象的onCreate()
方法都能正常使用這個進程的全部ContentProvider。而後再調用Application對象的onCreate()
方法。最後纔開始初始化原本須要用到這個進程的組件。因此,進程的啓動是根據組件的需求啓動的。進一步的,這種設計下理應讓進程的結束也根據組件的需求。實際上也是這樣的,當這個進程中的全部組件都再也不被須要時,好比Activity finish了,或者Service stop了,或者沒有任何bind了,都會讓系統認爲這個進程沒有存在的必要了。這時系統就會決定回收進程、殺死進程了。固然系統還會在一些「必要」的時刻直接回收進程,好比內存不足等,或者內存不足首先回收了不在前臺的Activity、Service等,進而致使進程符合了前面說的條件,再也不被須要了。所以,當咱們用System.exit()
等方法關閉進程時,或者遭遇了Crash,系統是不會認爲進程再也不須要了的。大概是爲了不死循環Crash,Crash的組件會被系統認爲再也不須要。不過若是這個進程還有其餘組件處於活躍狀態,或者Activity棧中有多個Activity,最上面的Activity Crash了,它下面壓着的Activity就應該露出了成爲活躍的了。這種狀況下,因爲系統認爲其餘組件仍是須要這個進程的,就會將進程的建立流程從新走一遍,啓動應該活躍的組件。因此,這裏要理解好,組件對於系統來講不是咱們常見的「對象」的概念,它不在本身運行的進程內存中表示和記錄,而是在系統管理進程中以記錄的形式記錄的。這些組件中以Activity最爲特殊,在編寫Activity的時候,不能將Activity簡單思考爲一個對象。要進一步理解,Activity是有持久化狀態的,這些狀態就是經過savedInstanceState來表達的。因此,Activity和Service最大的區別不是Activity有界面而Service沒有界面,Activity和Service最大的區別是Activity是有狀態的,Service是無狀態的。github
將組件放置在單獨的進程中有不少優勢,基本上都是圍繞進程具備單獨的內存資源的。對於插件框架來講,有兩點十分必要。一是插件通常都是熱更新的,質量上要求可能會下降一些,一旦出現Crash不會影響其餘進程的組件。好比說在宿主的主進程顯示一個大廳界面,其中某個按鈕跳轉到插件。插件在單獨進程啓動後若是出現Crash,宿主的大廳界面不會受到任何影響。若是插件也在宿主的主進程,就會致使大廳界面也會因進程重啓而從新建立。二是Android的JVM虛擬機不支持Native動態庫反加載,因此在同一個進程中相同so庫的不一樣版本即不能同時加載,也不能換着加載,會形成插件和宿主存在so庫衝突。框架
多進程也帶來更多複雜性,就是它的缺點了。好比,跨進程調用的全部參數都必須是可序列化對象;跨進程通訊時對面的進程可能沒有啓動,也可能已經死了;跨進程通訊出現異常,整個跨進程調用的堆棧不會是連着的,並且異常對象一般是不能序列化跨進程傳輸的。如何控制插件進程退出或重啓供另外一業務使用。另外,進程的啓動速度也比較慢。ide
主要基於以上兩點,Shadow設計的插件框架基本模型是:Manager、LoadParameters、Loader三個部分。其中Manager工做在宿主進入插件的入口界面所在進程,負責下載插件、安裝插件,而後將插件信息封裝在LoadParameters中控制Loader啓動插件。LoadParameters是一個可序列化的結構體,能夠跨進程傳輸。Loader工做在插件進程,負責將插件免安裝的運行起來,解決插件框架的核心問題。ui
Shadow中有一個叫作PluginProcessService
的Service是跨進程設計的關鍵部分,咱們簡稱它PPS。操作系統
PPS有多個做用:插件
咱們前面複習過,進程的啓動必須由一個組件觸發。那麼一個沒有界面的Service就是一個不錯的選擇,由於咱們一般要對插件進行「預加載」,可能會靜默啓動插件的Application對象,或啓動插件的Service等。還有要想讓系統知道這個插件進程是有用的,就必須有活躍的組件在這個進程。咱們的插件中的組件全都是沒有安裝的組件,系統都不知道他們存在,確定不能靠它們了。靠插件的殼子代理組件也不行,由於咱們是一個全動態插件框架,那些殼子代理組件也是插件的一部分,尚未加載呢,因此也不能靠它們。這就須要有一個專門負責啓動插件進程的Service,因此它就叫PluginProcessService
了。Service的Bind語義在這裏也很正常,Manager就以Bind的方式啓動這個PPS,直到宿主認爲再也不須要這個插件了,再經過Manager unbind這個PPS。Manager經過Bind拿到的Binder就是PPSController,經過這個PPSController操做PPS,讓宿主得以使用「插件服務」。因此PPS是一個貨真價實的Service。設計
選擇Service來觸發啓動插件進程還有一個緣由是,咱們若是想讓插件進程的插件Service能像正常Service同樣跨進程通訊,就必須在插件進程至少有一個真的註冊在宿主中的Service。這涉及一個Binder的基本知識,就是Binder是一箇中心化的跨進程通訊框架。每個Binder都分本地端和遠程端,本地端實現功能,遠程端供其餘進程調用功能。直接實現的Binder天然就是本地端了,而遠程端怎麼實現呢?實際上把一個本地Binder經過另外一個已經存在遠程端的Binder跨進程傳輸一下,就自動把這個本地Binder送到Binder的中心管理器中註冊並生成遠程端了,新生成的遠程端就經過那個已經存在的Binder的遠程端輸出出來了。這裏可能天然會想到第一個Binder哪裏來的的問題,簡單說就是第一個Binder在設計中特殊處理了,詳細的設計能夠自行Google一下。因此,要想插件Service能正常跨進程工做,就要把插件Service的Binder經過一個已經存在的Binder傳輸一次。所以,最簡單的辦法就是經過PPS的Binder傳輸一次。代理
因此,咱們將Loader自己也設計成了一個插件Service(即dynamic-loader
)。由於全動態的設計中,宿主中的代碼不會直接操做Loader,真正操做Loader的是動態實現的Manager。所以Loader和Manager都是動態實現,Loader上的接口就不必在PPS上固定寫死了。PPS上只保留了加載Runtime和Loader的必要方法。Loader自己的Binder先經過PPS跨進程通訊到Manager進程,從而使Loader的Binder成了跨進程的Binder。而後Loader上再暴露的bindPluginService
方法再將插件Service的Binder經過Loader的Binder跨進程傳輸其餘進程,就是的插件Service真正能夠面向其餘進程工做起來了。咱們的插件Service實現就是這麼簡單。能夠看出來Shadow的插件Service是沒有單獨的代理殼子Service的,只依賴一個PPS就實現了不限數量的插件Service支持。
這是由於這些Binder跨進程調用都是有可能會失敗的,失敗了不能粗暴的Crash。因此,PPS和dynamic-loader
的Binder都是半手工寫的Binder。半手工就是用aidl先生成代碼,再複製出來添加自定義可序列化Exception的能力。實現Manager跨進程操做Loader能夠Catch異常。
因爲全動態的設計,在一個宿主中能夠有多個Manager實現。一個Manager實現也能夠同時操做多個PPS,只須要繼承PPS註冊在不一樣的進程中就能夠了。因爲Loader、Runtime也是動態的,因此不一樣的插件進程可使用不一樣版本的Loader實現。
Shadow的Sample中還有咱們本身的業務中,都對殼子代理組件指定了進程名。對咱們業務來講,這些殼子其實是舊框架遺留在宿主中被Shadow複用的。實際上開發完Shadow的PPS,咱們就意識到,這些殼子組件應該是能夠應用android:multiprocess
特性的。
根據文檔: developer.android.com/guide/topic…
android:multiprocess爲true時,Activity和Provider的行爲是啓動Intent時處於哪一個進程,就將被啓動的Activity或Provider啓動在哪一個進程。
既然PPS已經決定了插件進程,由PPS啓動插件Activity就可使殼子工做在PPS所在進程了。這樣能夠在同一個宿主中應用多個插件進程時少註冊一些殼子組件。
歡迎你們實驗一下而後提一個PR來改進這個問題。
PS: 新註冊的掘金帳號,發文章曝光量很低。選擇來掘金分享也是但願吸引更多開發者關注到Shadow。因此請你們支持一下,點個贊提升點個人掘力值,以便更好的繼續分享。