1. 程式人生 > >android IPC(程序間通訊)機制

android IPC(程序間通訊)機制

一、多程序的情況

1.       一個應用因為某些原因自身需要採用多程序模式實現。

可能是某些模組由於特殊原因需要執行在單獨的執行緒中;或是為了增大一個應用可以使用的記憶體空間。android對單個應用使用的最大記憶體做了限制,早期一些版本是16M,不同裝置有不同的大小。

2.       當前應用需要向其他應用獲取資料。

二、Android中開啟多程序模式

在Android中使用多程序只有一種方式,那就是給四大元件(Activity、Service、Receiver、Contentprovider)在AndroidMenifest中指定android:process屬性,如下圖所示。


圖 1. Android元件指定所在程序的方式

        以上分別給ProcessOneActivity和ProcessTwoActivity指定process屬性。假設當前的應用包名為“com.example.aidltest”,當ProcessOneActivity啟動時,系統會為它建立一個單獨的程序,經常名為 “com.example.aidltest:remote”;當ProcessTwoActivity啟動時,系統也會為它建立一個單獨的程序,程序名為“com.example.application2”。同時入口Activity是MainActivity,沒有為它指定process屬性,那麼它執行在預設程序中,預設程序的程序名是包名。

        可以在android studio中的DDMS檢視中檢視程序資訊,也可是用shell來檢視,命令為:adb shell ps或者 adb shell ps | grep com.example.aidltest。

        以上兩種方式為ProcessOneActivity和ProcessTwoActivity指定process屬性。這兩種方式的區別有兩方面:首先“:”的含義是指要在當前的程序名前面加上當前的包名,第二種是一種完整的命名方式,不會附加包名資訊;其次,程序名以“:”開頭的程序屬於當前應用的私有程序,其他應用的元件不可以和它跑在同一個程序中,而程序名不以“:”開頭的程序屬於全域性程序,其他應用可以通過ShareUID方式和它跑在同一個程序中。

        Android系統為每個應用分配一個唯一的UID,具有相同UID的應用才能共享資料,兩個應用通過UID跑在一塊的要求是:這兩個應用有相同的UID並且簽名相同。

三、多程序模式的執行機制

用一個例子來說明多程序模式的執行機制。新建一個UserManager類,這個類有一個靜態成員變數,如下所示:


圖 2. UserManager類

       然後在MainActivity的onCreate中把sUserId 賦值為2,然後打印出這個靜態變數的值之後,在啟動ProcessOneActivity,在ProcessOneActivity中再列印一下sUserId的值。按照正常邏輯,靜態變數是可以在所有的地方共享的,並且一處有修改,處處都會同步。但是從列印日誌來看,ProcessOneActivity中打印出來的sUserId值依然是1。

        出現以上問題的原因是ProcessOneActivity執行在獨立的程序中,android中每個程序都分配一個獨立的虛擬機器,不同的虛擬機器在記憶體分配上有不同的地址空間。這就導致在不同的虛擬機器中訪問同一個類的物件會產生多分副本。所以程序“com.example.aidltest:remote”和程序“com.example.aidltest”中都存在一個UserManager類,並且這兩個類是互不干擾的,在一個程序中修改sUserId的值,對其他程序不會造成任何影響。

一般來說,使用多程序會造成如下幾個方面的問題:

(1)靜態成員和單例模式完全失效。

(2)執行緒同步機制完全失效。

(3)SharedPreferences的可靠性下降。

(4)Application會多次建立

第(3)個問題,SharedPreferences不支援兩個程序同時去執行寫操作,否則會導致一定機率的資料丟失。這是因為SharedPreferences的底層是通過讀寫xml檔案來實現的,併發寫顯然是會出現問題的,甚至是併發讀/寫都會出現問題。

第(4)個問題,當一個元件跑在一個新程序中的時候,由於系統要在建立新的程序同時分配獨立的虛擬機器,所以這個過程其實就是啟動一個應用的過程。

執行在不同程序中的元件是屬於兩個不容的虛擬機器和Application的。圖為在application的onCreate方法中列印當前程序的名稱的結果:


圖 3. Application所在的當前程序

從log可以看出,Application執行了三次onCreate,並且每次的程序名都不一樣。這就證實了,在多程序模式中,不同程序的元件的確會擁有獨立的虛擬機器、Application以及記憶體空間。

四、IPC基礎概念介紹

Android實現跨程序通訊的方式有很多,比如通過Intent來傳遞資料,共享檔案和SharePreferences,基於Binder的Messenger和AIDL以及Socket等。為了更好的理解各種IPC方式,介紹一些基礎的概念。

1.       Serializable介面

Serializable是java所提供的一個序列化介面。如果一個類需要序列化,直接實現該介面即可。


圖 4. 物件的序列化和反序列化過程

        可以在類的定義中指定一個序列化標識(並不是必須的)private static final long serialVersionUID = 79248759238475

序列化後的資料中的serialVersionUID 只有和當前類的serialVersionUID 相同才能夠正常地被反序列化。

注:首先,靜態成員變數屬於類不屬於物件,所以不會參與序列化過程;其次,用transient關鍵字標記的成員變數不參與序列化。

1.       Parcelable介面

       只要實現Parcelable介面,一個類的物件就可以實現序列化並可以通過Intent和Binder傳遞。

        Serializable和Parcelable之間如何選取?

        Serializable和Parcelable都可以實現序列化並且都可以用於Intent間的資料傳遞。Serializable是java中的介面,使用簡單但是開銷很大;Parcelable是android中的序列化方式,效率很高,因此適合在android平臺上使用,但是使用起來稍微麻煩;Parcelable主要用在記憶體序列化上。將物件序列化到儲存裝置中或者將物件序列化後通過網路傳輸使用Parcelable過程會稍顯複雜,因此建議使用Serializable。

2.       Binder

       從android應用層來看,Binder是客戶端和服務端進行通訊的媒介,當bindSercive的時候,服務端會返回一個包含了服務端業務呼叫的Binder物件。通過Binder物件,客戶端可以獲取服務端提供的服務或者是資料。這裡的服務包括普通服務和基於AIDL的服務。

android開發中,Binder主要用在Service中,其中普通server中的Binder不涉及程序間通訊,所以較簡單,無法觸及Binder的工作機制。用於程序通訊的Binder底層是基於AIDL的,這裡選擇用AIDL來分析Binder的工作機制。

      新建一個AIDL檔案後,SDK會為我們生產AIDL所對應的Binder類。接下來,新建一個AIDL示例。

新建Book.java、Book.aidl和IBookManager.aidl檔案。

Book.java程式碼如下:


圖 5. Book類

        Book.java是一個表示圖書資訊的類,該類實現了Parcelable介面;Book.aidl是Book類在AIDL中的宣告。IBookManager.aidl是一個介面,裡面有兩個方法getBookList和addBook,分別用來從遠端服務獲取圖書列表和往圖書列表中新增一本書。

       在包 com.example.aidltest下新建aidl檔案,如新建Book.aidl檔案,android studio會自動新建一個aidl資料夾,並在該資料夾下生成一個com.example.aidltest包,然後將Book.aidl檔案放在該包下。(注:因為已經有了Book.java檔案,在新建Book.aidl檔案時androidstudio 會報“Interface Name must be unique”問題,我們可以先命名為其他名字,然後再將aidl檔名字修改為Book.aidl)。


圖 6. aidl目錄

Book.aidl檔案

圖 7. Book.aidl

IBookManager.aidl檔案


圖 8. IBookManager.aidl

雖然IBookManager.aidl和Book.java在同一個包下,但是仍要在IBookManager中匯入Book類。

編譯之後,在build/generated/source/aidl/debug/包名 目錄下會自動生成IBookManager.aidl對應的IBookManager.java類。這個就是系統自動生成的Binder類。下面對IBookManager.java類進行分析。

1)  IBookManager.java繼承了IInterface這個介面,同時它自己也是個介面,所有可以在Binder中傳輸的介面都需要繼承IInterface介面。

