1. 程式人生 > >IPC(中)-程序間通訊方式詳解

IPC(中)-程序間通訊方式詳解

IPC(中)

1 Android中IPC方式

在第一篇IPC(上)中我們已經介紹了IPC的基礎知識:序列化和Binder,本篇將詳細介紹各種跨程序通訊方式.具體有如下幾種:

  • Intent中extras傳遞

  • 共享檔案

  • Binder

  • ContentProvider

  • Socket

1.1 Bundle

四大元件中的三大元件(Activity,Service,Receiver)都是支援在Intent中傳遞Bundle資料的,由於Bundle實現了Parcelable介面,所以他可以方便在不同程序間傳輸,所以在我們開啟另一個程序的Activity,Service,Receiver時候,就可以使用Bundle的方式來

,但是有一點需要注意,我們在Bundle中的資料必須可以被序列化,比如基本資料型別,實現了Parcelable介面的物件,實現了Serializable介面的物件等等,具體支援型別如下

如果是Bundle不支援的型別我們無法通過它在程序間通訊.但有的時候可以適當改變下實現方式來解決問題,比如A程序進行計算,得到結果後給到B程序,但是結果的資料型別Bundle不支援傳遞,那麼這個時候我們可以將計算過程放在B程序的後臺服務中,然後當需要計算的時候A程序通過Intent告知B程序的Service開始計算了,由於Service在B程序所以可以很方便的拿到資料,這樣就成功避免了程序間通訊的問題.

1.2 使用檔案共享

共享檔案也是一種程序間通訊的方式,兩個程序通過讀/寫同一個檔案來交換資料,交換資訊除了文字資訊外,還可以序列化物件到檔案在從另一個程序中讀取這個物件,但是有一點需要注意,Android基於Linux,併發讀/寫檔案沒有限制,當兩個執行緒同時寫檔案的時候可能會出現問題,這裡尤其需要注意.下面是序列化物件到檔案共享資料的栗子

這次我們在MainActivity中,序列化一個User物件到檔案,在另一個程序執行的SecondActivity中反序列化,看物件的屬性值是否相同.

// MainActivity
public void serialize(View v) {
        User user = new
User("zhuliyuan", 22); try { File file = new File(getCacheDir(), "user.txt"); FileOutputStream fos = new FileOutputStream(file); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(user); fos.close(); oos.close(); } catch (Exception e) { e.printStackTrace(); } } //SecondActivity public void deserialize(View v) { File file = new File(getCacheDir(), "user.txt"); if (file.exists()) { try { FileInputStream fis = new FileInputStream(file); ObjectInputStream ois = new ObjectInputStream(fis); User user = (User) ois.readObject(); fis.close(); ois.close(); Log.i("yyjun", user.toString()); } catch (Exception e) { e.printStackTrace(); } } }

下面看下日誌

可以發現SecondActivity成功恢復了User資料,這裡雖然資料相同但是和之前MainActivity的User物件並不是同一個.

通過文字共享這種方式共享資料對文字格式沒有要求,只要雙方按約定好格式即可.但是也有侷限性當併發讀/寫的時候讀取的檔案可能不是最新的,如果併發寫就更加的嚴重了,要儘量避免這種情況或者使用執行緒同步來限制併發.通過上面分析我們可以知道,檔案共享方式適合在對資料同步要求不高的程序間進行通訊,並且需要妥善處理併發問題.

當然SharedPreferences是個特例,它通過鍵值對方式儲存資料,在底層上採用xml檔案來儲存鍵值對,一般情況每個應用的SharedPreferences檔案目錄位於/data/data/package name/shared_prefs目錄下.從本質上來說SharedPreferences也是屬於檔案的一種,但是由於系統對它的讀寫有一定的快取策略,所以記憶體中會有一份SharedPreferences快取,而在多程序模式下,系統對他讀寫變得不可靠,當高併發時候有很大機率丟失資料,因為,在多程序通訊的時候最好不要使用SharedPreferences.

api文件中也明確指出了這一點

1.3 使用Messenger

Messenger可以翻譯為信使,通過它可以在不同程序中傳遞Message物件,在Message中放 入我們想傳遞的資料,就可以實現資料的程序間傳遞了.Messenger底層實現是AIDL,這個可以通過構造方法初見端倪

public Messenger(Handler target) {
        mTarget = target.getIMessenger();
    }

public Messenger(IBinder target) {
        mTarget = IMessenger.Stub.asInterface(target);
    }

是不是可以明顯看出AIDL的痕跡,既然這麼像那我們就通過原始碼來分析下,車總是要發的不過且慢,我覺得咱們先看下Messenger的栗子熱熱身,在來分析更好

實現步驟如下

  1. 服務端程序

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

  2. 客戶端程序

    客戶端程序中,首先需要繫結服務端service,繫結成功後用服務端返回的IBinder物件建立一個Messenger,通過這個Messenger向服務端傳送訊息,傳送的訊息型別為Message物件,如果需要服務端能夠迴應客戶端,就必須和服務端一樣,建立一個Handle並建立一個新的Messenger,並把這個Messenger物件通過Message的replyTo引數傳遞給服務端,服務端就可以通過replyTo引數迴應客戶端.

下面先來個簡單的栗子,此慄中服務端無法迴應客戶端

在service中建立一個handle,然後new一個Messenger將Handle作為引數傳入,再在onBind方法中返回Messenger底層的Binder

public class MessengerService extends Service {

    private static final String TAG = "MessengerService";

    private static class MessengerHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case Constants.TYPE_MSG_FROM_CLIENT:
                    Log.i(TAG, "receiver client msg " + msg.getData().getString("msg"));
                    break;
            }
        }
    }

    private Messenger mMessenger = new Messenger(new MessengerHandler());

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return mMessenger.getBinder();
    }
}

在註冊service讓其在單獨的程序

<service android:name=".MessengerService"
            android:process=":remote"/>

接下來客戶端實現,先繫結MessengerService服務,在根據服務端返回的Binder物件建立Messenger並使用此物件向服務端傳送訊息.

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    private Messenger mMessenger;

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mMessenger = new Messenger(service);
            Message msg = Message.obtain(null, Constants.TYPE_MSG_FROM_CLIENT);
            Bundle bundle = new Bundle();
            bundle.putString("msg", "this is client msg");
            msg.setData(bundle);
            try {
                mMessenger.send(msg);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        bindService(new Intent(this, MessengerService.class), mConnection, BIND_AUTO_CREATE);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbindService(mConnection);
    }
}

最後執行,看一下日誌,很顯然服務端收到了客戶端的訊息

01-18 09:49:01.823 31013-31013/? I/MessengerService: receiver client msg this is client msg

通過上面栗子可以看出,Messenger中進行資料傳輸必須將資料放入Message中,而Messenger和Message都實現了Parcelable介面,因此可以跨程序傳輸.簡單來說Message中所支援的資料型別就是Messenger支援的資料型別,而Message中能作為載體傳送資料的只有what,arg1,arg2,obj,replyTo,而obj在同一程序中是很實用的,但是程序間通訊的時候,在Android2.2以前obj不支援跨程序傳遞,2.2以後僅僅支援系統實現的Parcelable介面的物件才能通過它來傳遞,也就等於我們自定義的類即使實現了parcelable也無法通過obj傳遞,但是不要方,我們還有Bundle可以支援大量的資料傳遞.

具體在Message的obj欄位的註釋可以窺探一二.

可以看到在跨程序傳遞的時候,obj只支援非空的系統實現Parcelable介面的資料,要想傳遞其他資料使用setData,也就是Bundle方式,Bundle中可以支援大量的資料型別.

上面只能客戶端向服務端傳送資訊,但有的時候我們還需要能夠迴應客戶端,下面就介紹如何實現這種效果.還是上面的栗子只是稍微改下,當服務端接受到客戶端訊息後回覆客戶端接受成功.

首先我們修改下客戶端,為了接受服務端傳送訊息,客戶端也需要一個接受訊息的Messenger和Handler

private Messenger clientMessenger = new Messenger(new ClientHandler());

private static class ClientHandler extends Handler {
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case Constants.TYPE_MSG_FROM_SERVICE:
                Log.i(TAG, msg.getData().getString("reply"));
                break;
        }
    }
}

還有一點就是客戶端傳送訊息的時候,需要把接受服務端回覆的messenger通過message的reply帶到服務端

private ServiceConnection mConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        mMessenger = new Messenger(service);
        Message msg = Message.obtain(null, Constants.TYPE_MSG_FROM_CLIENT);
        Bundle bundle = new Bundle();
        bundle.putString("msg", "this is client msg");
        msg.setData(bundle);
        msg.replyTo = clientMessenger;
        try {
            mMessenger.send(msg);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {

    }
};

然後是服務端的修改,需要修改MessengerHandler,當收到訊息後,立即給客戶端回覆

private static class MessengerHandler extends Handler {
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case Constants.TYPE_MSG_FROM_CLIENT:
                Log.i(TAG, "receiver client msg " + msg.getData().getString("msg"));

                Message serviceMsg = Message.obtain(null, Constants.TYPE_MSG_FROM_SERVICE);
                Bundle bundle = new Bundle();
                bundle.putString("reply", "收到了");
                serviceMsg.setData(bundle);
                try {
                    msg.replyTo.send(serviceMsg);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
                break;
        }
    }
}

執行後檢視日誌

01-18 14:00:13.873 29745-29745/? I/MessengerService: receiver client msg this is client msg

01-18 14:00:13.877 29715-29715/? I/MainActivity: 收到了

到這裡Messenger程序間通訊介紹完了,這裡給出一張Messenger工作原理圖方便理解

栗子完了,該開車了,後面的趕緊的上車
現在我們從Messenger原始碼看看為啥說他是AIDL實現的,我們先隨著服務端Messenger流程來分析.

  1. 建立Messenger在構造方法中傳入Handler

  2. 在onBind中返回mMessenger.getBinder()

那麼這裡我們先看到Messenger引數為Handler的構造方法

可以看到mTarget = target.getIMessenger()
那我們來到Handler中檢視getIMessenger()方法

可以發現返回值為MessengerImpl,那我們再來看MessengerImpl,發現他就是Handler中一個內部類

看到IMessenger.Stub類後不知道有沒有想到點什麼,我稍微提示下在IPC上中我們使用AIDL的時候是不是在服務中寫一個內部類實現了xxx.Stub,這裡其實是一個套路等於Handler中有一個實現好的類MessengerImpl實現了send方法.如果你實在感覺不出啥這裡附上AIDL簡單使用連結仔細看看AIDL使用的流程,應該就能體會到.

那麼看到現在我們已經知道mTarget就是MessengerImpl,也就是我們AIDL使用時候在服務端實現xxx.Stub類,然後在看我們在onBind中返回的mMessenger.getBinder()

再往下追我沒找到MessengerImpl的asBinder方法,但是猜也能猜到asBinder就是返回的xxx.Stub而Stub繼承的Binder你要問我為啥能猜到你可以在看看IPC(上)中2.3.3Binder這一節關於AIDL生成類的分析

所以Service中Messenger的過程分析完發現其實跟AIDL幾乎一模一樣

  • 建立一個Messenger就等於AIDL中實現xxx.stub,

  • onBind中返回mMessenger.getBinder()就等於AIDL中在onBind返回我們實現的xxx.Stub類

再來根據客戶端流程的分析

  1. 繫結服務端,用服務端返回的IBinder物件建立一個Messenger

  2. 然後用Messenger向服務端傳送資料

繫結服務端跟aidl一樣不做分析,用服務端返回的IBinder物件建立一個Messenger

耶嘿,是不是感覺又似曾相識,沒錯AIDL中我們是xxx.Stub.asInterface(service)拿到服務端的介面,這裡我們在Messenger構造方法中通過IMessenger.Stub.asInterface(target)拿到Handler替我們實現好的MessengerImpl

然後呼叫send傳送資料,

這個還是跟AIDL完全一致呼叫服務端實現介面的方法傳送資料.
經過上面分析應該能完全明白為啥說Messenger底層是AIDL實現.

1.4 使用AIDL

在上面我們介紹了Messenger來進行程序間通訊,可以發現Messenger是序列的方式處理客戶端發來的訊息,如果有大量訊息同時傳送到服務端,那麼如果還是隻能一個個處理就太不合適了,並且很多時候我們需要跨程序呼叫服務端方法,這時候用Messenger就無法做到了,但是我們可以使用AIDL來實現,AIDL也是Messenger的底層實現,因此Messenger本質上也是AIDL,只不過系統為我們做了封裝方便呼叫而已.接下來介紹使用AIDL來進行程序間通訊的流程,分為客戶端和服務端.

  1. 服務端
    首先建立一個Service來監聽客戶端的連線請求,然後建立一個AIDL檔案,將要給客戶端的介面在AIDL檔案中宣告,然後在Service實現AIDL檔案生成的類.最後在onBind方法返回實現的類.

  2. 客戶端
    首先繫結服務端的Service,將連線成功後返回的Binder物件轉換成AIDL介面所屬類,接著就可以呼叫AIDL中的方法了.

上面描述的是AIDL的使用過程,在IPC(上)中我們已經講過,這次我們會對其中的細節和難點進行詳細的介紹.並完善在IPC(上)Binder那一節中提供的栗子.

1 AIDL介面的建立

首先看AIDL介面的建立,如下所示,我們建立了一個字尾為AIDL的檔案,在裡面聲明瞭一個介面和兩個方法

// IBookManager.aidl
package com.zly.www.ipc2;

// Declare any non-default types here with import statements
import com.zly.www.ipc2.Book;

interface IBookManager {
    List<Book> getBookList();
    void addBook(in Book book);
}

在AIDL中並不是所有型別都可以使用,具體可以使用的型別如下

  • 基本資料型別(int,long,char,boolean,double等)

  • String 和 CharSequence;

  • List 中的所有元素都必須是以上列表中支援的資料型別、其他 AIDL 生成的介面或您宣告的可打包型別。 可使用List泛型(例如,List< String >)。另一端實際接收的具體類始終是 ArrayList,生成的方法中支援的型別是 List 介面。

  • Map 中的所有元素都必須是以上列表中支援的資料型別、其他 AIDL 生成的介面或您宣告的可打包型別。 不支援Map泛型(如 Map< String,Integer > 形式的 Map)。 另一端實際接收的具體類始終是 HashMap,生成的方法中支援的型別是 Map 介面。

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

  • AIDL: 所有的AIDL介面本身也是可以在AIDL檔案中使用的

以上6種資料型別就是AIDL所支援的所有型別,其中自定義的Parcelable物件和AIDL物件必須要顯示的import進來,不管他們是否和當前AIDL檔案位於同一包內.

另一個需要注意的地方是,如果AIDL檔案中使用了自定義的類,那麼它必須繼承Parcelable,因為Android系統可通過它將物件分解成可編組到各程序的原語.並且必須新建一個和它同名的AIDL檔案,並在其中宣告它為Parcelable型別,在上面IBookManager.aidl中我們用到了Book這個類,所以我們需要建立Book.aidl,並新增如下內容

package com.zly.www.ipc2;
parcelable Book;

除此在外,AIDL中除了基本資料型別,其他型別的引數必須標上方向:in,out或者inout,in表示輸入型引數,out表示輸出型引數,inout表示輸入輸出型引數.至於區別有點長下一段講解.我們要根據實際需要去指定型別,不能一概使用out或者inout,因為這在底層有開銷,最後AIDL介面中只支援方法,不支援宣告靜態常亮,這一點有別於傳統介面.

接下來解釋in,out,inout的意義
AIDL中的定向 tag 表示了在跨程序通訊中資料的流向,其中 in 表示資料只能由客戶端流向服務端, out 表示資料只能由服務端流向客戶端,而 inout 則表示資料可在服務端與客戶端之間雙向流通。其中,資料流向是針對在客戶端中的那個傳入方法的物件而言的。in 為定向 tag 的話表現為服務端將會接收到一個那個物件的完整資料,但是客戶端的那個物件不會因為服務端對傳參的修改而發生變動;out 的話表現為服務端將會接收到那個物件的的空物件,但是在服務端對接收到的空物件有任何修改之後客戶端將會同步變動;inout 為定向 tag 的情況下,服務端將會接收到客戶端傳來物件的完整資訊,並且客戶端將會同步服務端對該物件的任何變動。其實具體的原因在AIDL生成的類中一看便知這裡不再贅述.

為了方便AIDL開發,建議把所有和AIDL相關的類和檔案放入一個包,這樣把整個包複製到客戶端比較方便.需要注意的是,AIDL的包結構在服務端和客戶端要保持一致,否則會出錯,因為客戶端需要反序列化服務端中的AIDL介面相關的所有類,如果類的完整路徑不一樣的話,就無法成功反序列化,程式也就無法正常的執行.

2 遠端服務端Service實現

接下來我們就要實現AIDL介面了,程式碼如下

public class BookManagerService extends Service {

    private static final String TAG = "BookManagerService";

    private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<>();

    private Binder mBinder = new IBookManager.Stub() {
        @Override
        public List<Book> getBookList() throws RemoteException {
            return mBookList;
        }

        @Override
        public void addBook(Book book) throws RemoteException {
            mBookList.add(book);
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        mBookList.add(new Book(1, "Android"));
        mBookList.add(new Book(2, "Ios"));
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }


}

首先在onCreate中初始化添加了兩本圖書的資訊,然後建立一個Binder物件並在onBind中返回它,這個物件繼承自IBookManager.Stub並實現了它內部的AIDL方法,這個過程之前講過這裡不再介紹,注意這裡採用了CopyOnWriteArrayList,這個CopyOnWriteArrayList支援併發讀/寫.在Binder那節我們說過,AIDL方法是在服務的Binder執行緒池中執行的,因此在多個客戶端同時連線的時候,會存在多個執行緒同時訪問的情況,所以我們要在AIDL中處理執行緒同步,而我們這裡直接使用CopyOnWriteArrayList來進行自動的執行緒同步.

前面我們說過,AIDL中能夠使用的List只有ArrayList,但這裡我們使用了CopyOnWriteArrayList(它不是繼承的ArrayList),為什麼可以正常工作,這個因為AIDL中所支援的是抽象的List介面,因此雖然服務端返回的是CopyOnWriteArrayList,但在Binder中會按照List的規範去訪問資料並最終形成一個新的ArrayList傳遞給客戶端,所以我們在服務端採用CopyOnWriteArrayList是完全可以.

現在我們需要註冊BookManager讓它執行在獨立的程序中.

<service android:name=".BookManagerService"
    android:process=":remote"/>

3 客戶端的實現

首先繫結遠端服務,繫結成功後將服務端返回的Binder物件轉換成AIDL介面,然後就可以通過這個介面呼叫服務端的遠端方法了,程式碼如下

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IBookManager bookManager = IBookManager.Stub.asInterface(service);

            try {
                List<Book> list = bookManager.getBookList();
                Log.i(TAG, "query book list, list type:" + list.getClass().getCanonicalName());
                Log.i(TAG, "query book list:" + list.toString());
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        bindService(new Intent(this, BookManagerService.class), mConnection, BIND_AUTO_CREATE);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbindService(mConnection);
    }
}

繫結成功後,會通過bookManager呼叫getBookList方法,然後列印獲得的圖書資訊.這裡有一點需要注意,服務端的方法可能需要很久才能執行完畢,所以在UI執行緒執行就有可能ANR,這裡之所以這麼寫是為了方便演示.

執行後日志如下

01-20 08:28:36.341 13073-13073/com.zly.www.ipc2 I/MainActivity: query book list, list type:java.util.ArrayList
01-20 08:28:36.341 13073-13073/com.zly.www.ipc2 I/MainActivity: query book list:[Book{bookId=1, bookName='Android'}, Book{bookId=2, bookName='Ios'}]

可以發現,雖然我們在服務端返回的是CopyOnWriteArrayList型別,但是客戶端收到的仍然是ArrayList型別,這也證實了我們前面所說的另一端接受的實際型別始終是ArrayList,第二行說明客戶端成功得到了服務端的資訊.

