Android IPC入門

一、Android IPC簡介

IPC是Inter-Process Communication的縮寫,含義就是進程間通信或者跨進程通信,是指兩個進程之間進行數據交換的過程。那么什么是進程,什么是線程,進程和線程是兩個截然不同的概念。在操作系統中,線程是CPU調度的最小單元,同時線程是一種有限的系統資源。而進程指的一個執行單元,在PC和移動設備上指的是一個程序或者一個應用。一個進程可以包含多個線程,因此進程和線程是包含被包含的關系,最簡單情況下,一個進程可以只有一個線程,即主線程,在Android里面也叫UI線程,在UI線程里才能操作界面元素。

那么在Android中,有特色的進程間通信方式就是Binder了,通過Binder可以輕松實現進程間通信。除了Binder,Android還支持Socket,通過Socket也可以實現任意兩個終端之間的通信,當然一個設備上的兩個進程之間通過Socket通信自然也是可以的。

說到IPC的使用場景就必須提到多進程,只有面對多進程這種場景下,才需要考慮進程間通信。所有運行在不同進程中的四大組件,只要它們之間需要通過內存來共享數據,都會共享失敗,這也是多進程所帶來的主要影響。正常情況下,四大組件中間不可能不通過一些中間層來共享數據,那么通過簡單地指定進程名來開啟多進程都會無法正確運行。一般來說,使用多進程會造成如下幾方面的問題:

  • 靜態成員和單例模式完全失效
  • 線程同步機制完全失效
  • SharedPreferences的可靠性下降
  • Application會多次創建

二、Android中的多進程模式

1.開啟多進程模式

Android中一個應用可以存在多個進程,在Android中組件使用多進程的方式只有一種方法,設置AndroidMenifiest里的android:process屬性。

還有一種非常規方法,通過JNI在native層去fork一個新的進程。

<activity
            android:name=".SecondActivity"
            android:configChanges="screenLayout"
            android:label="@string/app_name"
            android:process=":remote" />
<activity
            android:name=".ThirdActivity"
            android:configChanges="screenLayout"
            android:label="@string/app_name"
            android:process="com.ryg.chapter_2.remote" />

進程分全局進程和私有進程:

  • 全局進程:默認包名,無 “:”分割,可以通過ShareUID跑在一起,具有相同的UID可以共享數據
  • 私有進程:有“:”分割,其他應用組件不能跑在同一進程里。

Android會為每一個應用分配唯一的UID,相同的UID才能共享數據,兩個應用通過ShareUID跑在同一個進程是有要求的,需要ShareUID并且簽名相同才可以,并且可以訪問對方私有數據,比如data目錄、組件信息、甚至是內存數據等。

2.多進程模式的運行機制

Android從細處說可以是為每個進程分配了一個虛擬機,每個虛擬機中都保留這一份副本。

一般來說,使用多進程會有以下幾個問題

  • 靜態成員和單例模式完全失效(處于不同的內存塊(進程),擁有各自的副本)
  • 線程同步機制完全失效(同一差不多)
  • SharedPreferences的可靠性降低:因為SharedPreferences不支持兩個進程同時去讀寫xml文件
  • Application會多次創建:開啟一個進程其實就等同于開多一個Application

總結:同一應用不同組件運行在不同的進程里,會擁有獨立的虛擬機、Application以及內存空間,雖然是同一個應用,但可以看成兩個不同應用采用了SharedUID的模式進行數據的共享。


三、IPC基礎概念介紹

主要包含三個方面的內容:Serializable接口、Parcelable接口以及Binder。

Serializable接口、Parcelable接口可以完成對象序列化過程,我們使用Intent和Binder傳輸數據的時候就要使用序列化數據,同時需要將對象持久化存儲到存儲設備上或者通過網絡傳輸給客戶端,也需要序列化。

1. Serializable接口

Serializable是一個序列化接口,為對象提供標準的序列化和反序列化操作。使用只要在類聲明中指定一個標志就可實現默認化的序列化程序

public class User implements Parcelable, Serializable {
    
     private static final long serialVersionUID = 519067123721295773L;
}
//序列化的過程

