IPC(Inter-Process Communication),意爲進程間通訊或者跨進程通訊,是指兩個進程之間進行數據交換的過程。前面在學習Handler機制 機製時提到過執行緒與進程的概念,在安卓中一個進程可以簡單理解爲一個程式或應用,而執行緒是CPU排程的最小單元,一個進程可以包含多個執行緒,也可以只有一個執行緒即主(UI)執行緒。
既然是跨進程通訊,必然要在多進程模式下進行。
使用多進程的情況分爲兩種:
安卓中開啓多進程模式的方式:
多進程模式的執行機制 機製:
所有執行在不同進程的四大元件,只要它們之間需要通過記憶體來共用數據,都會共用失敗。這是因爲Android爲每個應用/進程分配了一個獨立的虛擬機器,不同的虛擬機器在記憶體分配上有不同的地址空間,這會導致在不同的虛擬機器中存取同一個類的物件會產生多份副本。舉例來說,假如兩個進程中都存在同一個類,並且這兩個類是互不幹 不乾擾的,在其中一個進程中修改類變數的值只會影響當前進程,對其他進程不會造成任何影響。
使用多進程會造成的問題:
我們知道任何一個操作系統都有對應的IPC機制 機製,例如:
傳輸效率高、可操作性強:傳輸效率主要影響因素是記憶體拷貝的次數,拷貝次數越少,傳輸速率越高。幾種數據傳輸方式比較:
對於訊息佇列、Socket和管道來說,數據先要從發送方的快取區拷貝到內核開闢的快取區中,再從內核快取區拷貝到接收方的快取區,一共兩次拷貝,圖片來自Android - Binder驅動:
而對於Binder來說,數據從發送方的快取區拷貝到內核的快取區,而接收方的快取區與內核的快取區是對映到同一塊實體地址的,只需要一次拷貝:
共用記憶體雖然在傳輸時沒有拷貝數據,但其控制機制 機製複雜,綜合來看Binder的傳輸效率是最好的。
實現C/S架構方便:Linux的IPC方式中只有Socket支援C/S的通訊模式,而Socket主要用於網路間的通訊且傳輸效率低、開銷大。Binder基於C/S 架構設計 ,Server端與Client端相對獨立,穩定性較好。
如下圖所示,Binder框架定義了四個角色:Server,Client,ServiceManager和Binder驅動,其中Server、Client、ServiceManager執行於使用者空間,Binder驅動執行於內核空間,Binder框架如下圖所示:
這裏先解釋下上面說的使用者空間和內核空間。我們知道在Android中每一個進程/應用都是獨立的,只能執行在自己進程所擁有的虛擬地址空間,且都由兩部分組成,一部分是使用者空間,另一部分是內核空間,對於使用者空間來說,不同進程/應用之間彼此是不能共用的,而內核空間卻是可共用的。Client進程與Server進程通訊利用進程間可共用的內核記憶體空間來完成底層通訊工作,Client端與Server端進程跟內核空間的驅動進行互動往往採用ioctl(專門用於裝置輸入輸出操作的系統呼叫)等方法。這樣做一方面可以限制其他進程存取自己的虛擬地址空間,安全性高,另一方面內核共用也有助於系統維護和併發操作,節省空間:
安全性高:傳統Linux IPC的接收方無法獲得對方進程可靠的UID/PID,從而無法鑑別對方身份,而Binder機制 機製爲每個進程分配了UID/PID且在Binder通訊時會根據UID/PID進行有效性檢測
剛上來就分析具體的流程確實比較困難,這裏我們通過一個例子瞭解Binder執行機制 機製。
首先介紹下Binder框架定義的幾個角色:
圖中也看到Client、Server、ServiceManager之間的互動都是虛線表示,是由於它們彼此都不是直接互動的,而是都通過與Binder驅動進行互動從而實現IPC通訊方式。這裏可能會有人問爲什麼Client不直接和Server通訊而要加個ServiceManager呢?其實這樣做一方面便於管理service和Client的請求;另一方面在應用程式開發時,只需爲Client建立到Server的連線,就可花很少時間和精力去實現Server相應功能。舉個例子,你(client)畢業了想向你們班所有同學(Server)寫信,考慮兩種情況:一是你們有個線上通訊錄文件(ServiceManager),同學們畢業前都主動把自己聯繫地址「註冊」了上去,這樣你寄信的時候查一下儲存的通訊錄(ServiceManager)對應名字的地址就好;二是沒有這個通訊錄(ServiceManager),你寫信要一個個打電話詢問然後寄出去,顯而易見第一種方式簡單一些。當然這個例子不太恰當,Android既然這樣做那麼肯定是有好處的。
這裏參考詳細說說Binder通訊原理與機制 機製給的Binder跨進程通訊的例子以理清上面的流程:
假如Client進程呼叫Server進程的computer物件的add方法:
再簡單一點看的話,之前在學習bindservice啓動Service的時候,自定義的Service(就看做是伺服器端吧)在onBind()方法中就要返回一個包含了「伺服器端」業務呼叫的Binder物件,在「用戶端」角度來說,這個返回的就是一個Binder參照,通過這個Binder參照,「用戶端」就可以獲取「伺服器端」提供的服務或者數據。這裏也可以看到,「伺服器端」想要實現被「跨進程」存取,就必須繼承Binder類。
MainActivity中:
上面還提到了代理就簡單瞭解一下即可,詳細可參考代理模式以及在Android中的使用 ,代理模式Proxy就是給某個物件提供一個代理物件,並由代理物件控制對原物件的存取。UML圖如下所示:
具體可參考:
前面提到過,Bundle、AIDL、Socket等一些IPC方式實際都是通過Binder來實現,只不過封裝方式不同,這裏簡單瞭解下Android中的幾種IPC也就是跨進程通訊的方式,詳細的解釋可以參考《Android開發藝術探索》:
Bundle在之前學習Activity通訊的時候學過,它可以在Activity、Service和Receiver之間通過Intent.putExtra()傳遞Bundle數據。舉個例子:
//MainActivity中
Intent intent = new Intent(MainActivity.this,SecondActivity.class);
Bundle data = new Bundle();
data.putString("name","lisi");//將數據放入bundle
data.putInt("age",27);
data.putBoolean("isfemale",true);
intent.putExtras(data);
//在SecondActivity中,將傳遞的數據取出
Bundle data = getIntent().getExtras();//從bundle中取出數據
String name = data.getString(MainActivity.NAME_KEY);
int age = data.getInt(MainActivity.AGE_KEY);
boolean isfemale = data.getBoolean(MainActivity.IS_FEMALE_KEY);
Log.d(LOG_TAG,"name = "+name);
Log.d(LOG_TAG,"age = "+age);
Log.d(LOG_TAG,"isfemale = "+isfemale);
Bundle也可以傳遞序列化物件:
//存入數據
Person person = new Person();//Person是一個JavaBean,實現了Serializable介面
person.setName("lisi");//設定相應的屬性值
person.setAge(27);
person.setIsfemale(true);
mIntent.putExtra(PERSON_KEY,person);
//取出數據
Bundle data = intent.getExtras();
Person person = (Person) data.getSerializable(MainActivity.PERSON_KEY);
Log.d(LOG_TAG,"name = "+person.getName());
Log.d(LOG_TAG,"age = "+person.getAge());
Log.d(LOG_TAG,"isfemale = "+person.isfemale());
其原理是實現了Parcelable介面,它可方便的在不同的進程中傳輸,另外要注意Bundle不支援的數據型別無法在進程中被傳遞,例如進程A進行計算後的結果想要傳遞給進程B,但是結果卻不是Bundle所支援的數據型別,這種情況下可以將在A進程進行的計算過程轉移到B進程中的一個Service裡去做,避免了進程間的通訊問題。
兩個進程通過讀/寫同一個檔案來交換數據,例如A進程把數據寫入檔案,B進程通過讀取這個檔案來獲取數據,這種方式適合對數據同步要求不高的進程之間的通訊,並且要妥善處理併發讀/寫的問題。
Messenger 是一種輕量級的 IPC 方案,通過它可在不同進程中傳遞Message物件。
Messenger.send(Message);
它的底層實現是 AIDL ,它對 AIDL 進行了封裝,更便於進行進程間通訊。Messenger一次只處理一個請求,所以在伺服器端不用考慮執行緒同步的問題。工作原理圖如下圖所示:
Message和Messenger實現了Parcelable介面,所以可以在進程間進行傳遞。Message是我們所要傳遞資訊的載體,Messenger提供了傳遞的渠道,Handler是最終的資訊接受和處理中心,Handler本身是無法進行接受訊息的,只不過在建立Messenger時,Messenger持有 Handler的物件 ,在 Messenger內部呼叫了Handler的handleMessage方法,讓其去處理Message 。
實現方法如下,具體程式碼可參考IPC 之 Messenger 的使用:
伺服器端
用戶端
ContentProvider是Android提供的用來進行不同應用間數據共用的方式,例如我們之前獲取通訊錄/簡訊資訊就是通過ContentProvider實現的,這裏因爲通訊錄/簡訊也是一個應用,那我們的應用要想獲取這些資訊勢必要跨進程/應用通訊,底層就是通過Binder實現的。具體用法參考之前的文章安卓開發之內容提供器。
使用時注意一下幾點:
不僅可以跨進程、還可以跨裝置通訊。
原理參考之前的文章安卓開發網路程式設計學習之Socket
Demo參考Android:這是一份很詳細的Socket使用攻略
AIDL(Android介面定義語言):如果在一個進程中要呼叫另一個進程中物件的方法,可使用AIDL生成可序列化的參數,AIDL會生成一個伺服器端物件的代理類,通過它,用戶端就會間接呼叫伺服器端物件的方法,對我們初學者來說,AIDL的作用就是可以在自己的APP裡系結一個其他APP的service,這樣自己的APP就可以和其他APP互動。當然我們也可以自己寫Binder程式碼去實現進程間通訊,但是你會發現寫的程式碼和AIDL的程式碼差不多,而且AIDL還幫我們自動生成了一些Binder相關程式碼不用我們自己再去寫,大大簡化了開發過程。大致流程可以參考前面Binder原理圖。
AIDL支援的數據型別包括以下幾種:
要注意的是,AIDL除了基本數據型別,其它型別(例如我們自定義的實體型別)的參數必須標上方向:in、out或inout,用於表示在跨進程通訊中數據的流向。
伺服器端:
首先看下目錄結構:
在AndroidStudio中工程目錄的Android檢視下,右鍵new一個AIDL檔案,預設將建立一個與java資料夾同級的aidl資料夾用於存放AIDL檔案,這裏命名爲Book.aidl,可以將原有抽象方法basicTypes刪除:
package com.demo.testaidl;
interface Book {
void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString);
}
新建一個同名的實體類Book實現Parcelable介面:
public class Book implements Parcelable {
private String name;
public Book(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "book name:" + name;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.name);
}
public void readFromParcel(Parcel dest) {
name = dest.readString();
}
protected Book(Parcel in) {
this.name = in.readString();
}
public static final Creator<Book> CREATOR = new Creator<Book>() {
@Override
public Book createFromParcel(Parcel source) {
return new Book(source);
}
@Override
public Book[] newArray(int size) {
return new Book[size];
}
};
}
新建AIDL檔案,定義一個介面IBookManager,在這個介面裏宣告兩個方法,分別用於新增Book數據和獲取所有Book數據,因爲AIDL是介面定義語言,所以不能在AIDL檔案裡對方法進行實現:
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
}
然後編譯伺服器端程式碼,會自動生成一個 IBookManager的java類, 裏面書寫了Binder相關程式碼:
IBookManager程式碼如下:
public interface IBookManager extends android.os.IInterface
{
/** Default implementation for IBookManager. */
public static class Default implements com.demo.testaidl.IBookManager
{
@Override public java.util.List<com.demo.testaidl.Book> getBookList() throws android.os.RemoteException
{
return null;
}
@Override public void addBook(com.demo.testaidl.Book book) throws android.os.RemoteException
{
}
@Override
public android.os.IBinder asBinder() {
return null;
}
}
/** Local-side IPC implementation stub class. */
public static abstract class Stub extends android.os.Binder implements com.demo.testaidl.IBookManager
{
private static final java.lang.String DESCRIPTOR = "com.demo.testaidl.IBookManager";
/** Construct the stub at attach it to the interface. */
public Stub()
{
this.attachInterface(this, DESCRIPTOR);
}
/**
* Cast an IBinder object into an com.demo.testaidl.IBookManager interface,
* generating a proxy if needed.
*/
public static com.demo.testaidl.IBookManager asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof com.demo.testaidl.IBookManager))) {
return ((com.demo.testaidl.IBookManager)iin);
}
return new com.demo.testaidl.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
{
java.lang.String descriptor = DESCRIPTOR;
switch (code)
{
case INTERFACE_TRANSACTION:
{
reply.writeString(descriptor);
return true;
}
case TRANSACTION_getBookList:
{
data.enforceInterface(descriptor);
java.util.List<com.demo.testaidl.Book> _result = this.getBookList();
reply.writeNoException();
reply.writeTypedList(_result);
return true;
}
case TRANSACTION_addBook:
{
data.enforceInterface(descriptor);
com.demo.testaidl.Book _arg0;
if ((0!=data.readInt())) {
_arg0 = com.demo.testaidl.Book.CREATOR.createFromParcel(data);
}
else {
_arg0 = null;
}
this.addBook(_arg0);
reply.writeNoException();
return true;
}
default:
{
return super.onTransact(code, data, reply, flags);
}
}
}
private static class Proxy implements com.demo.testaidl.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.demo.testaidl.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.demo.testaidl.Book> _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
boolean _status = mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
if (!_status && getDefaultImpl() != null) {
return getDefaultImpl().getBookList();
}
_reply.readException();
_result = _reply.createTypedArrayList(com.demo.testaidl.Book.CREATOR);
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
@Override public void addBook(com.demo.testaidl.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);
}
boolean _status = mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
if (!_status && getDefaultImpl() != null) {
getDefaultImpl().addBook(book);
return;
}
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
}
public static com.demo.testaidl.IBookManager sDefaultImpl;
}
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 static boolean setDefaultImpl(com.demo.testaidl.IBookManager impl) {
if (Stub.Proxy.sDefaultImpl == null && impl != null) {
Stub.Proxy.sDefaultImpl = impl;
return true;
}
return false;
}
public static com.demo.testaidl.IBookManager getDefaultImpl() {
return Stub.Proxy.sDefaultImpl;
}
}
public java.util.List<com.demo.testaidl.Book> getBookList() throws android.os.RemoteException;
public void addBook(com.demo.testaidl.Book book) throws android.os.RemoteException;
}
其中一些重要的類與變數:
編譯的時候可能會報錯,Book類未被發現啥的,這是因爲我們是在src/main/aidl資料夾下建立Book.java的,實際上這將因爲找不到Book.java而報錯,因爲在Android Studio中使用Gradle構建專案時,預設是在src/main/java資料夾中查詢java檔案的,如果把Book.java放在src/main/aidl對應包名下,自然就會找不到這個檔案了,所以需要修改app的build.gradle檔案,在sourceSets下新增對應的原始檔路徑,即src/main/aidl:
sourceSets {
main {
java.srcDirs = ["src/main/java", "src/main/aidl"]
}
}
接着建立一個Service,實現AIDL的介面函數並暴露AIDL介面,這裏就叫做AIDLService吧:
public class AIDLService extends Service {
private final String TAG = "Server";
private List<Book> bookList;
public AIDLService() {
}
@Override
public void onCreate() {
super.onCreate();
//因爲AIDL方法是在伺服器端的Binder執行緒池中執行的,當有多個用戶端同時連線時,可能存在多個執行緒同時存取mStuList物件的情況
//CopyOnWriteArrayList支援併發讀寫,可以保證執行緒安全
bookList = new CopyOnWriteArrayList<>();
initData();
}
private void initData() {
Book book1 = new Book("Android第一行程式碼");
Book book2 = new Book("Android開發藝術探索");
Book book3 = new Book("Android羣英傳");
bookList.add(book1);
bookList.add(book2);
bookList.add(book3);
}
private final IBookManager.Stub stub = new IBookManager.Stub() {
@Override
public List<Book> getBookList() throws RemoteException {
return bookList;
}
@Override
public void addBook(Book book) throws RemoteException {
if (book != null) {
bookList.add(book);
} else {
Log.e(TAG, "接收到了一個空物件 InOut");
}
}
};
@Override
public IBinder onBind(Intent intent) {
return stub;
}
}
這裏我們考慮單個應用多進程的情況,多應用的話之後也會提一下。AIDLService伺服器端另起一個進程,在AndroidManifest.xml組態檔中,宣告android:process=":remote",即可建立一個新的進程實現單應用多進程,從而模擬進程間通訊。
<service android:name=".AIDLService"
android:process=":remote"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.demo.testaidl.AIDLService"/>
</intent-filter>
</service>
用戶端
用戶端和伺服器端肯定是在不同的進程中,所以用戶端要想通過AIDL與遠端伺服器端通訊,那麼必須也要有伺服器端的這份AIDL程式碼。這裏分爲兩種情況:
伺服器端與用戶端是兩個獨立應用,也就是多應用
把伺服器端的aidl資料夾整個複製到用戶端的與java資料夾同級的目錄下,保持用戶端和伺服器端的aidl資料夾的目錄結構一致。這種情況下需要注意的是,如果前面的Book.java檔案是放置src/main/java對應包名路徑下,則在拷貝aidl資料夾到用戶端的同時,也要將對應的Book.java一併拷貝到用戶端相同的包名路徑下。
我們上面考慮的單應用多進程
這種情況下因爲用戶端與伺服器端同屬一個應用,兩個進程都可以使用這份AIDL程式碼,則不需要拷貝。用戶端進程即主進程,在MainActivity.java中系結遠端AIDLService,就可以向伺服器端進程remote發起請求了,修改MainActivity程式碼:
public class MainActivity extends AppCompatActivity implements View.OnClickListener{
private Button bind,add,get,unbind;
private IBookManager mIBookManager;
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//獲取遠端服務Binder的代理
mIBookManager = IBookManager.Stub.asInterface(service);
if(mIBookManager == null){
return;
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initViews();
bind.setOnClickListener(this);
add.setOnClickListener(this);
get.setOnClickListener(this);
unbind.setOnClickListener(this);
}
private void initViews() {
bind = findViewById(R.id.bind);
add = findViewById(R.id.add);
get = findViewById(R.id.get);
unbind = findViewById(R.id.unbind);
}
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.bind:
bindService();
break;
case R.id.add:
addBook();
break;
case R.id.get:
getBookList();
break;
case R.id.unbind:
unbindBookService();
break;
default:
break;
}
}
public void bindService() {
Intent intent = new Intent();
intent.setAction("com.demo.testaidl.AIDLService");
intent.setPackage("com.demo.testaidl");
startService(intent);
bindService(intent, mConnection, BIND_AUTO_CREATE);
}
public void addBook() {
try {
mIBookManager.addBook(new Book("Android安全權威指南"));
} catch (RemoteException e) {
e.printStackTrace();
}
}
public void getBookList() {
try {
List<Book> books = mIBookManager.getBookList();
Log.e("Client", "books:" + books.toString());
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
unbindBookService();
}
private void unbindBookService() {
unbindService(mConnection);
mIBookManager = null;
}
}
修改佈局檔案:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<Button
android:id="@+id/bind"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="系結服務"/>
<Button
android:id="@+id/add"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="新增數據"/>
<Button
android:id="@+id/get"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="獲取數據"/>
<Button
android:id="@+id/unbind"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="解綁服務"/>
</LinearLayout>
效果如下圖所示:
先點選系結服務,然後獲取數據:
再點選新增數據後,點選獲取數據:
使用完後解綁服務即可。
如果我們的應用有很多模組,而每一個模組都需要和伺服器端通訊,那麼我們也要爲每一個模組建立特定的aidl檔案,那麼伺服器端service也會產生很多個,顯然,如果aidl介面變多,那麼service也會跟着變多,那麼這樣的使用者體驗就會非常不好。《Android 開發藝術探索》中給出了一個Binder連線池的概念,即利用一個Binder連線池來管理所有Binder,伺服器端只需要管理這個Binder連線池即可,這樣就能實現一個service管理多個Binder,爲不同的模組返回不同的Binder,以實現進程間通訊。簡單來說Binder執行緒池將每個模組的Binder請求統一轉發到一個遠端Service中去執行,從而避免重複建立Service。
具體步驟如下:每個模組建立自己的AIDL介面並實現此介面,然後向伺服器端提供自己的唯一標識和其對應的Binder物件。伺服器端只需要一個Service,伺服器提供一個queryBinder介面,它會根據業務模組的特徵來返回相應的Binder對像,不同的業務模組拿到所需的Binder物件後就可進行遠端方法的呼叫。
詳細程式碼可參考: Android IPC機制 機製(四):細說Binder連線池
本文還參考了以下文章,如有理解錯誤還請指出: