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.zly.www.ipc2:remote I/BookManagerService: unregisterListener, current size: 0

使用RemoteCallbackList,有一點需要注意,雖然名字帶List但是我們無法像List一樣去操作它,遍歷RemoteCallbackList必須按照如下方式進行,其中beginBroadcast和finishBroadcast必須要配對使用.

int n = mListenerList.beginBroadcast();
for (int i = 0; i < n; i++) {
    IOnNewBookArrivedListener listener = mListenerList.getBroadcastItem(i);
    if(listener != null){
        //do something
    }
}
mListenerList.finishBroadcast();

到此aidl基本使用介紹完成,但還有幾點需要說明,我們知道,客戶端調用遠程服務端服務的方法,被調用的方法運行在服務端的Binder線程池中,同時客戶端會被掛起,這個時候如果比較耗時,會導致客戶端當前線程長時間阻塞,如果是ui線程就會ANR,因此當我們知道某個遠程方法是耗時的,那麼就要避免在客戶端ui線程去訪問遠程方法,由於客戶端onServiceConnected和onServiceDisconnected方法都運行在UI線程所以不能直接在裏面調用服務端耗時的方法,另外由於服務端方法本身就運行在服務端的binder線程池中,所以服務端方法本身就可以執行大量耗時操作,這個時候切記不要在服務端方法中開線程去執行異步任務,除非有明確必要,下面我們稍微修改下服務端的getBookList方法,我們假定這個方法是耗時的那麼我們可以這麼做.

@Override
public List<Book> getBookList() throws RemoteException {
    SystemClock.sleep(5000);
    return mBookList;
}

然後在客戶端放一個按鈕,點擊就調用服務端的getBookList方法,多點擊幾次,客戶端就ANR了.
爲了避免出現ANR其實很簡單,我們只需要在非ui線程調用即可

public void invoke(View v) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            if (mRemoteBookManager != null) {
                try {
                    mRemoteBookManager.getBookList();
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }
    }).start();
}

同樣,當服務端調用客戶端listener中的方法時,被調用的方法也運行在Binder線程池中,只不過是客戶端線程池.所以我們同樣不可以在服務端中調用客戶端耗時方法,比如剛剛栗子中BookManagerService的onNewBookArrived方法,如下所示,調用了客戶端內部的IOnNewBookArrivedListener中的onNewBookArrived方法,如果客戶端這個方法比較耗時的話,那麼服務端中onNewBookArrived方法同樣需要運行在非ui線程.

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();
}

另外,由於客戶端IOnNewBookArrivedListener中的onNewBookArrived方法運行在客戶端Binder線程池,所以它不能直接操作ui,如果要操作ui需要使用handler回到ui線程.

爲了程序的健壯性,我們要需要一件事情,Binder可能意外死亡,這時我們需要重新連接服務,有兩種方法一種是給Binder設置DeathRecipient監聽,當Binder死亡時候我們會收到binderDied回調,在binderDied方法中我們可以重新連接遠程服務.另一種是在onServiceDisconnected中重連遠程服務.他們區別在於:onServiceDisconnected在UI線程被回調,而binderDied在客戶端的Binder線程池中回調.下面我們驗證下兩者的區別.首先我們通過ddms殺死服務端進程,然後在這兩個方法打印當前線程名稱.

01-20 16:15:14.401 18827-18840/? I/MainActivity: binderDied :Binder_2
01-20 16:15:14.401 18827-18827/? I/MainActivity: onServiceDisconnected :main

從日誌上可以看到,onServiceDisconnected運行在main線程,而binderDied運行在Binder_2線程,很顯然是Binder線程池中的一個線程.

到此爲此,我們已經對AIDL有一個系統的認識了,但還有一點,如何在AIDL中使用權限驗證功能,默認情況下,我們的遠程服務任何人都可以連接,但這不是我們願意看到的,所以我們必須加入權限驗證功能,權限驗證失敗則無法調用服務中的方法,這AIDL中進行權限驗證,我們這裏介紹兩個方法.

第一種我們在onBind中進行驗證,驗證通不過就返回null,這樣驗證失敗的客戶端無法直接綁定服務,至於驗證方式有很多種,比如使用permission驗證.使用這種方式,我們需要在androidmenifest中聲明所需的權限

<permission android:name="com.zly.www.ipc2.permission.ACCESS_BOOK_SERVICE"
    android:protectionLevel="normal"/>

關於permission的定義方式我轉載了一篇blog,Android自定義權限,定義了權限後,我們可以在BookManagerService的onBInd方法中做權限驗證,如下所示

public IBinder onBind(Intent intent) {
    int check = checkCallingOrSelfPermission("com.zly.www.ipc2.permission.ACCESS_BOOK_SERVICE");
    if(check == PackageManager.PERMISSION_DENIED){
        return null;
    }
    return mBinder;
}

這樣一個應用來綁定服務的時候,會驗證這個應用的權限,如果沒有這個權限,onBInd方法就會返回null,最終無法綁定我們的服務,同樣這種方式也適用於Messenger中.

如果我們自己內部應用想綁定我們的服務,需要在androidMenifest文件中採用如下方式使用

    <uses-permission android:name="com.zly.www.ipc2.permission.ACCESS_BOOK_SERVICE"/>

第二種方式,我們可以在服務端的onTransact方法進行權限驗證,如果驗證失敗就返回false,這樣服務端就不會終止AIDL中的方法達到保護服務端的效果.也可以採用permission方式.具體實現方式和第一種一樣,只不過這裏還加了包名驗證.

public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws RemoteException {
            // 權限驗證
            int check = checkCallingOrSelfPermission("com.example.test1.permission.ACCESS_BOOK_SERVICE");
            L.d("check:"+check);
            if(check==PackageManager.PERMISSION_DENIED){
                L.d("Binder 權限驗證失敗");
                return false;
            }
            // 包名驗證
            String packageName=null;
            String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
            if(packages!=null && packages.length>0){
                packageName = packages[0];
            }
            if(!packageName.startsWith("com.example")){
                L.d("包名驗證失敗");
                return false;

            }
            return super.onTransact(code, data, reply, flags);
        };

當然除了以上兩種驗證方式還可以爲service指定permission等,這裏不依依介紹了.

剩下的下一篇在寫了…真心太多了..寫的我要嘔心瀝血了…下面一篇在介紹IPC另外兩種方式…

最後不能忘了謝鳴:中間看aidl文檔的時候有了涅槃大胸弟的幫助很快的理解了原來generic是泛型不是通用…特此感謝…你問我是誰涅槃大胸弟,我只能告訴你胸很大的就是的.

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