User user = new User(0,"jake",true);
ObjectOutputStream out = new ObjectOutputStream(
    new FileOutputStream("cache.txt");
out.writeObject(user);
out.close();


//反序列化過程

ObjectInputStream in = new ObjectInputStream(
    new FileInputStream(cache.txt));
User newUser = (User) in.readObject();
in.close();


serialVersionUID的詳細工作機制:
serialVersionUID是一串數字,可有可無,它相當于身份標識的作用。序列化的時候系統會把當前的類的serialVersionUID寫入到序列化文件中,當反序列化的時候會去檢查文件中的serialVersionUID,看它是否和當前類的一致,如果一致就證明反序列化中的版本同當前類的版本是相同的,可以進行反序列化,如果不相同,則證明當前類發生了某些變化,比如成員數量類型等變化,這個時候就反序列化不成功,就會報錯。

一般來說,我們應該指定serialVersionUID的值,也可以通過當前類的結構去自動生成它的hash值。指定serialVersionUID的值,這樣兩者在序列化和反序列化的serialVersionUID是相同的。如果不指定,當序列化類的某些變量改變后,系統會生成新的hash值給serialVersionUID,從而導致與反序列化中的不一致,導致反序列化不成功。一般程序要做到的是盡可能的恢復數據。

使用Serializable接口的方法:

  • bean類繼承該接口
  • 然后使用ObjectInputStream/OutputStream就正常的讀寫文件,系統自動實現序列化

serialVersionUID:是序列化的標志,相同可以發生反序列化,不同則不能發生反序列化

兩點注意:

  • 靜態成員屬于類不屬于對象,無法序列化
  • transient關鍵字的變量不參與序列化

2.Parcelable接口

 public int userId;
    public String userName;
    public boolean isMale;

    public Book book;

    public User() {
    }

    public User(int userId, String userName, boolean isMale) {
        this.userId = userId;
        this.userName = userName;
        this.isMale = isMale;
    }

    public int describeContents() {
        return 0;
    }

    public void writeToParcel(Parcel out, int flags) {
        out.writeInt(userId);
        out.writeString(userName);
        out.writeInt(isMale ? 1 : 0);
        out.writeParcelable(book, 0);
    }

    public static final Parcelable.Creator<User> CREATOR = new Parcelable.Creator<User>() {
        public User createFromParcel(Parcel in) {
            return new User(in);
        }

        public User[] newArray(int size) {
            return new User[size];
        }
    };

    private User(Parcel in) {
        userId = in.readInt();
        userName = in.readString();
        isMale = in.readInt() == 1;
        book = in
                .readParcelable(Thread.currentThread().getContextClassLoader());
    }

    @Override
    public String toString() {
        return String.format(
                "User:{userId:%s, userName:%s, isMale:%s}, with child:{%s}",
                userId, userName, isMale, book);
    }

我們來描述一下這個類,Parcel內部包裝了可序列化的數據,在序列化的過程中需要實現的功能有序列化、反序列化和內容描述。序列化功能由writeToParcel來實現;反序列化由CREATOR來完成,其內部會返回一個序列化對象和數組,并通過Parcel的一些列read方法來完成反序列化的過程;內容描述符由describeContents利來實現,幾乎都是返回0,除非存在文件描述符,則返回1;另外一點,book是一個可序列化對象,它的反序列化需要傳遞當前線程的上下文加載器。

方法 功能 標記位
createFromParcel(Parcel in) 從序列化后的對象中創建原始對象
newArray(int size) 創建指定長度的原始對象數組
User(Parcel in) 從序列化后的對象中創建原始對象
writeToParcel(Parcel out,int flags) 當前對象寫入序列化結構中,一般情況下flag為0 PARCELABLE_WRITE_RETURN_VALUE
describeContents 有文件描述符的時候需要返回1,沒有的時候返回0 CONTENTS_FILE_DESCRIPTOR

系統已經為我們提供了許多實現了Parcelable的類了,都可以直接序列化,比如Intent、Bundle、Bitamp、Map、List等,前提是他們內部元素也是可以序列化才行。

Serializable是Java中的序列化接口,其使用起來簡單但是開銷很大,在序列化和反序列化過程中需要大量的I/O操作。而Parcelable是Android中的序列化方式,因此更適合用在Android平臺上,它的缺點就是使用起來稍微麻煩點,但是它的效率很高。

3.Binder

直觀來說,Binder是Android中的一個類,它實現了IBinder接口。從IPC角度來說,Binder是Android中的一種跨進程通信方式,Binder還可以理解為一種虛擬的物理設備,它的設備驅動是/dev/binder,該通信方式在Linux中沒有。從Android Framework角度來說,Binder是ServiceManager連接各種Manager(ActivityManager、WindowManager等等)和相應ManagerService的橋梁。從Android應用層來說,Binder是客戶端和服務端進行通信的媒介,當bindService的時候,服務端會返回一個包含了服務端業務調用的Binder對象,通過Binder對象,客戶端就可以獲取服務端提供的服務或者數據,這里的服務包括普通服務和基于AIDL的服務。

Binder是一個跨進程通信的方式,可以用于Android的通信中,主要用于Service,包括AIDL和Messenger,其中普通的Service中的Binder不涉及進程間的通信,不觸及Binder的核心;而Messenger的底層是AIDL,而AIDL是通過Binder來實現,我們通過AIDL來分析Binder。

我們借助Android開發藝術中的demo來理解Binder:

首先我們新建Book.java、Book.aidl、IBookManager.aidl三個文件

public class Book implements Parcelable {

    public int bookId;
    public String bookName;

    public Book() {

    }

    public Book(int bookId, String bookName) {
        this.bookId = bookId;
        this.bookName = bookName;
    }

    public int describeContents() {
        return 0;
    }

    public void writeToParcel(Parcel out, int flags) {
        out.writeInt(bookId);
        out.writeString(bookName);
    }

    public static final Parcelable.Creator<Book> CREATOR = new Parcelable.Creator<Book>() {
        public Book createFromParcel(Parcel in) {
            return new Book(in);
        }

        public Book[] newArray(int size) {
            return new Book[size];
        }
    };

    private Book(Parcel in) {
        bookId = in.readInt();
        bookName = in.readString();
    }

    @Override
    public String toString() {
        return String.format("[bookId:%s, bookName:%s]", bookId, bookName);
    }

}
package com.ryg.chapter_2.aidl;

parcelable Book;

import com.ryg.chapter_2.aidl.Book;


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

上面這三個文件,Book.java代表圖書信息的類,實現了Parcelable接口,Book.aidl是Book類在AIDL中的聲明。IBookManager.aidl是我們定義的一個接口,理由有兩個方法。我們可以看到,雖然Book和IBookManager位于同一個包內,但是用到的時候還需要直接導入。現在我們來看看IBookManager.aidl產生的Binder類,我們找到他的.java文件,如下:

/*
 * This file is auto-generated.  DO NOT MODIFY.
 * Original file: D:\\LDProject\\Chapter_2\\app\\src\\main\\aidl\\com\\ryg\\chapter_2\\aidl\\IBookManager.aidl
 */
package com.ryg.chapter_2.aidl;
public interface IBookManager extends android.os.IInterface
{
/** Local-side IPC implementation stub class. */
public static abstract class Stub extends android.os.Binder implements com.ryg.chapter_2.aidl.IBookManager
{
private static final java.lang.String DESCRIPTOR = "com.ryg.chapter_2.aidl.IBookManager";
/** Construct the stub at attach it to the interface. */
public Stub()
{
this.attachInterface(this, DESCRIPTOR);
}
/**
 * Cast an IBinder object into an com.ryg.chapter_2.aidl.IBookManager interface,
 * generating a proxy if needed.
 */
public static com.ryg.chapter_2.aidl.IBookManager asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof com.ryg.chapter_2.aidl.IBookManager))) {
return ((com.ryg.chapter_2.aidl.IBookManager)iin);
}
return new com.ryg.chapter_2.aidl.IBookManager.Stub.Proxy(obj);
}
@Override public android.os.IBinder asBinder()
{
return this;
}
@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
{
switch (code)
{
case INTERFACE_TRANSACTION:
{
reply.writeString(DESCRIPTOR);
return true;
}
case TRANSACTION_getBookList:
{
data.enforceInterface(DESCRIPTOR);
java.util.List<com.ryg.chapter_2.aidl.Book> _result = this.getBookList();
reply.writeNoException();
reply.writeTypedList(_result);
return true;
}
case TRANSACTION_addBook:
{
data.enforceInterface(DESCRIPTOR);
com.ryg.chapter_2.aidl.Book _arg0;
if ((0!=data.readInt())) {
_arg0 = com.ryg.chapter_2.aidl.Book.CREATOR.createFromParcel(data);
}
else {
_arg0 = null;
}
this.addBook(_arg0);
reply.writeNoException();
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
private static class Proxy implements com.ryg.chapter_2.aidl.IBookManager
{
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote)
{
mRemote = remote;
}
@Override public android.os.IBinder asBinder()
{
return mRemote;
}
public java.lang.String getInterfaceDescriptor()
{
return DESCRIPTOR;
}
@Override public java.util.List<com.ryg.chapter_2.aidl.Book> getBookList() throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
java.util.List<com.ryg.chapter_2.aidl.Book> _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
_reply.readException();
_result = _reply.createTypedArrayList(com.ryg.chapter_2.aidl.Book.CREATOR);
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
@Override public void addBook(com.ryg.chapter_2.aidl.Book book) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
if ((book!=null)) {
_data.writeInt(1);
book.writeToParcel(_data, 0);
}
else {
_data.writeInt(0);
}
mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
}

finally {
_reply.recycle();
_data.recycle();
}
}
}
static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);

}
public java.util.List<com.ryg.chapter_2.aidl.Book> getBookList() throws android.os.RemoteException;
public void addBook(com.ryg.chapter_2.aidl.Book book) throws android.os.RemoteException;

}

