花費2年,字數超一萬最佳總結教你Android多進程,微信微博都在用

目錄java

  1. 前言
  2. 爲何要使用多進程?
  3. 爲何須要「跨進程通信」?
  4. 跨進程通信的方式有哪些?
  5. 使用AIDL實現一個多進程消息推送
  6. 實現思路
  7. 例子具體實現
  8. 知其然,知其因此然。
  9. 跨進程的回調接口
  10. DeathRecipient
  11. 權限驗證
  12. 根據不一樣進程,作不一樣的初始化工做
  13. 總結
  14. 結語

爲何要使用多進程android

  • 對於進程的概念,來到這裏的都是編程修仙之人,就再也不囉嗦了,相信你們倒着、跳着、躺着、各類姿式都能背出來。
  • 相信不少同窗在實際開發中,基本都不會去給app劃分進程,並且,在Android中使用多進程,還可能須要編寫額外的進程通信代碼,還可能帶來額外的Bug,這無疑加大了開發的工做量,在不少創業公司中工期也不容許,這致使了整個app都在一個進程中。

整個app都在一個進程有什麼弊端?git

  • 在Android中,虛擬機分配給各個進程的運行內存是有限制值的(這個值能夠是32M,48M,64M等,根據機型而定),試想一下,若是在app中,增長了一個很經常使用的圖片選擇模塊用於上傳圖片或者頭像,加載大量Bitmap會使app的內存佔用迅速增長,若是你還把查看過的圖片緩存在了內存中,那麼OOM的風險將會大大增長,若是此時還須要使用WebView加載一波網頁,我就問你怕不怕!

微信,微博等主流app是如何解決這些問題的?github

  • 微信移動開發團隊在 《Android內存優化雜談》 一文中就說到:「對於webview,圖庫等,因爲存在內存系統泄露或者佔用內存過多的問題,咱們能夠採用單獨的進程。微信當前也會把它們放在單獨的tools進程中」。

下面咱們使用adb查看一下微信和微博的進程信息(Android 5.0如下版本可直接在「設置 -> 應用程序」相關條目中查看):web

進入adb shell後,使用 「ps | grep 條目名稱」 能夠過濾出想要查看的進程。面試

  • 能夠看到,微信的確有一個tools進程,而新浪微博也有image相關的進程,並且它們當中還有好些其它的進程,好比微信的push進程,微博的remote進程等,這裏能夠看出,他們不僅僅只是把上述的WebView、圖庫等放到單獨的進程,還有推送服務等也是運行在獨立的進程中的。shell

  • 一個消息推送服務,爲了保證穩定性,可能須要和UI進程分離,分離後即便UI進程退出、Crash或者出現內存消耗太高等狀況,仍不影響消息推送服務。編程

可見,合理使用多進程不只僅是有多大好處的問題,我我的認爲並且是頗有必要的。緩存

因此說,咱們最好仍是根據自身狀況,考慮一下是否須要拆分進程。這也是本文的初衷:給你們提供一個多進程的參考思路,在遇到上述問題和場景的時候,能夠考慮用多進程的方法來解決問題,又或者,在面試的時候,跟面試官聊到這方面的知識時候也不至於尷尬。安全

爲何須要「跨進程通信」

  • Android的進程與進程之間通信,有些不須要咱們額外編寫通信代碼,例如:把選擇圖片模塊放到獨立的進程,咱們仍可使用startActivityForResult方法,將選中的圖片放到Bundle中,使用Intent傳遞便可。(看到這裏,你還不打算把你項目的圖片選擇弄到獨立進程麼?)

  • 可是對於把「消息推送Service」放到獨立的進程,這個業務就稍微複雜點了,這個時候可能會發生Activity跟Service傳遞對象,調用Service方法等一系列複雜操做。

  • 因爲各個進程運行在相對獨立的內存空間,因此它們是不能直接通信的,由於程序裏的變量、對象等初始化後都是具備內存地址的,舉個簡單的例子,讀取一個變量的值,本質是找到變量的內存地址,取出存放的值。

  • 不一樣的進程,運行在相互獨立的內存(其實就能夠理解爲兩個不一樣的應用程序),顯然不能直接得知對方變量、對象的內存地址,這樣的話也天然不能訪問對方的變量,對象等。此時兩個進程進行交互,就須要使用跨進程通信的方式去實現。簡單說,跨進程通信就是一種讓進程與進程之間能夠進行交互的技術。