這就已經是一次完整的AIDL進行IPC的過程,但是還沒完下面繼續介紹AIDL中常見的難點,我們接著呼叫另一個方法addBook,我們在客戶端新增一本書,然後在獲取一次,看看程式是否正常工作.還是上面的程式碼,客戶端在服務連線後,在onServiceConnected中做如下改動

IBookManager bookManager = IBookManager.Stub.asInterface(service);

try {
    List<Book> list = bookManager.getBookList();
    Log.i(TAG, "query book list:" + list.toString());

    Book newBook = new Book(3, "android精通到跑路");
    bookManager.addBook(newBook);
    Log.i(TAG, "add book:" + newBook.toString());

    List<Book> newList = bookManager.getBookList();
    Log.i(TAG, "query book list:" + newList.toString());

} catch (RemoteException e) {
    e.printStackTrace();
}

很顯然我們成功的向服務端添加了一本Android從精通到跑路

/MainActivity: query book list:[Book{bookId=1, bookName='Android'}, Book{bookId=2, bookName='Ios'}]
01-20 09:10:24.345 26474-26474/? I/MainActivity: add book:Book{bookId=3, bookName='android精通到跑路'}
01-20 09:10:24.345 26474-26474/? I/MainActivity: query book list:[Book{bookId=1, bookName='Android'}, Book{bookId=2, bookName='Ios'}, Book{bookId=3, bookName='android精通到跑路'}]

現在我們增加需求的難度,使用者不想時不時的查詢圖書列表了,於是,他去問圖書館能不能有新書直接告訴我.這個時候應該能馬上想到,這是一種典型的觀察者模式,每個感興趣的使用者都觀察新書,當新書到的時候,圖書館就通知每個對這本書感興趣的使用者.下面我們就這個情況來模擬,首先我們需要一個AIDL介面,每個使用者都實現這個介面並且有圖書館提醒新書到了的功能.之所以選擇AIDL介面而不是普通介面,是因為AIDL中不支援普通介面,這裡我們建立一個IOnNewBookArrivedListener.aidl檔案,我們期望的是,當服務端有新書來的時候,通知所有申請提醒功能的使用者,從程式上說就是呼叫所有IOnNewBookArrivedListener物件中的OnNewArrived方法,並把新書作為引數傳遞給客戶端.

// IOnNewBookArrivedListener.aidl
package com.zly.www.ipc2;

// Declare any non-default types here with import statements
import com.zly.www.ipc2.Book;
interface IOnNewBookArrivedListener {
    void onNewBookArrived(in Book newbook);
}

除了新加AIDL介面,我們還要在原有IBookManager.aidl中新增兩個方法分別是申請提醒和取消提醒,這裡需要注意即使在同一個包中加入AIDL也是需要import語句的.例如下面的import com.zly.www.ipc2.IOnNewBookArrivedListener;

// IBookManager.aidl
package com.zly.www.ipc2;

// Declare any non-default types here with import statements
import com.zly.www.ipc2.Book;
import com.zly.www.ipc2.IOnNewBookArrivedListener;
interface IBookManager {
    List<Book> getBookList();
    void addBook(in Book book);
    void registerListener(IOnNewBookArrivedListener listener);
    void unregisterListener(IOnNewBookArrivedListener listener);
}

接著服務端Service實現也要稍微修改,主要是我們新加方法的實現,同時在BookManagerService中還開啟了一個執行緒,每個5秒就像書庫中增加一本新書並通知所有感興趣使用者,程式碼如下

public class BookManagerService extends Service {
    private static final String TAG = "BookManagerService";

    private AtomicBoolean mIsServiceDestoryed = new AtomicBoolean(false);

    private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<>();
    private CopyOnWriteArrayList<IOnNewBookArrivedListener> mListenerList = new CopyOnWriteArrayList<>();