這個類看起來很混亂,但其實邏輯很清晰,首先先聲明了getBookList和addBook,這是我們在IBookManager.aidl中聲明的方法。同時還聲明了兩個整型的id標志兩個方法用來區分到底是請求了誰(服務器或客戶端),接著聲明了一個內部類Stub,相當于Binder類,當兩者位于同一個進程,方法調用不會使用transact過程,當兩者處于不同的進程,方法調用transact過程,這個邏輯由Stub的內部代理Proxy完成。

從這個類我們可以看出,這個接口的核心是它的內部類Stub和Stub的內部代理類,下面我們介紹下每個方法的含義:

  • DESCRIPTOR:Binder的唯一標志,一般是用當前Binder的類名表示,比如本例中的“com.ryg.chapter_2.aidl.IBookManager”
  • asInterface(android.os.IBinder obj):用于將服務端的Binder對象轉換為客戶端所需的AIDL接口類型的對象,這種轉換區分進程,若客戶端和服務器同一進程,返回的就是服務端的Stub對象本身,否則是系統封裝好的Stub.proxy對象
  • asBinder:返回當前Binder對象
  • onTransact:這個方法運行在服務端的Binder線程池中,當客戶端發起跨進程通信時會交由該方法,該方法的原型為public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags)。服務端通過code可以確定客戶端請求的目標方法是什么,接著從data中取出目標方法所需的參數,然后執行目標方法。當目標方法執行完畢,就向reply中寫入返回值。如果此方法返回false,則表示請求失敗。
  • Proxy#getBookList:這個方法運行在客戶端,當客戶端遠程調用此方法時:首先創建該方法需要的輸入型Parcel對象_data、輸出型Parcel對象_reply和返回值對象List,然后把該方法的參數信息寫入到_data;接著調用transact方法來發遠程過程調用請求,同時當前線程掛起;然后服務端的onTransact方法會被調用直到道RPC過程返回,當前線程繼續執行,并從_reply中取出返回的結果,返回_reply中的數據。
  • Proxy#addBook:這個方法執行過程和getBookList方法調用過程類似,只是沒有返回值。