跨進程的通信方式有哪些

  1. 四大組件間傳遞Bundle;
  2. 使用文件共享方式,多進程讀寫一個相同的文件,獲取文件內容進行交互;
  3. 使用Messenger,一種輕量級的跨進程通信方案,底層使用AIDL實現(實現比較簡單,博主開始本文前也想了一下是否要說一下這個東西,最後仍是以爲沒有這個必要,Google一下就能解決的問題,就不囉嗦了);
  4. 使用AIDL(Android Interface Definition Language),Android接口定義語言,用於定義跨進程通信的接口;
  5. 使用ContentProvider,經常使用於多進程共享數據,好比系統的相冊,音樂等,咱們也能夠經過ContentProvider訪問到;
  6. 使用Socket傳輸數據。
  • 接下來本文將重點介紹使用AIDL進行多進程通信,由於AIDL是Android提供給咱們的標準跨進程通信API,很是靈活且強大(貌似面試也常常會問到,可是真正用到的也很少…)。上面所說的Messenger也是使用AIDL實現的一種跨進程方式,Messenger顧名思義,就像是一種串行的消息機制,它是一種輕量級的IPC方案,能夠在不一樣進程中傳遞Message對象,咱們在Message中放入須要傳遞的數據便可輕鬆實現進程間通信。
  • 可是當咱們須要調用服務端方法,或者存在併發請求,那麼Messenger就不合適了。而四大組件傳遞Bundle,這個就不須要解釋了,把須要傳遞的數據,用Intent封裝起來傳遞便可,其它方式不在本文的討論範圍。

下面開始對AIDL的講解,各位道友準備好渡劫了嗎?

使用AIDL使用一個跨進程消息推送

像圖片選擇這樣的多進程需求,可能並不須要咱們額外編寫進程通信的代碼,使用四大組件傳輸Bundle就好了,可是像推送服務這種需求,進程與進程之間須要高度的交互,此時就繞不過進程通信這一步了。下面咱們就用即時聊天軟件爲例,手動去實現一個多進程的推送例子,具體需求以下:

  1. UI和消息推送的Service分兩個進程;
  2. UI進程用於展現具體的消息數據,把用戶發送的消息,傳遞到消息Service,而後發送到遠程服務器;
  3. Service負責收發消息,並和遠程服務器保持長鏈接,UI進程可經過Service發送消息到遠程服務器,Service收到遠程服務器消息通知UI進程;
  4. 即便UI進程退出了,Service仍須要保持運行,收取服務器消息。

實現思路

先來整理一下實現思路:

  1. 建立UI進程(下文統稱爲客戶端);
  2. 建立消息Service(下文統稱爲服務端);
  3. 把服務端配置到獨立的進程(AndroidManifest.xml中指定process標籤);
  4. 客戶端和服務端進行綁定(bindService);
  5. 讓客戶端和服務端具有交互的能力。(AIDL使用);

例子具體實現

爲了閱讀方便,下文中代碼將省略非重點部分,能夠把本文完整代碼Clone到本地再看文章:

github.com/V1sk/AIDL

Step0. AIDL調用流程概覽

開始以前,咱們先來歸納一下使用AIDL進行多進程調用的整個流程:

  1. 客戶端使用bindService方法綁定服務端;
  2. 服務端在onBind方法返回Binder對象;
  3. 客戶端拿到服務端返回的Binder對象進行跨進程方法調用;

整個AIDL調用過程歸納起來就以上3個步驟,下文中咱們使用上面描述的例子,來逐步分解這些步驟,並講述其中的細節。

Step1.客戶端使用bindService方法綁定服務端

1.1 建立客戶端和服務端,把服務端配置到另外的進程

  1. 建立客戶端 -> MainActivity;
  2. 建立服務端 -> MessageService;
  3. 把服務端配置到另外的進程 -> android:process=」:remote」