2)  聲明瞭getBookList和addBook這兩個方法,這兩個方法就是在IBookManager.aidl檔案中宣告的方法,同時還聲明瞭兩個整型的id分別用來表示這兩個方法,用於標識在transact過程中客戶端所請求的到底是哪個方法。

圖 9. IBookManager.java中函式宣告

1)  聲明瞭一個內部類Stub,這個Stub就是一個Binder類。Stub類有一個內部代理類Proxy,當客戶端和服務端不在同一個程序時,方法呼叫會走transact過程,這個邏輯是由Stub的內部代理類Proxy來完成的。可以知道,這個介面的核心實現就是它的內部類Stub和Stub的內部代理類Proxy,下面詳細解釋這兩個類。

DESCRIPTOR

        Binder的唯一標識,一般用當前Binder的類名標識。

asInterface(android.os.IBinderobj)

       用於將服務端的Binder物件轉化成客戶端所需的AIDL介面型別物件,如果客戶端和服務端位於同一程序,那麼此方法返回的就是服務端的Stub物件本身,否則返回的是系統封裝後的Stub.proxy物件。

asBinder()

      返回當前Binder物件。

onTransact

       該方法執行在服務端中的Binder執行緒池中,當客服端發起跨程序請求時,遠端請求會通過系統底層封裝後交由此方法處理。服務端通過code可以確定客戶端所請求的目標方法是什麼,然後從data中取出目標方法所需要的引數,然後執行目標方法。當目標方法執行完畢後,就向reply中寫入返回值。


圖 10. onTransact實現

Proxy#getBookList和Proxy#addBook

        這兩個方法執行在客戶端,當客戶端遠端呼叫此方法時,首先建立該方法所需要的輸入型Parcel物件_data、輸出型parcel物件_reply和返回物件List;然後把該方法的引數資訊寫入_data;接著呼叫transact方法發起RPC(遠端過程呼叫)請求,同時當前執行緒掛起;然後服務端的onTransact方法會被呼叫,直到RPC過程返回後,當前執行緒繼續執行,並從_reply中取出RPC過程的返回結果。

五、android中的IPC機制

        分別介紹Android中的各種跨程序通訊方式。

1.       使用Bundle

        當在一個程序中啟動另一個程序的Acitivity、Service和Receiver,可以使用Bundle中附加需要傳輸給遠端程序的資訊並通過Intent傳送出去。傳輸的資料必須能夠被序列化,如基本型別、實現了Parcellable介面的兌現、實現了Serializable介面的物件以及一些Android支援的特殊物件。

2.       使用檔案共享

       這種方式對檔案格式沒有具體要求,可以是文字檔案,也可以是XML檔案,只要讀寫雙方預定資料格式即可。SharedPreference是android中提供的輕量級儲存方案。

3、使用Messenger

       Messenger是一種輕量級的IPC方案,底層實現是AIDL。使用Messenger可以簡單地進行程序間通訊。同時,由於它一次處理一個請求,所以在服務端不用考慮執行緒同步的問題。實現一個Messenger有如下幾個步驟,分為服務端和客戶端。

(1)服務端

      首先,在服務端建立一個Service來處理客戶端的連線請求,同時建立一個Handler並通過它來建立一個Messenger物件,然後在Service的onBind中返回這個Messenger物件底層的Binder即可。

圖 11. 使用Messenger通訊時服務端實現

(2)客戶端程序

       客戶端程序中,首先要繫結服務端的Service,繫結成功後用服務端返回的IBinder物件建立一個Messenger,通過這個Messenger就可以向服務端傳送訊息了,傳送訊息為Message物件。同樣,如果需要在服務端能夠迴應客戶端,在服務端還需要建立一個Handler並傳建一個新的Messenger,並把這個Messenger對應通過Message的replyTo引數傳遞給服務端,服務端通過replyTo引數就可以迴應客戶端。

圖 12. 使用Messenger通訊時客戶端實現


圖 13. 客戶端繫結Service