總結一下:首先,當客戶端發起遠程請求的時候,由于當前線程會被掛起直至服務端進程返回數據,如果遠程方法是比較耗時的,那么不能在UI線程中發起遠程請求;其次,由于服務端的Binder方法運行在Binder線程池中,所以Binder方法不管是否耗時都應該采用同步的方式去實現。下面是Binder的工作機制圖:

image

四、Android中的IPC方式

1.使用Bundle

我們知道,四大組件中三大組件(activity、service、receiver)都是支持在Intent中傳遞Bundle數據的,由于Bundle實現了Parcelable接口,所以它可以方便地在不同的進程間傳輸。

Bundle實現的是Parcelable接口,可以在不同進程間傳輸數據,把傳輸的數據放到Bundle,再使用Intent去啟動目標組件,從而可以實現跨進程通信。

2.使用文件共享

共享文件也是一種不錯的進程間通信方式,兩個進程間通過讀/寫同一個文件來交換數據,比如A進程把數據寫入文件,B進程通過讀取這個文件來獲取數據。Android是基于Linux系統的,所以并發讀/寫任務沒有太多限制,甚至可以在兩個線程中對同一個文件進行讀寫操作。

這次我們在MainActivity的onResume中去序列化一個User到sd卡中,然后再SecondActivity中的onResume中去反序列化,恢復User的對象的值,兩個Activity是在不同的進程中的。

代碼示例:

//在MainActivity中的修改
 @Override
    protected void onResume() {
        Log.d(TAG, "UserManage.sUserId=" + UserManager.sUserId);
        persistToFile();

        super.onStart();
    }

    private void persistToFile() {
        new Thread(new Runnable() {

            @Override
            public void run() {
                User user = new User(1, "hello world", false);
                File dir = new File(MyConstants.CHAPTER_2_PATH);
                if (!dir.exists()) {
                    dir.mkdirs();
                }
                File cachedFile = new File(MyConstants.CACHE_FILE_PATH);
                ObjectOutputStream objectOutputStream = null;
                try {
                    objectOutputStream = new ObjectOutputStream(
                            new FileOutputStream(cachedFile));
                    objectOutputStream.writeObject(user);
                    Log.d(TAG, "persist user:" + user);
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    MyUtils.close(objectOutputStream);
                }
            }
        }).start();
    }
    
    //SecondActivity中的修改
    
     @Override
    protected void onResume() {
        super.onResume();
        User user = (User) getIntent().getSerializableExtra("extra_user");
        Log.d(TAG, "user:" + user.toString());
        // Log.d(TAG, "UserManage.sUserId=" + UserManager.sUserId);
        recoverFromFile();
    }

    private void recoverFromFile() {
        new Thread(new Runnable() {

            @Override
            public void run() {
                User user = null;
                File cachedFile = new File(MyConstants.CACHE_FILE_PATH);
                if (cachedFile.exists()) {
                    ObjectInputStream objectInputStream = null;
                    try {
                        objectInputStream = new ObjectInputStream(
                                new FileInputStream(cachedFile));
                        user = (User) objectInputStream.readObject();
                        Log.d(TAG, "recover user:" + user);
                    } catch (IOException e) {
                        e.printStackTrace();
                    } catch (ClassNotFoundException e) {
                        e.printStackTrace();
                    } finally {
                        MyUtils.close(objectInputStream);
                    }
                }
            }
        }).start();
    }
    
07-12 02:02:34.769 6940-6979/com.ryg.chapter_2 D/MainActivity: persist user:User:{userId:1, userName:hello world, isMale:false}, with child:{null}
07-12 02:03:35.413 6962-6962/com.ryg.chapter_2:remote D/SecondActivity: user:User:{userId:0, userName:jake, isMale:true}, with child:{[bookId:0, bookName:null]}

通過文件共享數據對文件格式是沒有具體的要求,可以是文本或者xml文件,只要雙方約定好數據格式就可以。但文件共享方式是由局限性的,可能存在讀寫不同步,所以適用在讀寫數據同步要求不高的前提下。

一般不建議是用SharePreferences讀寫,因為系統對它的讀寫有一定的緩存策略,即在內存中會有一份SharedPreferences文件的緩存,在多進程模式下,系統對它的讀寫就變得不可靠。

3.使用Messenger

Messenger可以翻譯為信使,可以在不同進程中傳遞Message對象,在Message中放入我們需要傳遞的數據,就可以實現數據的進程間的傳遞了,底層是通過AIDL實現的。

我們看一下Messenger這個類的構造方法,不管是IMessenger還是Stub.asInterface,這種使用方法都表明底層使用了AIDL

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

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

