程式間通訊 --IPC

夏雨友人帳發表於2017-04-02

*本篇文章已授權微信公眾號 guolin_blog (郭霖)獨家釋出

前言:

程式間通訊(Inter-Process Communication),簡稱IPC,就是指程式與程式之間進行通訊.一般來說,一個app只有一個程式,但是可能會有多個執行緒,所以我們用得比較多的是多執行緒通訊,比如handler,AsyncTask.

但是在一些特殊的情況下,我們app會需要多個程式,或者是我們在遠端服務呼叫時,就需要跨程式通訊了

1.設定多程式

Android設定多程式的步驟很簡單,只用在清單檔案中為四大元件加上process屬性

 <service android:name=".MessagerService"
             android:process=":messager">
</service>複製程式碼

( :messager 最終的程式名會變成 包名+:messager)

雖然多程式設定起來很簡單,但是使用的時候卻會有一系列的問題

(兩個程式對應的是不同的記憶體區域)

  • 1.Application物件會建立多次
  • 2.靜態成員不共用
  • 3.同步鎖失效
  • 4.單例模式失效
  • 5.資料傳遞的物件必須可序列化

2.可序列化

程式間通訊傳遞的物件是有嚴格要求的,除了基本資料型別,其他物件要想可以傳遞,必須可序列化,Android實現可序列化一般是通過實現Serializable或者是Parcelable

如果你在程式通訊中不需要傳非基本資料型別的物件,那麼你可以不瞭解序列化,但是可序列化是程式間通訊的基礎,所以還是建議不瞭解的朋友先熟悉一下

筆者之前介紹過序列化的相關知識,這裡就不重複介紹了

序列化--Serializable與Parcelable

blog.csdn.net/yulyu/artic…

3.通訊

跨程式通訊的方法有很多,比如通過Intent傳遞,通過AIDL以及Messager通訊,通過socket通訊,這裡主要介紹的是基於Binder的AIDL和Messager

3.1 Intent

Intent進行資料的傳遞是我們平時最常用的,他的原理其實是對於Binder的封裝,但是他只能做到單向的資料傳遞,所以並不能很好的實現跨程式通訊,我們這裡就不展開來介紹了

3.2 Messager

Messager的底層也是基於Binder的,其實應該說他是在AIDL的基礎上封裝了一層

一般來說安卓中使用Binder主要是通過繫結服務(bindService),服務端(這裡指的不是後臺,是指其中一個程式)主要是執行Service,客戶端通過bindService獲取到相關的Binder,Binder就作為橋樑進行跨程式的通訊.

這裡我們先演示同一個應用內的多程式通訊

3.2.1 伺服器端

首先我們先建立一個Service,

public class XiayuService extends Service{

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}複製程式碼

並在清單檔案中配置他的程式

<service android:name=".XiayuService"
             android:process=":xiayu"
 />複製程式碼

在Service裡面建立一個Hander用來接受訊息

private final static Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        System.out.println("地瓜地瓜,我是土豆,我是土豆, 聽到請回答,聽到請回答");

    }
};複製程式碼

在Service裡面建立一個Messager,並把Handler放入其中

private final static Messenger mMessenger = new Messenger(mHandler);複製程式碼

重寫onbind方法,返回Messager裡面的Binder

public IBinder onBind(Intent intent) {
    return mMessenger.getBinder();
}複製程式碼

3.2.2 客戶端

建立一個物件實現ServiceConnection

private class MyServiceConnection implements ServiceConnection {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //當連線上服務後會呼叫這個方法
            //TODO 
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
}複製程式碼

繫結服務

Intent    intent = new Intent(MainActivity.this, XiayuService.class);

MyServiceConnection  myServiceConnection = new MyServiceConnection();

bindService(intent, myServiceConnection, BIND_AUTO_CREATE);複製程式碼

繫結服務後,會呼叫ServiceConnection的onServiceConnected方法,通過Messager傳送訊息,伺服器端的Handler就能夠收到訊息了

private class MyServiceConnection implements ServiceConnection {

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        //通過Binder建立Messager
        Messenger messenger = new Messenger(service);
        //建立msg
        Message msg = Message.obtain();