以上程式的功能是:客戶端向服務端傳送了一句話,服務端收到這句話打印出來,然後向客服端傳送已經收到的訊息。

通過上面的例子可以看出,在Messenger中進行資料傳遞必須將資料放入Message中,而Messenger和Message都實現了Parcelable介面,因此可以跨程序傳輸。Message中所支援的資料型別就是Messenger所支援的資料傳輸型別。

下面給出一張Messenger的工作原理圖:


圖 14. Messenger的工作原理圖

注:Messeng服務端先建立一個Service用來監聽客戶端的連線請求,然後是以序列的方式處理客戶端發來的訊息,如果大量的訊息同時發到服務端,服務端仍然只能一個個處理,如果有大量的併發請求,那麼Messenger就不太合適。

4、使用AIDL

首先介紹一下使用AIDL來進行程序間通訊的流程,分為服務端和客戶端兩個方面:

(1)首先建立一個AIDL檔案,將暴露給客戶端的介面在這個AIDL檔案中宣告,最後在Service中實現這個AIDL介面即可。

(2)客戶端需要繫結服務端的Service,繫結成功後,將服務端返回的Binder物件轉化成AIDL介面所屬的型別,接著就可以呼叫AIDL中的方法了。

接下來對其中的細節和難點進行分析介紹:

(1)首先建立一個AIDL檔案IBookManager,在裡面宣告兩個介面方法。

    

圖 15. IBookManager.aidl

AIDL檔案中,不是所有與的資料型別都可以使用,AIDL檔案支援的資料型別有:

1)     基本資料型別(int、long、char、boolean、double等)

2)     List:只支援ArrayList,裡面每個元素都必須能夠被AIDL支援

3)     Map:只支援HashMap,裡面每個元素都必須能夠被AIDL支援,包括key和value。

4)     Parcelable:所有實現了Parcelable介面的物件

5)     AIDL:所有的AIDl介面本身也可以在AIDL檔案中使用。

        自定義的Parcelable物件和AIDL物件必須要顯示import進來,不管他們是否和當前的AIDL檔案位於同一個包內;另一個需要注意的地方是,如果AIDL檔案中用到了自定義的Parcelable物件,那麼必須新建一個和它同名的AIDL檔案,並在其中宣告它為Parcelable型別,例如Book這個類;除此之外,AIDL中出了基本型別,其他型別的引數必須標上方向:in、out或者inout,in表示輸入型引數,out表示輸出型引數,inout表示輸入輸出型引數;最後AIDL中只支援方法,不支援宣告靜態變數。

(2)遠端服務端Service的實現

       首先建立一個Service,稱為BookManagerService,程式碼如下:

圖 16. 使用aidl服務端程式碼

在服務端定義了一個Binder類的物件mBinder,mBinder中實現了抽象類的getBookList和addBook方法。然後在Service的onBind方法中將mBinder返回。

(3)客戶端的實現

客戶端的程式碼如下:


圖 17. 使用aidl客戶端實現

在客戶端通過IBookManager.Stub的asInterface方法得到IBookManager介面物件mRemoteBookManager,所以下面來看一下asInterface的方法實現。


        asInterface方法裡首先進行了驗空,這個很正常。第二步操作是呼叫了queryLocalInterface() 方法,這個方法是 IBinder 接口裡面的一個方法,而這裡傳進來的IBinder 物件就是Service的onBind方法中返回的,從字面意思上看,意思大概是搜尋本地是否有該物件,如果有的話就就返回。第三步是建立了一個物件返回,很顯然,這就是我們的目標。接下來,果斷看看BookManager.Stub.Proxy 類:

       看到這裡,我們幾乎可以確定:Proxy 類確實是我們的目標,客戶端最終通過這個類與服務端進行通訊。接下來看看getBooks()方法具體做了什麼:


在這段程式碼裡有幾個需要說明的地方,不然容易看得雲裡霧裡的:

  1. 關於 _data 與 _reply 物件:一般來說,我們會將方法的傳引數據存入_data 中,而將方法的返回值的資料存入 _reply 中。
  2. 關於 transact() 方法:這是客戶端和服務端通訊的核心方法。呼叫這個方法之後,客戶端將會掛起當前執行緒,等候服務端執行完相關任務後通知並接收返回的 _reply 資料流。關於這個方法的傳參,這裡有兩點需要說明的地方:
  • 方法 ID :transact() 方法的第一個引數是一個方法 ID ,這個是客戶端與服務端約定好的給方法的編碼,彼此一一對應。在AIDL檔案轉化為 .java 檔案的時候,系統將會自動給AIDL檔案裡面的每一個方法自動分配一個方法 ID。
  • 第四個引數:transact() 方法的第四個引數是一個 int 值,它的作用是設定進行 IPC 的模式,為 0 表示資料可以雙向流通,即 _reply 流可以正常的攜帶資料回來,如果為 1 的話那麼資料將只能單向流通,從服務端回來的 _reply 流將不攜帶任何資料。

注:AIDL生成的 .java 檔案的這個引數均為 0。

接下來總結一下在 Proxy 類的方法裡面一般的工作流程:

1)     生成 _data 和 _reply 資料流,並向 _data 中存入客戶端的資料。

2)     通過 transact() 方法將它們傳遞給服務端,並請求服務端呼叫指定方法。

3)     接收 _reply 資料流,並從中取出服務端傳回來的資料。

注意事項:

  1. 另外需要注意的是,當activity繫結service的時候,如果service和activity在一個程序中,那麼service和activity就是在一個執行緒中,所以在service中不要做耗時的操作,以免主執行緒阻塞,出現ANR。客戶端呼叫遠端服務的方法,被呼叫的方法執行在服務端的binder執行緒池中,同時客戶端執行緒會被掛起,這個時候如果服務端方法執行比較耗時的話,就會導致客戶端執行緒長時間的阻塞在這裡,而如果這個客戶端執行緒是UI執行緒的話,就會導致客戶端ANR,因此如果我們明確知道某個遠端方法是耗時的,那麼就要避免在客戶端的UI執行緒中去呼叫。由於客戶端的onServiceConnected和onServiceDisconnected方法都是在UI執行緒中,所以不能在這兩個方法中呼叫service耗時方法。
  2. l如果service端和client端是在同一個程序的話,那麼在兩端之間傳輸的物件都是同一個,也就是說發出端的物件傳到接收端還是它自己。如果不在同一個程序的話,那麼發出端的物件傳到接收端,就會建立一個一模一樣的物件,接收端的物件在接收端的記憶體中,傳送端的物件在傳送端的記憶體中,所以這是兩個物件。設想一種情況,多程序使用AIDL的情況下如果在service端維護一個列表List<Book> books。然後在client端生成一個Book的例項(書名叫book1)傳給service,並加入到books中,那麼按理來說此時service的books中應該有一本書,並且書名也叫book1。然後此時client再把剛剛生成的book例項傳給後臺,判斷service中的books是否存在這本書,books.contains(book),然而會發現返回的是false,並不存在這本書。這裡會覺得很奇怪,明明剛剛已經新增那本書,怎麼可能拿不到。這其實是因為第一次client端將book1傳給service的時候,底層建立了一個新的book2,只是book2和book1的書名一樣,但其實是兩個物件了。所以service的books中持有的其實是book2,然後第二次client端將book1傳給service的時候,其實service拿到的是book3,然後通過book3區去books中找,當然是找不到了。那有什麼辦法解決呢?辦法當然還是有的啦。可以重寫equals方法,通過比較兩本書的bookName是否一樣,如果bookName一樣,則說明是一本書,不一樣則是不一樣的書。如果在service端維護的是IInterface實現類的列表話,可以使用RemoteCallbackList<? implements IInterface> interfaces。RemoteCallbackList的內部有一個Map結構專門用來儲存所有的AIDL回撥。這個Map的Key是IBinder型別,value是Callback型別。

還有其他兩種IPC方式,這裡就不詳細介紹了。

使用ContentProvider

使用Socket