Messenger對AIDL做了封裝,使得我們可以更簡便地進行進程間的通信。同時由于一次處理一個請求,因此服務端我們不用考慮線程同步的問題。實現一個Messenger有多個步驟,分為服務端和客戶端:

  • 服務端:創建一個Service來處理客戶端的連接請求,同時創建一個Handle并通過它來創建一個Messenger對象,然后再Service的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 MyConstants.MSG_FROM_CLIENT:
                Log.i(TAG, "receive msg from Client:" + msg.getData().getString("msg"));
                Messenger client = msg.replyTo;
                Message relpyMessage = Message.obtain(null, MyConstants.MSG_FROM_SERVICE);
                Bundle bundle = new Bundle();
                bundle.putString("reply", "嗯,你的消息我已經收到,稍后會回復你。");
                relpyMessage.setData(bundle);
                try {
                    client.send(relpyMessage);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
                break;
            default:
                super.handleMessage(msg);
            }
        }
    }

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

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

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return super.onStartCommand(intent, flags, startId);
    }

}

從服務端的代碼中可以看到,MessageHandler用來處理客戶端發過來的消息,并從客戶端中取出文本信息,而mMessenger是和客戶端關聯在一起的,在onBind方法中返回它里面的Binder對象,這里的Messenger的作用是將客戶端發送的消息轉交給MessengerHandler處理。

  • 客戶端:首先綁定服務端的Service,用返回的IBinder創建一個Messenger,通過這個Messenger像服務器發送Message類型的數據,如果需要服務端能夠回應客戶端,同時還需要創建一個Handle并創建一個新的Messenger,把這個Messenger對象通過Message的replyTo傳遞給服務端。
public class MessengerActivity extends Activity {

    private static final String TAG = "MessengerActivity";

    private Messenger mService;
    private Messenger mGetReplyMessenger = new Messenger(new MessengerHandler());
    
    private static class MessengerHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case MyConstants.MSG_FROM_SERVICE:
                Log.i(TAG, "receive msg from Service:" + msg.getData().getString("reply"));
                break;
            default:
                super.handleMessage(msg);
            }
        }
    }

    private ServiceConnection mConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className, IBinder service) {
            mService = new Messenger(service);
            Log.d(TAG, "bind service");
            Message msg = Message.obtain(null, MyConstants.MSG_FROM_CLIENT);
            Bundle data = new Bundle();
            data.putString("msg", "hello, this is client.");
            msg.setData(data);
            msg.replyTo = mGetReplyMessenger;
            try {
                mService.send(msg);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        public void onServiceDisconnected(ComponentName className) {
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_messenger);
        Intent intent = new Intent("com.ryg.MessengerService.launch");
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
    }
    
    @Override
    protected void onDestroy() {
        unbindService(mConnection);
        super.onDestroy();
    }
}

從例子看來,在Messenger中進行數據傳遞必須是將數據放到Message中,實際上,Messenger來傳輸Message,Message中能使用的載體有what,arg1,arg2,Bundle以及replyTo。Message的object在同一個進程中的使用是很實用的。

Messenger跨進程通信原理圖:

image

4.使用AIDL

通過上一節,我們可以看到Messenger是串行方式來處理客戶端發來的消息,如果有大量消息同時發送到客戶端,服務器只能一個一個處理,這就有點耗時了。所以我們要使用AIDL來實現跨進程通信的方法調用。

(1)服務端

服務端首先要創建一個Service用來監聽客戶端的請求連接,然后創建一個AIDL文件,將暴露給客戶端的接口在這個AIDL文件中聲明,在Service中實現這個接口

(2)客戶端

首先綁定好服務端的Service,將服務端返回的Binder對象轉成AIDL接口所屬的類型,接著調用AIDL中的方法

(3)AIDL接口的創建

我們要先創建一個后綴為AIDL的文件,在里面聲明一個接口和兩個接口方法

package com.ryg.chapter_2.aidl;

import com.ryg.chapter_2.aidl.Book;

interface IBookManager {
     List<Book> getBookList();
     void addBook(in Book book);
    
}
  • AIDL并不支持所有數據,可以使用的數據類型如下:
    • 基本數據類型(int,long,char,boolean,double等)
    • String和CharSequence
    • List:只支持ArrayList
    • Map:只支持HashMap
    • Parcelable
    • AIDL

AIDL支持如上所有數據類型,但是對于自定義Parcelable和AIDL對象必須顯示import進去,同時對于自定義的Parcelable對象,需要同等的為它創建一個.aidl

public class Book implements Parcelable {

    public int bookId;
    public String bookName;

    public Book() {

    }

    public Book(int bookId, String bookName) {
        this.bookId = bookId;
        this.bookName = bookName;
    }

    public int describeContents() {
        return 0;
    }

    public void writeToParcel(Parcel out, int flags) {
        out.writeInt(bookId);
        out.writeString(bookName);
    }

    public static final Parcelable.Creator<Book> CREATOR = new Parcelable.Creator<Book>() {
        public Book createFromParcel(Parcel in) {
            return new Book(in);
        }

        public Book[] newArray(int size) {
            return new Book[size];
        }
    };

    private Book(Parcel in) {
        bookId = in.readInt();
        bookName = in.readString();
    }

    @Override
    public String toString() {
        return String.format("[bookId:%s, bookName:%s]", bookId, bookName);
    }

}
package com.ryg.chapter_2.aidl;

parcelable Book;

AIDL中除了基本數據類型外,其他類型參數必須標明方向:in表示出入參數、out表示出書參數、inout表示輸入輸出型參數,并且不支持聲明靜態常量。

(4)遠程服務端Service的實現

創建完AIDL接口后,我們就要實現這個接口,先創建一個Service,稱為BookManagerService

public class BookManagerService extends Service {