        try {
            //通過Messager傳送訊息
            messenger.send(msg);

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

    @Override
    public void onServiceDisconnected(ComponentName name) {

    }
}複製程式碼

這樣的話我們就能夠通過bindService獲取到一個包含Binder的Messager進行通訊了,但是我們目前只實現了客戶端對伺服器端傳遞訊息,那麼伺服器端如何對客戶端傳遞訊息呢?

我們先對伺服器端的程式碼進行修改,首先修改Service的Handler

(關鍵程式碼是 Messenger messenger = msg.replyTo)

private final static Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        System.out.println("地瓜地瓜,我是土豆,我是土豆, 聽到請回答,聽到請回答");
        //獲取Messager
        Messenger messenger = msg.replyTo;
        //建立訊息
        Message msg_reply = Message.obtain();
        try {
            //傳送
            messenger.send(msg_reply);

        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
};複製程式碼

接著我們在客戶端也增加一個Handler和Messager來處理訊息

private final static Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        System.out.println("土豆,土豆,我是地瓜,我已收到你的訊息");
    }
};

private final static Messenger mReplyMessager = new Messenger(mHandler);複製程式碼

還有一個比較關鍵的地方,就是要在客戶端傳送訊息的時候把客戶端的Messager通過訊息傳送到伺服器端

(msg.replyTo =mReplyMessager)

private class MyServiceConnection implements ServiceConnection {

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        Messenger messenger = new Messenger(service);

        Message msg = Message.obtain();
        //通過msg把客戶端的Messager傳送到伺服器端(關鍵程式碼)
        msg.replyTo =mReplyMessager;

        try {

            messenger.send(msg);

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

    @Override
    public void onServiceDisconnected(ComponentName name) {

    }
}複製程式碼

這樣一來,伺服器端和客戶端就能很好的實現跨程式通訊了.

如果需要傳送資料的話,可以通過Bundle設定資料,除了基本資料型別,還可以通過訊息傳送可序列化的物件

傳送方:

        Message msg = Message.obtain();

        Bundle bundle = new Bundle();

        //傳輸序列化物件
        //bundle.putParcelable();

        //bundle.putSerializable();

        msg.setData(bundle);複製程式碼

接收方:

        Bundle data = msg.getData();

        //獲取資料
        //data.getSerializable()

        //data.getParcelable()複製程式碼

3.2.3 弊端

上面我們已經實現了跨程式通訊,但是這裡面其實是有弊端的,服務端處理客戶端的訊息是序列的,必須一個一個來處理,所以如果是併發量比較大的時候,通過Messager來通訊就不太適合了

3.2.4 注意

上面演示的是應用內跨程式通訊,繫結服務可以通過顯示意圖來繫結,但是如果是跨應用的程式間通訊,那麼就需要用到隱式意圖了.這裡有一點需要注意的就是,在5.0以後隱式意圖開啟或者繫結service要setPackage(Service的包名),不然會報錯

    mIntent = new Intent();

    //設定Package為Service的包名
    mIntent.setPackage("com.xiayu.ipcservice");

    mIntent.setAction("myMessager");複製程式碼

3.3 AIDL

上面提到過通過Meaager跨程式不適合併發量大的情況,那麼如果併發量大的話,我們用什麼來處理呢?那就可以通過AIDL來進行,這裡是Google的描述

Note: Using AIDL is necessary only if you allow clients from different applications to access your service for IPC and want 
to handle multithreading in your service. If you do not need to perform concurrent IPC across different applications, you 
should create your interface by implementing a Binder or, if you want to perform IPC, but do not need to handle 
multithreading, implement your interface using a Messenger. Regardless, be sure that you understand Bound Services before 
implementing an AIDL.複製程式碼

主要意思就是你可以用Messager處理簡單的跨程式通訊,但是高併發量的要用AIDL

我們還是先演示一下同一個應用內的跨程式通訊

3.3.1 服務端

首先我們建立一個Service

public class AIDLService extends Service {
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}複製程式碼

然後在清單檔案裡面設定Service的程式