上面描述的客戶端、服務端、以及把服務端配置到另外進程,體如今AndroidManifest.xml中,以下所示:

開啓多進程的方法很簡單,只須要給四大組件指定android:process標籤。

1.2 綁定MessageService到MainActivity

建立MessageService:

此時的MessageService就是剛建立的模樣,onBind中返回了null,下一步中咱們將返回一個可操做的對象給客戶端。

客戶端MainActivity調用bindService方法綁定MessageService

這一步實際上是屬於Service組件相關的知識,在這裏就比較簡單地說一下,啓動服務能夠經過如下兩種方式:

  1. 使用bindService方法 -> bindService(Intent service, ServiceConnection conn, int flags);
  2. 使用startService方法 -> startService(Intent service);

bindService & startService區別:使用bindService方式,多個Client能夠同時bind一個Service,可是當全部Client unbind後,Service會退出,一般狀況下,若是但願和Service交互,通常使用bindService方法,使用onServiceConnected中的IBinder對象能夠和Service進行交互,不須要和Service交互的狀況下,使用startService方法便可。

正如上面所說,咱們是要和Service交互的,因此咱們須要使用bindService方法,可是咱們但願unbind後Service仍保持運行,這樣的狀況下,能夠同時調用bindService和startService(好比像本例子中的消息服務,退出UI進程,Service仍須要接收到消息),代碼以下:

Stpe2.服務端在onBind方法返回Binder對象

2.1 首先,什麼是Binder?

要說Binder,首先要說一下IBinder這個接口,IBinder是遠程對象的基礎接口,輕量級的遠程過程調用機制的核心部分,該接口描述了與遠程對象交互的抽象協議,而Binder實現了IBinder接口,簡單說,Binder就是Android SDK中內置的一個多進程通信實現類,在使用的時候,咱們不用也不要去實現IBinder,而是繼承Binder這個類便可實現多進程通信。

2.2 其次,這個須要在onBind方法返回的Binder對象從何而來?

在這裏就要引出本文中的主題了——AIDL多進程中使用的Binder對象,通常經過咱們定義好的 .adil 接口文件自動生成,固然你能夠走野路子,直接手動編寫這個跨進程通信所需的Binder類,其本質無非就是一個繼承了Binder的類,鑑於野路子走起來麻煩,並且都是重複步驟的工做,Google提供了 AIDL 接口來幫咱們自動生成Binder這條正路,下文中咱們圍繞 AIDL 這條正路繼續展開討論(可不能把人給帶偏了是吧)

2.3 定義AIDL接口

很明顯,接下來咱們須要搞一波上面說的Binder,讓客戶端能夠調用到服務端的方法,而這個Binder又是經過AIDL接口自動生成,那咱們就先從AIDL搞起,搞以前先看看注意事項,以避免出事故:

AIDL支持的數據類型:

  • Java 編程語言中的全部基本數據類型(如 int、long、char、boolean 等等)
  • String和CharSequence
  • Parcelable:實現了Parcelable接口的對象
  • List:其中的元素須要被AIDL支持,另外一端實際接收的具體類始終是 ArrayList,但生成的方法使用的是 List 接口
  • Map:其中的元素須要被AIDL支持,包括 key 和 value,另外一端實際接收的具體類始終是 HashMap,但生成的方法使用的是 Map 接口

其餘注意事項:

  • 在AIDL中傳遞的對象,必須實現Parcelable序列化接口;
  • 在AIDL中傳遞的對象,須要在類文件相同路徑下,建立同名、可是後綴爲.aidl的文件,並在文件中使用parcelable關鍵字聲明這個類;
  • 跟普通接口的區別:只能聲明方法,不能聲明變量;
  • 全部非基礎數據類型參數都須要標出數據走向的方向標記。能夠是 in、out 或 inout,基礎數據類型默認只能是 in,不能是其餘方向。

下面繼續咱們的例子,開始對AIDL的講解~

2.4 建立一個AIDL接口,接口中提供發送消息的方法(Android Studio建立AIDL:項目右鍵 -> New -> AIDL -> AIDL File),代碼以下:

一個比較尷尬的事情,看了不少文章,歷來沒有一篇能說清楚in、out、inout這三個參數方向的意義,後來在stackoverflow上找到能理解答案(stackoverflow.com/questions/4…我翻譯一下大概意思:

  • 被「in」標記的參數,就是接收實際數據的參數,這個跟咱們普通參數傳遞同樣的含義。在AIDL中,「out」 指定了一個僅用於輸出的參數,換而言之,這個參數不關心調用方傳遞了什麼數據過來,可是這個參數的值能夠在方法被調用後填充(不管調用方傳遞了什麼值過來,在方法執行的時候,這個參數的初始值老是空的),這就是「out」的含義,僅用於輸出。
  • 而「inout」顯然就是「in」和「out」的合體了,輸入和輸出的參數。區分「in」、「out」有什麼用?這是很是重要的,由於每一個參數的內容必須編組(序列化,傳輸,接收和反序列化)。in/out標籤容許Binder跳過編組步驟以得到更好的性能。

上述的MessageModel爲消息的實體類,該類在AIDL中傳遞,實現了Parcelable序列化接口,代碼以下:

手動實現Parcelable接口比較麻煩,安利一款AS自動生成插件android-parcelable-intellij-plugin建立完MessageModel這個實體類,別忘了還有一件事要作:」在AIDL中傳遞的對象,須要在類文件相同路徑下,建立同名、可是後綴爲.aidl的文件,並在文件中使用parcelable關鍵字聲明這個類「。代碼以下:

對於沒有接觸過aidl的同窗,光說就能讓人懵逼,來看看此時的項目結構壓壓驚:

咱們剛剛新增的3個文件:

  • MessageSender.aidl -> 定義了發送消息的方法,會自動生成名爲MessageSender.Stub的Binder類,在服務端實現,返回給客戶端調用
  • MessageModel.java -> 消息實體類,由客戶端傳遞到服務端,實現了Parcelable序列化
  • MessageModel.aidl -> 聲明瞭MessageModel可在AIDL中傳遞,放在跟MessageModel.java相同的包路徑下

OK,相信此時懵逼已解除~

2.5 在服務端建立MessageSender.aidl這個AIDL接口自動生成的Binder對象,並返回給客戶端調用,服務端MessageService代碼以下:

MessageSender.Stub是Android Studio根據咱們MessageSender.aidl文件自動生成的Binder對象(至因而怎樣生成的,下文會有答案),咱們須要把這個Binder對象返回給客戶端。

2.6 客戶端拿到Binder對象後調用遠程方法

調用步驟以下:

  1. 在客戶端的onServiceConnected方法中,拿到服務端返回的Binder對象;
  2. 使用MessageSender.Stub.asInterface方法,取得MessageSender.aidl對應的操做接口;
  3. 取得MessageSender對象後,像普通接口同樣調用方法便可。

此時客戶端代碼以下:

在客戶端中咱們調用了MessageSender的sendMessage方法,向服務端發送了一條消息,並把生成的MessageModel對象做爲參數傳遞到了服務端,最終服務端打印的結果以下:

這裏有兩點要說:

  1. 服務端已經接收到客戶端發送過來的消息,並正確打印;
  2. 服務端和客戶端區分兩個進程,PID不同,進程名也不同;

到這裏,咱們已經完成了最基本的使用AIDL進行跨進程方法調用,也是Step.0的整個細化過程,能夠再回顧一下Step.0,既然已經學會使用了,接下來…全劇終。。。

若是寫到這裏全劇終,那跟鹹魚有什麼區別…

知其然,知其因此然

咱們經過上述的調用流程,看看從客戶端到服務端,都經歷了些什麼事,看看Binder的上層是如何工做的,至於Binder的底層,這是一個很是複雜的話題,本文不深究。(若是看到這裏你又想問什麼是Binder的話,請手動倒帶往上看…)

咱們先來回顧一下從客戶端發起的調用流程:

  1. MessageSender messageSender = MessageSender.Stub.asInterface(service);
  2. messageSender.sendMessage(messageModel);

拋開其它無關代碼,客戶端調跨進程方法就這兩個步驟,而這兩個步驟都封裝在 MessageSender.aidl 最終生成的 MessageSender.java 源碼(具體路徑爲:build目錄下某個子目錄,本身找,不爽你來打我啊 )

請看下方代碼和註釋,前方高能預警…

只看代碼的話,可能會有點懵逼,相信結合代碼再看下方的流程圖會更好理解:

從客戶端的sendMessage開始,整個AIDL的調用過程如上圖所示,asInterface方法,將會判斷onBind方法返回的Binder是否存處於同一進程,在同一進程中,則進行常規的方法調用,若處於不一樣進程,整個數據傳遞的過程則須要經過Binder底層去進行編組(序列化,傳輸,接收和反序列化),獲得最終的數據後再進行常規的方法調用。

敲黑板:對象跨進程傳輸的本質就是 序列化,傳輸,接收和反序列化 這樣一個過程,這也是爲何跨進程傳輸的對象必須實現Parcelable接口

跨進程的回調接口

在上面咱們已經實現了從客戶端發送消息到跨進程服務端的功能,接下來咱們還須要將服務端接收到的遠程服務器消息,傳遞到客戶端。有同窗估計會說:「這不就是一個回調接口的事情嘛」,設置回調接口思路是對的,可是在這裏使用的回調接口有點不同,在AIDL中傳遞的接口,不能是普通的接口,只能是AIDL接口,因此咱們須要新建一個AIDL接口傳到服務端,做爲回調接口。

新建消息收取的AIDL接口MessageReceiver.aidl:

接下來咱們把回調接口註冊到服務端去,修改咱們的MessageSender.aidl:

以上就是咱們最終修改好的aidl接口,接下來咱們須要作出對應的變動:

  1. 在服務端中增長MessageSender的註冊和反註冊接口的方法;
  2. 在客戶端中實現MessageReceiver接口,並經過MessageSender註冊到服務端。

客戶端變動,修改MainActivity:

客戶端主要有3個變動:

  1. 增長了messageReceiver對象,用於監聽服務端的消息通知;
  2. onServiceConnected方法中,把messageReceiver註冊到Service中去;
  3. onDestroy時候解除messageReceiver的註冊。

下面對服務端MessageServie進行變動:

服務端主要變動:

  1. MessageSender.Stub實現了註冊和反註冊回調接口的方法;
  2. 增長了RemoteCallbackList來管理AIDL遠程接口;
  3. FakeTCPTask模擬了長鏈接通知客戶端有新消息到達。(這裏的長鏈接能夠是XMPP,Mina,Mars,Netty等,這裏弄個假的意思意思,有時間的話咱開個帖子聊聊XMPP)
  • 這裏還有一個須要講一下的,就是RemoteCallbackList,爲何要用RemoteCallbackList,普通ArrayList不行嗎?固然不行,否則幹嗎又整一個RemoteCallbackList ,registerReceiveListener 和 unregisterReceiveListener在客戶端傳輸過來的對象,通過Binder處理,在服務端接收到的時候實際上是一個新的對象,這樣致使在 unregisterReceiveListener 的時候,普通的ArrayList是沒法找到在 registerReceiveListener 時候添加到List的那個對象的,可是它們底層使用的Binder對象是同一個,RemoteCallbackList利用這個特性作到了能夠找到同一個對象,這樣咱們就能夠順利反註冊客戶端傳遞過來的接口對象了。

  • RemoteCallbackList在客戶端進程終止後,它能自動移除客戶端所註冊的listener,它內部還實現了線程同步,因此咱們在註冊和反註冊都不須要考慮線程同步,的確是個666的類。(至於使用ArrayList的幺蛾子現象,你們能夠本身試試,篇幅問題,這裏就不演示了)

到此,服務端通知客戶端相關的代碼也寫完了,運行結果無非就是正確打印就不貼圖了,能夠本身Run一下,打印的時候注意去選擇不一樣的進程,否則瞪壞屏幕也沒有日誌。

DeathRecipient

你覺得這樣就完了?too young too simple…

不知道你有沒有感受到,兩個進程交互老是以爲缺少那麼一點安全感…好比說服務端進程Crash了,而客戶端進程想要調用服務端方法,這樣就調用不到了。此時咱們能夠給Binder設置一個DeathRecipient對象,當Binder意外掛了的時候,咱們能夠在DeathRecipient接口的回調方法中收到通知,並做出相應的操做,好比重連服務等等。

DeathRecipient的使用以下:

  1. 聲明DeathRecipient對象,實現其binderDied方法,當binder死亡時,會回調binderDied方法;
  2. 給Binder對象設置DeathRecipient對象。

在客戶端MainActivity聲明DeathRecipient:

Binder中兩個重要方法:

  1. linkToDeath -> 設置死亡代理 DeathRecipient 對象;
  2. unlinkToDeath -> Binder死亡的狀況下,解除該代理。

此外,Binder中的isBinderAlive也能夠判斷Binder是否死亡

權限驗證

就算是公交車,上車也得嘀卡對不,若是但願咱們的服務進程不想像公交車同樣誰想上就上,那麼咱們能夠加入權限驗證。

介紹兩種經常使用驗證方法:

  1. 在服務端的onBind中校驗自定義permission,若是經過了咱們的校驗,正常返回Binder對象,校驗不經過返回null,返回null的狀況下客戶端沒法綁定到咱們的服務;
  2. 在服務端的onTransact方法校驗客戶端包名,不經過校驗直接return false,校驗經過執行正常的流程。

自定義permission,在Androidmanifest.xml中增長自定義的權限:

服務端檢查權限的方法:

根據不一樣進程,作不一樣的初始化工做

相信前一兩年不少朋友還在使用Android-Universal-Image-Loader來加載圖片,它是須要在Application類進行初始化的。打個好比,咱們用它來加載圖片,並且還有一個圖片選擇進程,那麼咱們但願分配更多的緩存給圖片選擇進程,又或者是一些其餘的初始化工做,不須要在圖片選擇進程初始化怎麼辦?

這裏提供一個簡單粗暴的方法,博主也是這麼幹的…直接拿到進程名判斷,做出相應操做便可:

每一個進程建立,都會調用Application的onCreate方法,這是一個須要注意的地方,咱們也能夠根據當前進程的pid,拿到當前進程的名字去作判斷,而後作一些咱們須要的邏輯,咱們這個例子,拿到的兩個進程名分別是:

  1. **客戶端進程:**com.example.aidl
  2. 服務端進程:com.example.aidl:remote

總結

  1. 多進程app能夠在系統中申請多分內存,但應合理使用,建議把一些高消耗但不經常使用的模塊放到獨立的進程,不使用的進程可及時手動關閉;
  2. 實現多進程的方式有多種:四大組件傳遞Bundle、Messenger、AIDL等,選擇適合本身的使用場景;
  3. Android中實現多進程通信,建議使用系統提供的Binder類,該類已經實現了多進程通信而不須要咱們作底層工做;
  4. 多進程應用,Application將會被建立屢次;

結語

這篇文章斷斷續續寫了好久,並且我相信真正使用起來的同窗可能很少,選擇這樣一個話題我是吃力不討好… 可是我仍是但願能夠在這裏提供一個完整的解決方案給你們。簡單的多進程使用,並且效果顯著的,好比把圖片選擇和WebView配置到獨立的進程,這個我但願能夠你們行動起來。這篇文章的知識點很是多,理解起來可能不是太容易,若是有興趣,我建議你手動去寫一下,而後不理解的地方,打斷點看看是什麼樣的運行步驟。

對於面試的同窗,若是在面試過程當中說到多進程,跟面試官聊得開,估計也是能加點分的,或者在實際工做中,一些使用多進程能夠更好地解決問題的地方,你能夠在會議中拍桌猛起,跟主管說:「我有一個大膽的想法…」,這樣裝逼也不錯(固然,被炒了的話就不關個人事了…)

文章不易,若是你們喜歡這篇文章,或者對你有幫助但願你們多多點贊轉發關注哦。文章會持續更新的。絕對乾貨!!!

相關文章
相關標籤/搜索