    private static final String TAG = "BMS";

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

    private Binder mBinder = new IBookManager.Stub() {

        @Override
        public List<Book> getBookList() throws RemoteException {
            SystemClock.sleep(5000);
            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"));
    }

    @Override
    public IBinder onBind(Intent intent) {
        int check = checkCallingOrSelfPermission("com.ryg.chapter_2.permission.ACCESS_BOOK_SERVICE");
        Log.d(TAG, "onbind check=" + check);
        if (check == PackageManager.PERMISSION_DENIED) {
            return null;
        }
        return mBinder;
    }

}

上面是一個服務端Service的典型實現,首先在onCreate方法中初始化信息,然后創建一個Binder對象并返回它,這個對象繼承自IBookManager.Stub ,并實現它內部的AIDL方法,我們這里采用了CopyOnWriteArrayList,CopyOnWriteArrayList支持并發讀寫的ArrayList,AIDL方法是在服務端的Binder線程池中執行的因此多個客戶端同時連接的時候存在多個線程訪問的情形,所以我們要在AIDL中處理線程同步。。

前面我們提到AIDL中能夠使用的List只有ArrayList,其實是支持抽象的List,而List是一個接口,雖然服務端返回的是CopyOnWriteArrayList,但在Binder會按照List的規范去訪問數據并形成一個新的ArrayList傳遞給客戶端。

(5)客戶端的實現:

客戶端首先要綁定遠程服務,綁定成功后返回Binder,將Binder對象轉為AIDL接口,然后通過這個接口去調用遠程服務的方法:

public class BookManagerActivity extends Activity {

    private static final String TAG = "BookManagerActivity";

    private ServiceConnection mConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className, 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());
                bookManager.addBook(newBook);
                Log.i(TAG, "add book:" + newBook);
                List<Book> newList = bookManager.getBookList();
                Log.i(TAG, "query book list:" + newList.toString());
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

    };


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

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

}

綁定成功后,就會通過bookManager去調用遠程AIDL中的方法。

現在我們來考慮一種情況,當有新書到,可不可以自動通知用戶,這是一種典型的觀察者模式。首先我們要定義一個AIDL接口,每個用戶都需要實現這個接口并且向圖書館申請新書的提醒功能,當然也可以隨時取消這種功能。

package com.ryg.chapter_2.aidl;

import com.ryg.chapter_2.aidl.Book;

interface IOnNewBookArrivedListener {
    void onNewBookArrived(in Book newBook);
}

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

