Android開發藝術探索 - 第2章 IPC機制

1.多進程模式

給四大組件指定android:process。命名方式:“:”開頭,該process將是application的私有進程,最終的process名會加上package前綴;小寫字母開頭,該process將是全局進程,多個application可以共享該process。
多進程造成的問題:

  • 靜態成員和單例模式失效
  • 線程同步機制失效
  • SharedPreferences可靠性下降
  • Application多次創建:進程->虛擬機->application->四大組件

2.IPC基礎

  • Serializable
    實現了該接口即可被序列化和反序列化。通過指定serialVersionUID,保證被反序列化的數據和預期的對象類型相符:
ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;

如果不指定serialVersionUID,Java運行時會根據類的各個方面屬性計算出該值。但是因爲不同的運行時環境的差異,計算結果可能會不同,導致拋出InvalidClassException異常,所以建議直接指定該值。並且設置爲private,因爲該值對子類來說沒有使用價值。
static成員屬於類,不屬於對象,不會被序列化;被transient修飾的成員不會序列化。

  • Parcelable
    實現了該接口,可以被Intent和Binder傳遞。Intent、Bundle、Bitmap實現了該接口,List、Map維護的類型實現了該接口也可以被序列化。
  • Serializable & Parcelable對比
    前者的序列化和反序列化需要大量的IO操作,開銷大;後者效率高。前者多用於本地存儲或者網絡傳輸,後者主要用於內存的序列化。
  • Binder
    Binder學習指南
    • Binder通信模型

      1. ServiceManager建立(建立通信錄);首先有一個進程向驅動提出申請爲SM;驅動同意之後,SM進程負責管理Service(注意這裏是Service而不是Server,因爲如果通信過程反過來的話,那麼原來的客戶端Client也會成爲服務端Server)不過這時候通信錄還是空的,一個號碼都沒有。
      2. 各個Server向SM註冊(完善通信錄);每個Server端進程啓動之後,向SM報告,我是zhangsan, 要找我請返回0x1234(這個地址沒有實際意義,類比);其他Server進程依次如此;這樣SM就建立了一張表,對應着各個Server的名字和地址;就好比B與A見面了,說存個我的號碼吧,以後找我撥打10086;
      3. Client想要與Server通信,首先詢問SM;請告訴我如何聯繫zhangsan,SM收到後給他一個號碼0x1234;Client收到之後,開心滴用這個號碼撥通了Server的電話,於是就開始通信了。
    • Binder通信過程

      1. 首先,Server進程要向SM註冊;告訴自己是誰,自己有什麼能力;在這個場景就是Server告訴SM,它叫zhangsan,它有一個object對象,可以執行add 操作;於是SM建立了一張表:zhangsan這個名字對應進程Server;
      2. 然後Client向SM查詢:我需要聯繫一個名字叫做zhangsan的進程裏面的object對象;這時候關鍵來了:進程之間通信的數據都會經過運行在內核空間裏面的驅動,驅動在數據流過的時候做了一點手腳,它並不會給Client進程返回一個真正的object對象,而是返回一個看起來跟object一模一樣的代理對象objectProxy,這個objectProxy也有一個add方法,但是這個add方法沒有Server進程裏面object對象的add方法那個能力;objectProxy的add只是一個傀儡,它唯一做的事情就是把參數包裝然後交給驅動。(這裏我們簡化了SM的流程,見下文)
      3. 但是Client進程並不知道驅動返回給它的對象動過手腳,畢竟僞裝的太像了,如假包換。Client開開心心地拿着objectProxy對象然後調用add方法;我們說過,這個add什麼也不做,直接把參數做一些包裝然後直接轉發給Binder驅動
      4. 驅動收到這個消息,發現是這個objectProxy;一查表就明白了:我之前用objectProxy替換了object發送給Client了,它真正應該要訪問的是object對象的add方法;於是Binder驅動通知Server進程,調用你的object對象的add方法,然後把結果發給我,Sever進程收到這個消息,照做之後將結果返回驅動,驅動然後把結果返回給Client進程;於是整個過程就完成了。

      另外,Android系統實現這種機制使用的是代理模式, 對於Binder的訪問,如果是在同一個進程(不需要跨進程),那麼直接返回原始的Binder實體;如果在不同進程,那麼就給他一個代理對象(影子);我們在系統源碼以及AIDL的生成代碼裏面可以看到很多這種實現。
      我們爲了簡化整個流程,隱藏了SM這一部分驅動進行的操作;實際上,由於SM與Server通常不在一個進程,Server進程向SM註冊的過程也是跨進程通信,驅動也會對這個過程進行暗箱操作:SM中存在的Server端的對象實際上也是代理對象,後面Client向SM查詢的時候,驅動會給Client返回另外一個代理對象。Sever進程的本地對象僅有一個,其他進程所擁有的全部都是它的代理。
      一句話總結就是:Client進程只不過是持有了Server端的代理;代理對象協助驅動完成了跨進程通信。

    • Binder的含義
      Server進程裏面的Binder對象指的是Binder本地對象,Client裏面的對象值得是Binder代理對象;在Binder對象進行跨進程傳遞的時候,Binder驅動會自動完成這兩種類型的轉換;因此Binder驅動必然保存了每一個跨越進程的Binder對象的相關信息;在驅動中,Binder本地對象的代表是一個叫做binder_node的數據結構,Binder代理對象是用binder_ref代表的;有的地方把Binder本地對象直接稱作Binder實體,把Binder代理對象直接稱作Binder引用(句柄),其實指的是Binder對象在驅動裏面的表現形式。

    • AIDL封裝下的Binder通信流程:
      0. 首先聲明一個AIDL接口,以此編譯出一個IInterface接口類型的java類。繼承該類中的Stub子類,實現相應的方法,然後在server端的Service的onBind方法中返回該類的實例。該實例即爲Binder實體。

      1. 當client成功綁定到了server的Service之後,在onServiceConnected方法中,根據參數中的Binder對象(對於遠程server,這裏一定是Binder.BinderProxy類型,即Binder引用),調用YourInterfaceName.Stub.asInterface((IBinder)service)方法,得到我們可以去執行遠程方法的IInterface類型的實例。
      2. asInterface方法中,針對傳入的同一進程的Binder實體,返回的實則爲該Binder實體強轉類型後的IInterface實例,之後的方法調用則爲簡單的進程內直接調用;針對傳入的是不同進程的Binder引用(BinderProxy類型),則會通過代理模式,將其包裹後返回一個Stub.Proxy類型的實例。
      3. 當client通過這個Proxy去執行遠程方法時,會創建兩個Parcel對象,分別用於傳遞參數和返回值,然後執行BinderProxy#transact方法,進而通過JNI執行本地方法。該函數通過ioctl系統調用,client進程陷入內核態,調用遠程方法的線程掛起,Binder驅動完成一系列操作後喚醒server進程,調用其Binder實體的onTranscat方法。
      4. 在client的BinderProxy#transact方法中,會傳遞一個調用號,用來指定需要調用的方法。Binder#onTranscat中則根據這個調用號,確定需要執行的本地方法,取出參數,進行計算,將參數寫回。最終返回結果會傳遞給Binder驅動。
      5. Binder驅動喚醒client的調用線程,BinderProxy#transact繼續執行,取出其中的執行結果。
      6. 一次Binder RPC完成了。