    <service android:name=".AIDLService"
             android:process=":xiayu"
    />複製程式碼

然後右鍵選擇新建AIDL檔案,Android Studio就會幫你在你的aidl目錄的同名資料夾下面建立一個AIDL檔案

程式間通訊 --IPC

程式間通訊 --IPC

// IShop.aidl
package com.xiayu.aidldemo;

interface IShop {
       //此方法是建立aidl自帶的方法,告知你可以使用那些資料型別
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
}複製程式碼

在AIDL檔案裡面會有一個介面,並宣告瞭一個方法,那個方法主要是告訴你AIDL支援哪些資料型別傳輸,所以我們把這個方法刪掉,我們再自己宣告一個方法,用於之後的呼叫

(注意:每次修改了AIDI檔案後,需要同步一下才會生效,因為每次同步後,Android Studio會在 專案/build/generated/source/aidl/debug 目錄下生成相應的java檔案)

interface IShop {
    //自己宣告的方法,用於之後的呼叫
    void sell();
}複製程式碼

我們在Service中建立一個Binder,並在onbind的時候返回

public class AIDLService extends Service{

    private Binder mBinder = new IShop.Stub() {
        @Override
        public void sell() throws RemoteException {
            System.out.println("客官,您需要點什麼?");
        }
    };


    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
}複製程式碼

3.3.2客戶端

建立自定義一個類實現ServiceConnection

private class  XiayuConnection implements ServiceConnection{

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
       //繫結成功時會呼叫這個方法
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {

    }
}複製程式碼

繫結服務,當繫結成功時會走Connection的onServiceConnected方法,並把Binder傳過來

mIntent = new Intent(this, AIDLService.class);
mXiayuConnection = new XiayuConnection();
bindService(mIntent, mXiayuConnection, BIND_AUTO_CREATE);複製程式碼

在onServiceConnected方法裡面通過asInterface獲取伺服器傳過來的物件,並呼叫服務端的方法

@Override
public void onServiceConnected(ComponentName name, IBinder service) {
    //獲取到伺服器傳過來的物件
    IShop iShop = IShop.Stub.asInterface(service);
    try {
        iShop.sell();
    } catch (RemoteException e) {
        e.printStackTrace();
    }
}複製程式碼

現在客戶端就可以呼叫sell方法來進行跨程式通訊了,但目前只能傳輸基本資料型別的資料,那麼如果想要傳其他資料呢?那麼我們接著往下講

3.3.3 通過AIDL傳送複雜資料

首先我們要知道AIDL支援那麼資料型別

  • 1.基本資料型別
  • 2.實現了Parcelable介面的物件
  • 3.List:只支援ArrayList,並且裡面的元素需要時AIDL支援的
  • 4.Map:只支援HashMap,並且裡面的key和value都需要是被AIDL支援的

那麼我們定義一個物件Product實現Parcelable介面,如何實現Parcelable我這裡也不重複介紹了,如果不瞭解的朋友可以看看筆者之前寫的這篇文章

序列化--Serializable與Parcelable

blog.csdn.net/yulyu/artic…

Product我們設定了兩個欄位

public class Product implements Parcelable {
    public String name;
    public int    price;
    ...
}複製程式碼

接著我們需要在aidl資料夾的相同目錄建立一個相同檔名的aidl檔案

(注意,這裡我們是要通過new File的方式建立,並且要自己輸入檔案字尾aidl,如果你用new AIDL的方式建立的話,他會提示你Interface Name must be unique)

程式間通訊 --IPC

接著我們需要在這個aidl檔案裡面輸入包名,並且宣告一下變數為Parcelable型別

(注意,這裡宣告的時候是用小寫的parcelable)

// Product.aidl
package com.xiayu.aidldemo;

parcelable Product;複製程式碼

我們回到之前的IShop.aidl,刪掉之前的sell方法,並再建立兩個新方法

// IShop.aidl
package com.xiayu.aidldemo;

import com.xiayu.aidldemo.Product;

interface IShop {

    Product buy();

    void setProduct(in Product product);

}複製程式碼

這裡有三個需要注意的地方

(1)IShop.aidl雖然跟Product.aidl在同一個包下,但是這裡還是需要手動import進來

(2)這裡宣告方法時,需要在引數前面增加一個tag,這個tag有三種,in,out,inout,這裡表示的是這個引數可以支援的流向:

  • 1.in: 這個物件能夠從客戶端到伺服器,但是作為返回值從伺服器到客戶端的話資料不會傳送過去(不會為null,但是欄位都沒有賦值)
  • 2.out: 這個物件能夠作為返回值從伺服器到客戶端,但是從客戶端到伺服器資料會為空(不會為null,但是欄位都沒有賦值)
  • 3.inout: 能從客戶端到伺服器,也可以作為返回值從伺服器到客戶端

用一張圖來總結:

程式間通訊 --IPC

(不要都設為inout,要看需求來設定,因為會增加開銷)

(3)預設實現Parcelable的模版只支援in ,如果需要需要支援out或inout需要手動實現readFromParcel方法

@Override
public void writeToParcel(Parcel dest, int flags) {
    dest.writeString(this.name);
    dest.writeInt(this.price);
}
//手動實現這個方法
public void readFromParcel(Parcel dest) {
    //注意,這裡的讀取順序要writeToParcel()方法中的寫入順序一樣
    name = dest.readString();
    price = dest.readInt();
}複製程式碼

現在就可以在客戶端中通過IShop呼叫方法來進行通訊了

@Override
public void onServiceConnected(ComponentName name, IBinder service) {
    IShop iShop = IShop.Stub.asInterface(service);
    try {
        //呼叫方法進行通訊
        iShop.setProduct(mProduct);
        Product buy = iShop.buy();
    } catch (RemoteException e) {
        e.printStackTrace();
    }
}複製程式碼

3.4 不同應用間的多程式通訊(AIDL)

上面我們介紹了同一個應用內的程式間通訊,接下來我們就來介紹不同應用之間的程式間通訊

3.4.1 伺服器端

首先我們需要把Product.java放到aidl目錄相同名字的資料夾下(如果要提供服務給其他app,最好把需要的物件都放在aidl目錄下,這樣比較容易拷貝)

程式間通訊 --IPC

但是這個時候你執行程式的話,編譯會提示說找不到Product,那是因為Android Studio預設會去java目錄下找,這時候需要在build.gradle檔案 android{ } 中間增加一段程式碼,讓aidl目錄裡面的java檔案也能被識別

sourceSets {
    main {
        java.srcDirs = ['src/main/java', 'src/main/aidl']
    }
}複製程式碼

接著我們為Service增加intent-filter,這樣其他應用才能通過隱式意圖繫結服務,伺服器端的修改就結束了

<service android:name=".AIDLService"
         android:process=":xiayu">
    <intent-filter>
        <action android:name="action.xiayu"/>
    </intent-filter>
</service>複製程式碼

3.4.2 客戶端

我們需要建立一個新的應用來作為客戶端,並且把伺服器端的aidl目錄下的所有檔案都拷貝過來,這裡要注意的就是裡面的目錄不能改變,需要與以前一致

程式間通訊 --IPC

點選同步,Android Studio會自動生成相應的java檔案供我們使用

程式間通訊 --IPC

這個時候我們需要通過隱式意圖來繫結服務了

(注意:5.0以後隱式意圖開啟或者繫結service要setPackage,不然會報錯)

    mIntent.setAction("action.xiayu");
    mIntent.setPackage("com.xiayu.aidldemo");複製程式碼

接下來的操作就和之前一樣了,建立一個類實現ServiceConnection

private class  XiayuConnection implements ServiceConnection {

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        //TODO
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {

    }
}複製程式碼

繫結服務

bindService(mIntent, mXiayuConnection, BIND_AUTO_CREATE);複製程式碼

通過ServiceConnection的onServiceConnected裡面的IBinder進行通訊

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        IShop iShop = IShop.Stub.asInterface(service);
        try {
            iShop.setProduct(mProduct);
            Product buy = iShop.buy();
            System.out.println("buy=" + buy.price);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }複製程式碼

解除繫結的時候釋放資源

public void unbind(View v) {
    unbindService(mXiayuConnection);
    mXiayuConnection = null;
    mIShop = null;
}複製程式碼

這樣我們就可以通過獲得的IShop進行不同應用之間的程式間通訊了

最後再提幾點用到服務時需要注意的地方(很簡單,但是有些人經常會忽略這幾點)

  • 1: startService和stopService需要用同一個Intent物件
  • 2: bindService和unbindService需要用同一個ServiceConnection物件
  • 3: 5.0以後隱式意圖開啟或者繫結service要setPackage(包名)

熱門文章

相關文章