接著要修改下Service的實現,主要是Service中IBookManage.Stub的實現:

   private RemoteCallbackList<IOnNewBookArrivedListener> mListenerList = new RemoteCallbackList<IOnNewBookArrivedListener>();
        @Override
        public void registerListener(IOnNewBookArrivedListener listener)
                throws RemoteException {
            mListenerList.register(listener);

            final int N = mListenerList.beginBroadcast();
            mListenerList.finishBroadcast();
            Log.d(TAG, "registerListener, current size:" + N);
        }

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

            if (success) {
                Log.d(TAG, "unregister success.");
            } else {
                Log.d(TAG, "not found, can not unregister.");
            }
            final int N = mListenerList.beginBroadcast();
            mListenerList.finishBroadcast();
            Log.d(TAG, "unregisterListener, current size:" + N);
        };
        
        
    private void onNewBookArrived(Book book) throws RemoteException {
        mBookList.add(book);
        final int N = mListenerList.beginBroadcast();
        for (int i = 0; i < N; i++) {
            IOnNewBookArrivedListener l = mListenerList.getBroadcastItem(i);
            if (l != null) {
                try {
                    l.onNewBookArrived(book);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }
        mListenerList.finishBroadcast();
    }
    
    private class ServiceWorker implements Runnable {
        @Override
        public void run() {
            // do background processing here.....
            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);
                try {
                    onNewBookArrived(newBook);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }
    } 
    
    @Override
    public void onCreate() {
        super.onCreate();
        mBookList.add(new Book(1, "Android"));
        mBookList.add(new Book(2, "Ios"));
        new Thread(new ServiceWorker()).start();
    }    

最后我們修改下客戶端的代碼,主要有兩個方面:首先客戶端要注冊IOnNewBookArrivedListener到遠程服務端,同時我們在Activity退出的時候要解除這個注冊。

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case MESSAGE_NEW_BOOK_ARRIVED:
                Log.d(TAG, "receive new book :" + msg.obj);
                break;
            default:
                super.handleMessage(msg);
            }
        }
    };
    private ServiceConnection mConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className, IBinder service) {
            IBookManager bookManager = IBookManager.Stub.asInterface(service);
            mRemoteBookManager = bookManager;
            try {
                mRemoteBookManager.asBinder().linkToDeath(mDeathRecipient, 0);
                List<Book> list = bookManager.getBookList();
                Log.i(TAG, "query book list, list type:"
                        + list.getClass().getCanonicalName());
                Log.i(TAG, "query book list:" + list.toString());
                Book newBook = new Book(3, "Android進階");
                bookManager.addBook(newBook);
                Log.i(TAG, "add book:" + newBook);
                List<Book> newList = bookManager.getBookList();
                Log.i(TAG, "query book list:" + newList.toString());
                bookManager.registerListener(mOnNewBookArrivedListener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        public void onServiceDisconnected(ComponentName className) {
            mRemoteBookManager = null;
            Log.d(TAG, "onServiceDisconnected. tname:" + Thread.currentThread().getName());
        }
    };
    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 onDestroy() {
        if (mRemoteBookManager != null
                && mRemoteBookManager.asBinder().isBinderAlive()) {
            try {
                Log.i(TAG, "unregister listener:" + mOnNewBookArrivedListener);
                mRemoteBookManager
                        .unregisterListener(mOnNewBookArrivedListener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        unbindService(mConnection);
        super.onDestroy();
    }

RemoteCallbackList是系統專門提供的用于刪除跨進程listener的接口。它是一個泛型類,支持AIDL接口

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 = listener.asBinder();
Callback value = new Callback(listener,cookie);

雖然說跨進程通信傳輸客戶端的對象會在服務端中生成不同的對象,但是這些新生成的對象有一個共同點,就是底層的Binder是同一個,當客戶端解注的時候只要遍歷服務器端的所有listener,找出和解注listener具有相同的Binder并把它刪除。

5.使用ContentProvider

ContentProvider是Android中提供的專門用于不同應用間進行數據共享的方式,它的底層實現同樣也是Binder。

系統預制了許多ContentProvider,比如通訊記錄、日程表信息,要實現跨進程通信,只要通過ContentResolve的query、update、insert、delete方法即可。

我們可以定義一個繼承于ContentProvide的子類,實現里面的6個方法:

  • onCrete:在主線程中調用,主要做一些初始化工作
  • getType:返回一個Uri請求對應的MIME類型,如果我們的應用不關注這個選項可以直接返回null
  • 剩下的四個方法對應CRUD操作,主要由外界調用并應用在Binder線程池中。

ContentProvider主要以表格的形式來組織數據,并可以返回多個表,對于每個表格來說具有行和列的層次。除此之外還支持文件數據、比如圖片視頻等等。

下面是ContentProvider的一個demo:


public class BookProvider extends ContentProvider {

    private static final String TAG = "BookProvider";

    public static final String AUTHORITY = "com.ryg.chapter_2.book.provider";

    public static final Uri BOOK_CONTENT_URI = Uri.parse("content://"
            + AUTHORITY + "/book");
    public static final Uri USER_CONTENT_URI = Uri.parse("content://"
            + AUTHORITY + "/user");

    public static final int BOOK_URI_CODE = 0;
    public static final int USER_URI_CODE = 1;
    private static final UriMatcher sUriMatcher = new UriMatcher(
            UriMatcher.NO_MATCH);

    static {
        sUriMatcher.addURI(AUTHORITY, "book", BOOK_URI_CODE);
        sUriMatcher.addURI(AUTHORITY, "user", USER_URI_CODE);
    }

    private Context mContext;
    private SQLiteDatabase mDb;

    @Override
    public boolean onCreate() {
        Log.d(TAG, "onCreate, current thread:"
                + Thread.currentThread().getName());
        mContext = getContext();
        initProviderData();
        return true;
    }

    private void initProviderData() {
        mDb = new DbOpenHelper(mContext).getWritableDatabase();
        mDb.execSQL("delete from " + DbOpenHelper.BOOK_TABLE_NAME);
        mDb.execSQL("delete from " + DbOpenHelper.USER_TALBE_NAME);
        mDb.execSQL("insert into book values(3,'Android');");
        mDb.execSQL("insert into book values(4,'Ios');");
        mDb.execSQL("insert into book values(5,'Html5');");
        mDb.execSQL("insert into user values(1,'jake',1);");
        mDb.execSQL("insert into user values(2,'jasmine',0);");
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {
        Log.d(TAG, "query, current thread:" + Thread.currentThread().getName());
        String table = getTableName(uri);
        if (table == null) {
            throw new IllegalArgumentException("Unsupported URI: " + uri);
        }
        return mDb.query(table, projection, selection, selectionArgs, null, null, sortOrder, null);
    }

    @Override
    public String getType(Uri uri) {
        Log.d(TAG, "getType");
        return null;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        Log.d(TAG, "insert");
        String table = getTableName(uri);
        if (table == null) {
            throw new IllegalArgumentException("Unsupported URI: " + uri);
        }
        mDb.insert(table, null, values);
        mContext.getContentResolver().notifyChange(uri, null);
        return uri;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        Log.d(TAG, "delete");
        String table = getTableName(uri);
        if (table == null) {
            throw new IllegalArgumentException("Unsupported URI: " + uri);
        }
        int count = mDb.delete(table, selection, selectionArgs);
        if (count > 0) {
            getContext().getContentResolver().notifyChange(uri, null);
        }
        return count;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection,
            String[] selectionArgs) {
        Log.d(TAG, "update");
        String table = getTableName(uri);
        if (table == null) {
            throw new IllegalArgumentException("Unsupported URI: " + uri);
        }
        int row = mDb.update(table, values, selection, selectionArgs);
        if (row > 0) {
            getContext().getContentResolver().notifyChange(uri, null);
        }
        return row;
    }

    private String getTableName(Uri uri) {
        String tableName = null;
        switch (sUriMatcher.match(uri)) {
        case BOOK_URI_CODE:
            tableName = DbOpenHelper.BOOK_TABLE_NAME;
            break;
        case USER_URI_CODE:
            tableName = DbOpenHelper.USER_TALBE_NAME;
            break;
            default:break;
        }

        return tableName;
    }
}

接著我們需要注冊這個BookProvider,其中android:authorities是ContentProvider的唯一標識,通過這個標志可以訪問到我們的provider,并且可以給provider加上一個訪問權限permission,如果分別聲明了讀寫權限,外界也必須一次聲明相應的權限才可以進行讀寫操作。

        <provider
            android:name=".provider.BookProvider"
            android:authorities="com.ryg.chapter_2.book.provider"
            android:permission="com.ryg.PROVIDER"
            android:process=":provider" >
        </provider>

現在我們給我們的ContentProvider提供一個數據庫。我們借助SQLiteOpenHelper來管理數據庫的創建、升級和降級。通過ContentProvider的Uri來區分外界要訪問哪種數據。當要觀察一個ContentProvider中的數據是否改變了,我們可以使用ContentResolve的registerContentObserver方法來注冊觀察者,通過unregisterContentObserver來解除觀察者。

package com.ryg.chapter_2.provider;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class DbOpenHelper extends SQLiteOpenHelper {

    private static final String DB_NAME = "book_provider.db";
    public static final String BOOK_TABLE_NAME = "book";
    public static final String USER_TALBE_NAME = "user";

    private static final int DB_VERSION = 3;

    private String CREATE_BOOK_TABLE = "CREATE TABLE IF NOT EXISTS "
            + BOOK_TABLE_NAME + "(_id INTEGER PRIMARY KEY," + "name TEXT)";

    private String CREATE_USER_TABLE = "CREATE TABLE IF NOT EXISTS "
            + USER_TALBE_NAME + "(_id INTEGER PRIMARY KEY," + "name TEXT,"
            + "sex INT)";

    public DbOpenHelper(Context context) {
        super(context, DB_NAME, null, DB_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_BOOK_TABLE);
        db.execSQL(CREATE_USER_TABLE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // TODO ignored
    }

}


public class ProviderActivity extends Activity {
    private static final String TAG = "ProviderActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_provider);
        // Uri uri = Uri.parse("content://com.ryg.chapter_2.book.provider");
        // getContentResolver().query(uri, null, null, null, null);
        // getContentResolver().query(uri, null, null, null, null);
        // getContentResolver().query(uri, null, null, null, null);

        Uri bookUri = Uri.parse("content://com.ryg.chapter_2.book.provider/book");
        ContentValues values = new ContentValues();
        values.put("_id", 6);
        values.put("name", "程序設計的藝術");
        getContentResolver().insert(bookUri, values);
        Cursor bookCursor = getContentResolver().query(bookUri, new String[]{"_id", "name"}, null, null, null);
        while (bookCursor.moveToNext()) {
            Book book = new Book();
            book.bookId = bookCursor.getInt(0);
            book.bookName = bookCursor.getString(1);
            Log.d(TAG, "query book:" + book.toString());
        }
        bookCursor.close();

        Uri userUri = Uri.parse("content://com.ryg.chapter_2.book.provider/user");
        Cursor userCursor = getContentResolver().query(userUri, new String[]{"_id", "name", "sex"}, null, null, null);
        while (userCursor.moveToNext()) {
            User user = new User();
            user.userId = userCursor.getInt(0);
            user.userName = userCursor.getString(1);
            user.isMale = userCursor.getInt(2) == 1;
            Log.d(TAG, "query user:" + user.toString());
        }
        userCursor.close();
    }
}

6.使用scoket

Socket來實現進程間通信,Socket也成為套接字,是網絡通信中的概念,它分為流式套接字和用戶數據報套接字兩種,分別對應TCP和UDP
Java提供了良好的接口進行通信

使用Socket前要獲取權限:

    <uses-permission android:name="com.ryg.PROVIDER" />
    <uses-permission android:name="com.ryg.chapter_2.permission.ACCESS_BOOK_SERVICE" />

其次是不能在主線程中訪問網絡,不然會產生NetworkOnMainThreadException

在Java中能充當服務器接受請求的類是ServerSocket,它來監聽scoket連接。一直處于在線狀態。ServerSocket包含一個監聽來自客戶端連接請求的方法:

  • Socket accept():該方法返回與客戶端對應的scoket,否則線程會被阻塞

ServerScoket類有幾個構造器:

  • ServerScoket(int port)
  • ServerScoket(int port,int backlog):增加一個用來改變連接隊列長度的參數backlog
  • ServerScoket(int port,int backlog,InetAddress localAddr):localAddr用來綁定指定的ip地址

ServerScoket應不斷的調用accpet()來響應客戶端的所有請求

2.使用Socket進行通信

Scoket提供兩個構造器

  • Scoket(InetAddress/String remoteAddress,int port)
  • Scoket(InetAddress/String remoteAddress,int port,InetAddress localAddress,int localPort)

當客戶端和服務端都生成了自己的socket之后,且服務端accept()后,兩個socket就可以互相通信,Scoket提供兩個方法來獲取輸入流和輸出流

  • InputStream getInputStream():從該Socket對象中取出數據
  • OutputStream getOutputStream():返回該Socket的輸出流,向該Socket輸出數據

對于設定客戶端Scoket的連接時長,由于不存在包含此參數的構造器,所以需要通過Scoket的connect()方法設置


五、選擇合適的IPC方式

image
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容