3.IPC方式

  • Bundle
    通過在Intent中添加Bundle實現不同進程間組件的數據傳輸。
  • 文件共享
    數據同步,併發讀寫問題。
  • Messenger 基於AIDL
    Messenger每次處理一個client的請求,適用於service不需要處理併發的情況。使用步驟:
    1. The service implements a Handler that receives a callback for each call from a client.
    2. The service uses the Handler to create a Messenger object (which is a reference to the Handler).
    3. The Messenger creates an IBinder that the service returns to clients from onBind().
    4. Clients use the IBinder to instantiate the Messenger (that references the service’s Handler), which the client uses to send Message objects to the service.
    5. The service receives each Message in its Handler—specifically, in the handleMessage() method.
    6. If you want the service to respond, you need to also create a Messenger in the client. When the client receives the onServiceConnected() callback, it sends a Message to the service that includes the client’s Messenger in the replyTo parameter of the send() method.
  • AIDL 基於Binder
    1. 定義接口(server)
      1. 創建.aidl文件
        AIDL支持的數據類型:Java原始數據類型、String、CharSequeue、List、Map。List和Map中必須是受支持的數據類型、其他AIDL接口或者是可序列化類型。同時另一端收到的實際類型對應的是ArrayList和HashMap。
        • 以上未列出的類型,需要使用import導入;
        • 非原始類型的參數需要指定方向標記(in/out/inout),以減少開銷。原始類型爲in,不能更改。
        • aidl文件中不在import和package間的註釋,會包含在生成的IBinder接口中。
        • 可以在其中定義String和int常量。如const int VERSION = 1;.
        • 可以定義方法的索引值,transact在調用時即通過該值定位到具體方法。void method() = 10;
        • 使用@nullable指定可爲空的參數或返回值。
      2. 實現接口
        sdk工具根據aidl文件生成一個同名的java文件。實現該文件中的Stub子類的方法,該Stub類中聲明瞭需要實現的接口。
        • 該RPC調用是同步調用。執行該調用時,主調方會掛起。保證主調方從子線程調用,避免因此可能造成的ANR問題。
        • 引發的異常不會回傳給調用方。
      3. 向客戶端公開該接口
        擴展Service,實現onBind(),返回上一步創建的Binder實例。
    2. 調用IPC方法(client)
      1. 在項目 src/ 目錄中加入 .aidl 文件。
      2. 聲明一個 IBinder 接口實例(基於 AIDL 生成)。
      3. 實現 ServiceConnection。
      4. 調用 Context.bindService(),以傳入您的 ServiceConnection 實現。
      5. 在您的 onServiceConnected() 實現中,您將收到一個 IBinder 實例(名爲 service)。調用 YourInterfaceName.Stub.asInterface((IBinder)service),以將返回的參數轉換爲 YourInterface 類型。
      6. 調用您在接口上定義的方法。您應該始終捕獲 DeadObjectException 異常,它們是在連接中斷時引發的;這將是遠程方法引發的唯一異常。
      7. 如需斷開連接,請使用您的接口實例調用 Context.unbindService()。
    3. 一些說明
      • AIDL中的方法如果接受Bundle類型的參數,在讀取其中數據之前,要設置其classloader,否則會拋出ClassNotFoundException:
        bundle.setClassLoader(getClass().getClassLoader());
      • AIDL可以實現遠程回調,即server端調用client端實現的在server端定義的listener。方法步驟:
        • server端創建一個AIDL接口,類似一般的listener。
        • server端聲明並實現register和unregister方法,用於client向server端註冊listener。
        • client在bind到server的時候,即onServiceConnection中,register一個本地listener到server。
        • server在需要回調的時刻,直接調用遠端client的listener的方法。
      • 遠程回調的原理:
        重點在於server端的register方法。
        在Stub.Proxy#register中:
        _data.writeStrongBinder((((listener != null)) ? (listener.asBinder()) : (null)));
        mRemote.transact(Stub.TRANSACTION_register, _data, _reply, 0);
        
        client將本地的listener傳遞給register,該listener因爲本身即是Binder,所以同樣可以傳遞給遠端的server使其可以訪問本地的方法。
        在Stub.register#onTransact中:
        _arg0 = com.wdjhzw.androidts.aidl.IListener.Stub.asInterface(data.readStrongBinder());
        this.register(_arg0);
        
        server端註冊的listener即是遠端client的listener實體的引用,即BindProxy。在需要執行遠程回調的時候,通過該BindProxy最終由Binder驅動執行遠程方法。
      • 遠程回調解除:因爲server端register的是Binder引用,而client要unregister的是Binder實體,所以unregister被調用時,server不能直接解除client的listener。所以要有一種機制,保存Binder實體到Binder引用的映射,在需要unregister的時候,根據傳遞過來的Binder引用的Binder實體去解除之前註冊的Binder引用。RemoteCallbackList就做了上面這件事:
        • register和unregister很簡單,傳遞進listener即可;
        • server進行回調時,按照固定模板:
        int i = callbacks.beginBroadcast();
        while (i > 0) {
            i--;
            try {
                callbacks.getBroadcastItem(i).somethingHappened();
            } catch (RemoteException e) {
                // The RemoteCallbackList will take care of removing
                // the dead object for us.
            }
        }
        callbacks.finishBroadcast();
        
      • Binder線程池
        Binder實體中的方法,運行在其進程的Binder線程池中。client調用server的方法時,會被掛起。所以:
        • 因爲不是主線程,其中不能直接進行UI操作。
        • 因爲不是主線程,其中可以直接執行耗時操作。
        • 因爲主調方會被掛起,所以無論client還是server,執行遠程方法時,儘量不要在主線程中直接調用。
      • 權限驗證
        Binder中提供了getCallingPid()getCallingUid()方法,在service中可以檢查對端進程是否具有權限。具體來說,可以在Binder的onTransact方法中,先驗證權限,驗證不通過則直接返回。
  • ContentProvider 基於Binder
    ContentProvider用於應用間的複雜數據共享,如果是在應用內部,使用SQLite數據庫即可。
    開發步驟:
    1. 設計原始數據。ContentProvider以兩種方式提供數據:文件數據和結構化數據。前者如圖片、視頻,文件存儲在私有空間,通過提供文件句柄供訪問者使用。後者的數據一般存儲在SQLite數據庫中。
    2. 實現ContentProvider類。
      實現六個方法:query()/insert()/delete()/update()/getType()/onCreate()
      注意事項:
      • ContentResolver在嘗試訪問ContentProvider時,纔會被創建。
      • 除了onCreate(),其他5個方法都運行在Binder線程池中。如果ContentProvider用於同一個進程(應用),5個方法運行在主線程。
      • CRUT操作可由多個線程同時調用,因此必須是線程安全的。如果內部維護了一個SQLite數據庫,則因爲其本身實現了線程同步,所以是安全的。如果有多個數據庫,則需要另外處理。
      • onCreate中要避免執行長時間的操作。您只應在此方法中執行運行快速的初始化任務,並將數據庫創建和數據加載推遲到提供程序實際收到數據請求時進行。
    3. 設計內容URI。
      UriMatcher
  • Socket

4.Binder連接池

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章