    private Binder mBinder = new IBookManager.Stub() {
        @Override
        public List<Book> getBookList() throws RemoteException {
            return mBookList;
        }

        @Override
        public void addBook(Book book) throws RemoteException {
            mBookList.add(book);
        }

        @Override
        public void registerListener(IOnNewBookArrivedListener listener) throws RemoteException {
            if (!mListenerList.contains(listener)) {
                mListenerList.add(listener);
                Log.i(TAG, "registerListener success");
            } else {
                Log.i(TAG, "already exists");
            }
            Log.i(TAG, "registerListener size:" + mListenerList.size());
        }

        @Override
        public void unregisterListener(IOnNewBookArrivedListener listener) throws RemoteException {
            if (mListenerList.contains(listener)) {
                mListenerList.remove(listener);
                Log.i(TAG, "unregister listener success");
            } else {
                Log.i(TAG, "no found, can not unregister");
            }
            Log.i(TAG, "unregisterListener current size:" + mListenerList.size());
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        mBookList.add(new Book(1, "Android"));
        mBookList.add(new Book(2, "Ios"));
        new Thread(new ServiceWorker()).start();
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        mIsServiceDestoryed.set(true);
    }

    private void onNewBookArrived(Book book) {
        mBookList.add(book);
        for (int i = 0; i < mListenerList.size(); i++) {
            IOnNewBookArrivedListener listener = mListenerList.get(i);
            Log.i(TAG, "onNewBookArrived, notify listener:" + listener);
            try {
                listener.onNewBookArrived(book);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    }

    private class ServiceWorker implements Runnable {

        @Override
        public void run() {
            while (!mIsServiceDestoryed.get()) {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int bookId = mBookList.size() + 1;
                Book newBook = new Book(bookId, "new book#" + bookId);
                onNewBookArrived(newBook);
            }
        }
    }
}

最後,我們還需要改下客戶端程式碼,主要兩個方面:首先客戶端需要註冊IOnNewBookArrivedListener到服務端,同時在Activity退出的時候登出,另一個,當有新書的時候,服務端會回撥客戶端的IOnNewBookArrivedListener物件的OnNewBookArrived方法,但是這個方法是在客戶端的BInder執行緒池中執行的,因此,為了便於進行UI操作,我們需要一個Handler可以將其切換到客戶端的主執行緒中去執行.客戶端程式碼修改如下.

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";
    private static final int MESSAGE_NEW_BOOK_ARRIVED = 1;

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MESSAGE_NEW_BOOK_ARRIVED:
                    Log.i(TAG, "receive new book :" + msg.obj);
                    break;
            }
        }
    };

    private IBookManager mRemoteBookManager;
    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IBookManager bookManager = IBookManager.Stub.asInterface(service);
            mRemoteBookManager = bookManager;
            try {
                List<Book> list = bookManager.getBookList();
                Log.i(TAG, "query book list:" + list.toString());

                Book newBook = new Book(3, "android精通到跑路");
                bookManager.addBook(newBook);
                Log.i(TAG, "add book:" + newBook.toString());

                List<Book> newList = bookManager.getBookList();
                Log.i(TAG, "query book list:" + newList.toString());

                bookManager.registerListener(mOnNewBookArrivedListener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            mRemoteBookManager = null;
            Log.i(TAG, "binder died");
        }
    };

    private IOnNewBookArrivedListener mOnNewBookArrivedListener = new IOnNewBookArrivedListener.Stub() {
        @Override
        public void onNewBookArrived(Book newbook) throws RemoteException {
            mHandler.obtainMessage(MESSAGE_NEW_BOOK_ARRIVED, newbook).sendToTarget();
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        bindService(new Intent(this, BookManagerService.class), mConnection, BIND_AUTO_CREATE);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mRemoteBookManager != null && mRemoteBookManager.asBinder().isBinderAlive()) {
            Log.i(TAG, "unregister listener:" + mOnNewBookArrivedListener);
            try {
                mRemoteBookManager.unregisterListener(mOnNewBookArrivedListener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        unbindService(mConnection);
    }
}

執行程式,看下日誌,客戶端的確收到了服務端每隔5s一次的新書推送

01-20 13:57:41.676 365-365/com.zly.www.ipc2 I/MainActivity: receive new book :Book{bookId=4, bookName='new book#4'}
01-20 13:57:46.677 365-365/com.zly.www.ipc2 I/MainActivity: receive new book :Book{bookId=5, bookName='new book#5'}

但是到此還沒有結束,AIDL遠不止這麼簡單,目前還有些難點我們還未涉及到.接下來繼續飆車.

從上面程式碼可以發現,當MainActivity關閉時,我們會在onDestory中去解除註冊到服務端的listener,就相當於我們不在需要新書提醒了,那我們按back退出MainActivity在檢視日誌.

01-20 14:02:48.584 498-510/com.zly.www.ipc2:remote I/BookManagerService: no found, can not unregister
01-20 14:02:48.584 498-510/com.zly.www.ipc2:remote I/BookManagerService: unregisterListener current size:1

從上面日誌可以看出,在解除註冊過程中,服務端竟然無法找到我們之前註冊的那個listener,但是我們在客戶端註冊和解除註冊傳遞的明明是同一個物件,仔細想想你就會發現,其實這是必然的,這種解除註冊的方式在日常開發中經常用到,但是在多程序的開發中卻無法奏效,因為Binder會把客戶端傳遞過來的物件重新轉化成一個新的物件,雖然我們註冊和登出都傳的同一個物件,但別忘了物件是不能跨程序傳遞的,物件傳輸本質上都是反序列化的過程,這就是為什麼AIDL中自定義物件都必須實現Parcelable介面的原因,那麼到底我們該怎麼辦呢,答案是使用RemoteCallbackList.接下來詳細分析.

RemoteCallbackList是系統專門提供的用於刪除跨程序Listener的介面.RemoteCallbackList是一個泛型,支援管理任意的AIDL介面,這點從他的宣告就可以看出,因為所有的AIDL介面都繼承自IInterface介面,在前面Binder那節有講過.

public class RemoteCallbackList<E extends IInterface>

它的工作原理很簡單,它內部有一個Map結構專門來儲存所有的AIDL回撥,這個Map的key是IBinder型別,value是Callback型別,如下

ArrayMap<IBinder, Callback> mCallbacks
            = new ArrayMap<IBinder, Callback>();

其中Callback中封裝了遠端listener,當客戶端註冊listener的時候,它會把這個listener的資訊存入mCallbacks中,其中key和value通過如下方式獲得

IBinder key = callback.asBinder();
Callback value = new Callback(callback, cookie);

到這裡,應該明白了,雖然說多次跨程序傳遞客戶端的同一個物件會服務端會生成不同物件,但是這些新生成物件有個共同點,就是他們底層的Binder物件是同一個,利用這個特點,我們就可以實現上面的功能.當客戶端登出的時候,我們只需要遍歷服務端所有listener,找出那個和登出listener具有相同Binder物件的服務端listener並把它刪除掉,這就是RemoteCallbackList為我們做的事情.同時RemoteCallbackList還有一個很有用的功能,就是當客戶端程序終止後,它能夠自動移除客戶端所註冊的listener,另外,RemoteCallbackList內部實現了執行緒同步的功能,所以我們使用它來註冊和登出時候,不需要做額外的執行緒同步,下面就來演示如何使用.

我們要對BookManagerService做一些修改,首先要建立一個RemoteCallbackList物件來代替之前的CopyOnWriteArrayList.

    private RemoteCallbackList<IOnNewBookArrivedListener> mListenerList = new RemoteCallbackList<>();

然後修改registerListener和unregisterListener這兩個介面的實現

@Override
public void registerListener(IOnNewBookArrivedListener listener) throws RemoteException {
    mListenerList.register(listener);
}

@Override
public void unregisterListener(IOnNewBookArrivedListener listener) throws RemoteException {
    mListenerList.unregister(listener);
}

接下來修改onNewBookArrived方法,當有新書的時候我們就需要通知所有註冊的listener

private void onNewBookArrived(Book book) {
    mBookList.add(book);
    int n = mListenerList.beginBroadcast();
    for (int i = 0; i < n; i++) {
        IOnNewBookArrivedListener listener = mListenerList.getBroadcastItem(i);
        if(listener != null){
            try {
                listener.onNewBookArrived(book);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    }
    mListenerList.finishBroadcast();
}

BookManagerService修改完畢了,為了方便我們驗證程式的功能,我們還需要新增一些log,在註冊和登出後我們分別列印所有listener數量,如果正常的話那麼註冊後是1,登出後是0,我們再次執行下看看日誌.可以發現RemoteCallback完全可以完成跨程序的登出功能.

01-20 15:20:29.090 28830-28842/? I/BookManagerService: registerListener, current size: 1
01-20 15:20:36.479 28830-28842/com
            
           

相關推薦

IPC()-程序通訊方式

IPC(中) 1 Android中IPC方式 在第一篇IPC(上)中我們已經介紹了IPC的基礎知識:序列化和Binder,本篇將詳細介紹各種跨程序通訊方式.具體有如下幾種: Intent中extras傳遞 共享檔案 Binder ContentPr

4種程序通訊方式

     程序間通訊有4種方式,以下從簡單到複雜的方式出場:1.管道(pipe)     管道是一種具有兩個端點的通訊通道,一個管道實際上就是隻存在在記憶體中的檔案,對這個檔案操作需要兩個已經開啟檔案進行,他們代表管道的兩端,也叫兩個句檳,管道是一種特殊的檔案,不屬於一種檔案系統,而是一種獨立的檔案系統,有自

一個小Demo來理解關於IPC程序通訊的aidl

專案地址: Server端程式碼:Server端程式碼連結 Client端程式碼:Client端程式碼連結 1、IPC的基本要求 IPC(Inter-Process Communication)程序間通訊是要在兩個相互獨立的程序之間進行資訊的傳遞,在Android中每個程序都會被分配

嵌入式Linux併發程式設計,程序通訊方式,System V IPC,訊號燈集,建立/開啟semget(),初始化semctl(),P/V操作semop(),sembuf結構體定義

文章目錄 1,System V IPC - 訊號燈 2,System V IPC - 訊號燈特點 3,System V訊號燈使用步驟 3.1,訊號燈建立/開啟 semget() 3.2,訊號燈初始化 semctl()

嵌入式Linux併發程式設計,程序通訊方式,System V IPC,訊息佇列,開啟/建立msgget(), 傳送訊息msgsnd(),格式,接收訊息msgrcv(),控制訊息佇列 msgctl()

文章目錄 1,訊息佇列 2,訊息佇列結構 3,訊息佇列使用步驟 3.1,開啟/建立訊息佇列 msgget() 3.1.1,開啟/建立訊息佇列---示例msgget() 3.2,向訊息佇列傳送訊息 msgs

嵌入式Linux併發程式設計,程序通訊方式,System V IPC物件,ftok(),共享記憶體使用步驟,建立shmget(),對映shmat(),撤銷對映shmdt(),控制shmctl(),注意

文章目錄 1,System V IPC 2,使用IPC物件的大致流程 3,生成KEY值ftok() ftok示例 4,共享記憶體 4.1,共享記憶體使用步驟 4.2,共享記憶體建立 shmget()

Android——IPC機制(二)程序通訊方式

在上一章中,我們已經介紹了IPC的幾個基礎知識:序列化和Binder,本章將詳細介紹各種跨程序同行方式。具體的方式有很多,比如可以通過在Intent中附加extras來傳遞資訊,或者通過共享檔案的方式來共享資料,還可以採用Binder的方式來跨程序通訊,另外Co

JavaScript定義類的方式

轉載:JavaScript中定義類的方式詳解 JavaScript中定義類的方式詳解 這篇文章主要介紹了JavaScript中定義類的方式,結合例項形式分析了JavaScript實現面向物件類的定義及使用相關技巧,並附帶了四種JavaScript類的定義方式,需要的朋友可以參考下 本文例

linux 程序通訊方式

1 無名管道通訊 無名管道( pipe ):管道是一種半雙工的通訊方式,資料只能單向流動,而且只能在具有親緣關係的程序間使用。程序的親緣關係通常是指父子程序關係。 2 高階管道通訊 高階管道(popen):將另一個程式當做一個新的程序在當前程式程序中啟動,則它算是當前程式的子程序

Linux下程序通訊方式 - UNIX Domain Socket

概述 Linux下程序通訊方式有很多,比較典型的有套接字,平時比較常用的套接字是基於TCP/IP協議的,適用於兩臺不同主機上兩個程序間通訊, 通訊之前需要指定IP地址. 但是如果同一臺主機上兩個程序間通訊用套接字,還需要指定ip地址,有點過於繁瑣. 這個時候就需要用到UNIX Domain Sock

面試必問:程序與執行緒的異同以及程序通訊方式

秋招面試必問的題目,感覺今年被問了差不多10次了。 1.程序與執行緒 程序:具有獨立功能的程式關於某個資料集合上的一次執行活動。 執行緒:程序的一個實體。 比喻:一列火車是一個程序,火車的每一節車廂是執行緒。 2.程序與執行緒的聯絡 ①一個執行緒只能屬於一個程序,一個程序

Pandas 的四中索引方式

Pandas 中的四中索引方式詳解 總結 Pandas 中的四中索引方式詳解 第一次使用pandas 對於其中的Series 和DataFrame 的索引弄暈了

HTTP協議報文、工作原理及Java的HTTP通訊技術

一、web及網路基礎       1、HTTP的歷史            1.1、HTTP的概念:         &nb

最快的程序通訊方式你get了麼

  前言:天下武功為快不破!在資訊爆炸、快速發展的新時代...,扯遠了...。程序間通訊方式有很多,但最快的方式你知道麼?由我娓娓道來...   一、共享記憶體方式   主角閃亮登場了,噔噔瞪...,最快的方式就是共享記憶體了。實現共享記憶體的方式主要有兩種: 儲存對映I/O mmap函式實現 s

Linux程序通訊機制----訊息佇列

一、什麼是訊息 訊息(message)是一個格式化的可變長的資訊單元。訊息機制允許由一個程序給其它任意的程序傳送一個訊息。當一個程序收到多個訊息時,可將它們排成一個訊息佇列。 1、訊息機制的資料結構 (1)訊息首部 記錄一些與訊息有關的資訊,如訊息的型別、大小、

(三)程序通訊方式-----訊息佇列

訊息佇列 訊息佇列,是訊息的連結表,存放在核心中。一個訊息佇列由一個識別符號(即佇列ID)來標識。使用者程序可以向訊息佇列新增訊息,也可以向訊息佇列讀取訊息。 同管道檔案相比,訊息佇列中的每個訊息指定特定的訊息型別,接收的時候可以不需要按照佇列次序讀取,可以根據自定義型別

Linux程序通訊方式一:有名管道FIFO

有名管道 我們經常把FIFO稱為有名管道(命名管道)。使用它可以實現兩個不相干的程序之間的通訊。它雖然被稱之為檔案,但是管道檔案在磁碟上只有一個inode結點,這個ionde結點指向的是記憶體中的一塊區域,當A程序建立並使用有名管道時,直接把資料寫入記憶體中,而B程序也是直

嵌入式Linux併發程式設計,程序通訊方式,無名管道,無名管道特點,無名管道建立pipe(),獲取管道大小,管道斷裂

1,Linux下的程序間通訊機制 Linux下的程序間通訊機制 應用 早期UNIX程序間通訊方式(很多是從Unix繼承的) 無名管道(pipe) 本地通訊,用於一臺計算機內部不同程序之間的通訊

嵌入式Linux併發程式設計,程序通訊方式,有名管道,有名管道特點,有名管道建立mkfifo()

1,有名管道的特點 對應管道檔案,可用於任意程序之間進行通訊:有名管道建立好之後,在系統中有實際的檔案和有名管道對應,任意兩個程序可以通過路徑和檔名指定同一個有名管道進行通訊 開啟管道時可指定讀寫方式:有名管道用open()開啟的時候可以指定不同的讀寫方

嵌入式Linux併發程式設計,程序通訊方式,訊號,訊號機制,檢視新號kill -l,常用訊號,發訊號命令kill [-signal] pid、killall [-u user | prog]

1,訊號機制 訊號是在軟體層次上對中斷機制的一種模擬,是一種非同步通訊方式 (一個程序在任何條件下,都可以隨時的接收訊號,不需要其他的處理) Linux核心通過訊號通知使用者程序,不同的訊號型別代表不同的事件 Linux對早期的unix訊號機制進行了擴充