從AIDL開始談Android程式間Binder通訊機制

apkbus發表於2015-09-23

本文首先概述了Android的程式間通訊的Binder機制,然後結合一個AIDL的例子,對Binder機制進行了解析。

概述

我們知道,在Android app中的眾多activity,service等元件可以執行在同一程式中,也可以執行在不同程式中。當元件執行在同一程式中進行通訊就顯得比較簡單,在之前的Android執行緒間通訊機制中已經講過了;而當它們執行在不同的程式 中時,就需要使用我們本文中所要介紹的Binder機制了。

Binder作為一種程式間通訊機制,負責提供遠端呼叫的功能(RPC),它的系統元件主要包括四種:Client, Server, ServiceManager, Binder Driver. 它們之間的關係如下圖所示:

1.gif

從圖中我們可以看出,Client, Server, ServiceManager執行在系統的使用者態,而Binder Driver執行在核心態。為了完成Client端到Server端的通訊任務,使用者空間的需要操作Binder Driver提供的/dev/binder檔案來完成互動。那麼ServiceManager的工作是什麼呢?ServiceManager負責管理Server並向Client端提供一個Server的代理介面(proxy)。通過代理介面中定義的方法,Client端就可以使用Server端提供的服務了。整個過程如下:

    • Client端呼叫代理介面的方法,將Client的引數打包為parcel物件傳送給核心空間中BinderDriver;
    • Server端讀取到BinderDriver中的請求資料,將parcel物件解包並處理;
    • 處理好後,將處理結果打包返回給BinderDriver,再交給Client端。

另外,Client端與Server端的呼叫過程是同步的,即在Server返回結果之前,Client端是阻塞的。呼叫過程如下所示:

OK,下面我們通過AIDL的一個例子來分析一下以上過程時如何進行的。

AIDL小栗子

首先,建立一個aidl檔案“ICalculateAIDL.aidl”,這個介面裡面定義了要對外提供的服務,我們這裡定義了計算加法和減法的函式:

package com.cqumonk.calculate.aidl;

interface ICalculateAIDL {
    int add(int a,int b);    
    int minus(int a,int b);
}

建立好後,rebuild一下我們的專案,可以生成同名的java檔案。這是一個介面,裡面包含了一個名為Stub的靜態抽象類,以及我們在aidl檔案中定義的加減法函式。 裡面詳細程式碼在後面分析,介面檔案結構如下:

3.png

然後我們需要實現服務端方面的功能,建立一個service,我們順手把生命週期中各方法都列印出來,並完成加法和減法函式的實現:

public class CalculateService extends Service {  
  private static final String TAG="SERVER";  
  public CalculateService() {  
  }  
  @Override  
  public void onCreate() {    
    super.onCreate();    
    Log.e(TAG,"OnCreate");  
  }  
  @Override  
  public int onStartCommand(Intent intent, int flags, int startId) {    
    Log.e(TAG,"onStartCommand");    
    return super.onStartCommand(intent, flags, startId);  
  }  
  @Override  
  public IBinder onBind(Intent intent) {    
    Log.e(TAG,"onBind");    
    return mBinder;  
  }  
  @Override  
  public void onDestroy() {    
    super.onDestroy();    
    Log.e(TAG,"onDestroy");  
  }  
  @Override  
  public boolean onUnbind(Intent intent) {    
    Log.e(TAG,"onUnbind");    
    return super.onUnbind(intent);  
  }  
  @Override  
  public void onRebind(Intent intent) {    
    Log.e(TAG,"onRebind");    
    super.onRebind(intent);  
  }  
  private final ICalculateAIDL.Stub mBinder=new ICalculateAIDL.Stub(){    
    @Override    
    public int add(int a, int b) throws RemoteException {      
      return a+b;    
    }    
    @Override    
    public int minus(int a, int b) throws RemoteException {      
      return a-b;    
    }  
  };
}

在service中,我們使用剛剛生成的ICalculateAIDL.Stub靜態抽象類建立了一個Binder物件,實現了其中有關計算的業務方法,並在onBind方法中返回它。另外我們還需要在manifest檔案中對該服務進行註冊:

<service  
  android:name=".CalculateService"  
  android:enabled="true"  
  android:exported="true" >  
  <intent-filter>    
    <action android:name="com.cqumonk.adil.calculate"/>    
    <category android:name="android.intent.category.DEFAULT"/>  
  </intent-filter>
</service>

這時候,我們的服務端工作就完成了。我們開始編寫客戶端的程式碼來呼叫這個服務。首先,我們定義一個activity,包含四個按鈕:

4.png

然後我們可以在activity中啟動之前定義好的service並呼叫它所提供的服務:

public class MainActivity extends Activity implements View.OnClickListener { 
  Button mBind; 
  Button mUnbind;  
  Button mAdd;  
  Button mMinus;  
  TextView txt_res;  
  private static final String TAG="CLIENT";  
  private ICalculateAIDL mCalculateAIDL; 
  private boolean binded=false; 
  @Override  
  protected void onCreate(Bundle savedInstanceState) {    
    super.onCreate(savedInstanceState);    
    setContentView(R.layout.activity_main);    
    mBind= (Button) findViewById(R.id.btn_bind);    
    mUnbind= (Button) findViewById(R.id.btn_unbind);    
    mAdd= (Button) findViewById(R.id.btn_add);    
    mMinus= (Button) findViewById(R.id.btn_minus);    
    txt_res= (TextView) findViewById(R.id.txt_res);    
    mBind.setOnClickListener(this);    
    mUnbind.setOnClickListener(this);    
    mAdd.setOnClickListener(this);    
    mMinus.setOnClickListener(this);  
  } 
  @Override  
  protected void onStop() {    
    super.onStop();   
    unbind();  
  }  
  private void unbind(){    
    if (binded){      
      unbindService(mConnection);      
      binded=false;    
    }  
  }  
  @Override  
  public void onClick(View v) {    
    int id=v.getId();   
    switch (id){      
      case R.id.btn_bind:        
        Intent intent=new Intent();       
        intent.setAction("com.cqumonk.adil.calculate");        
        bindService(intent,mConnection, Context.BIND_AUTO_CREATE);        
        break;      
      case R.id.btn_unbind:        
        unbind();        
        break;      
      case R.id.btn_add:        
        if(mCalculateAIDL!=null){          
          try {            
            int res=mCalculateAIDL.add(3,3);            
            txt_res.setText(res+"");          
          } catch (RemoteException e) {            
            e.printStackTrace();          
          }        
        }else{          
          Toast.makeText(this,"please rebind",Toast.LENGTH_SHORT).show();        
        }        
        break;      
      case R.id.btn_minus:        
        if(mCalculateAIDL!=null){          
          try {           
          int res=mCalculateAIDL.minus(9,4);           
          txt_res.setText(res+"");          
         }catch (RemoteException e) {            
          e.printStackTrace();         
         }        
       }else{         
         Toast.makeText(this,"please rebind",Toast.LENGTH_SHORT).show();        
       }        
       break;    
    }  
  } 
  private ServiceConnection mConnection=new ServiceConnection() {    
    @Override    
    public void onServiceConnected(ComponentName name, IBinder service) {      
      Log.e(TAG,"connect");      
      binded=true;      
      mCalculateAIDL=ICalculateAIDL.Stub.asInterface(service);    
    }    
    @Override    
    public void onServiceDisconnected(ComponentName name) {     
      Log.e(TAG,"disconnect");      
      mCalculateAIDL=null;      
      binded=false;    
    }  
  };
}

當我們點選繫結按鈕,觀察日誌:

5.png

點選加法和減法按鈕,都可以完成計算並返回值。然後點選解綁按鈕:

6.png

我們並未發現有連線斷開的日誌列印,這時候,我們繼續點選加減法按鈕,發現仍然可以完成計算功能,這又是為毛呢?我們看到unbind和destroy的日誌列印,說明連線已經斷開,service已經被銷燬,但是我們返回的stub物件仍然是可以繼續使用的。而並不是說service仍然在執行。

過程分析

首先,我們呼叫bindservice方法來啟動了service:

一方面連線成功時呼叫了serviceConnection的onServiceConnected方法。我們從該方法中獲取到一個Binder物件(注意,這個binder並不是我們在service中實現的那個哦。當service時本apk中的service時,這裡返回的是同一個binder),我們通過此binder來與server進行通訊。為了區分,我們稱為clientBinder。

public void onServiceConnected(ComponentName name, IBinder service) {
            Log.e(TAG,"connect");
            binded=true;
            mCalculateAIDL= ICalculateAIDL.Stub.asInterface(service);       
}

另一方面,onBind方法返回了我們實現的Stub物件,其實也是一個Binder,用於和Client進行通訊。 (之前我們定義了一個aidl介面檔案,並根據它生成了ICalculateAIDL介面。這個介面中有我們定義的兩個方法以及一個 靜態抽象內部類Stub,它是一個Binder的子類。)我們來看一下Stub是如何定義的:

public static abstract class Stub extends android.os.Binder implements com.cqumonk.calculate.aidl.ICalculateAIDL

它繼承了Binder類也實現了ICalculateAIDL介面, 我們實現了定義的add和minus方法。我們仍然要強調一下它並不是clientBinder,在負責與clientBinder進行通訊互動的同時,它也維護了service描述符與服務端service的對映。

我們在Client中的onServiceConnected裡呼叫了stub物件的asInterface方法,並將之前得到的clientBinder傳入:

public static com.cqumonk.calculate.aidl.ICalculateAIDL asInterface(android.os.IBinder obj) {            if ((obj == null)) {                
          return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);//根據包名獲取本地實現的一個介面的例項,如果是本地service則可以獲取到
            if (((iin != null) && (iin instanceof com.cqumonk.calculate.aidl.ICalculateAIDL))) {                    return ((com.cqumonk.calculate.aidl.ICalculateAIDL) iin);  //如果得到的例項是ICalculateAIDL的物件,則返回
            }            
            return new com.cqumonk.calculate.aidl.ICalculateAIDL.Stub.Proxy(obj);//如果無法得到本地實現的物件則會返回一個代理物件}

在這個方法中,首先在系統中查詢註冊的的service,如果沒有找到,那麼一定是別的apk實現的service,於是返回一個此service的靜態代理類物件供Client呼叫。 我們來看一下這個代理,建立時我們將clientBinder傳入了,同時也它實現了ICalculateAIDL介面:

private static class Proxy implements com.cqumonk.calculate.aidl.ICalculateAIDL {      
      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 int add(int a, int b) throws android.os.RemoteException {        
        android.os.Parcel _data = android.os.Parcel.obtain();        
        android.os.Parcel _reply = android.os.Parcel.obtain();        
        int _result;        
        try {          
          _data.writeInterfaceToken(DESCRIPTOR);          
          _data.writeInt(a);  //將引數打包         
          _data.writeInt(b);          
          mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0);  //呼叫binderDriver的提供的方法將引數發給服務端          
          _reply.readException();          
          _result = _reply.readInt();  //讀取到返回結果        
        } finally {          
          _reply.recycle();          
          _data.recycle();        
        }        
        return _result;      
       }      
       @Override      
       public int minus(int a, int b) throws android.os.RemoteException {        
         android.os.Parcel _data = android.os.Parcel.obtain();        
         android.os.Parcel _reply = android.os.Parcel.obtain();        
         int _result;        
         try {          
           _data.writeInterfaceToken(DESCRIPTOR);          
           _data.writeInt(a);          
           _data.writeInt(b);          
           mRemote.transact(Stub.TRANSACTION_minus, _data, _reply, 0);          
           _reply.readException();          
           _result = _reply.readInt();        
         } finally {          
           _reply.recycle();         
           _data.recycle();        
         }        
         return _result;      
       }    
     }

代理中也實現了ICalculateAIDL介面定義的方法,我們以add方法為例,裡面將引數打包傳送給Server端。在Server端收到請求後,會呼叫service中我們實現的那個stub物件(mBinder)的onTransact方法:

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_add: {          
         data.enforceInterface(DESCRIPTOR);          
         int _arg0;          
         _arg0 = data.readInt();          
         int _arg1;          
         _arg1 = data.readInt();         
         int _result = this.add(_arg0, _arg1); //呼叫我們實現好的方法          
         reply.writeNoException();          
         reply.writeInt(_result); //把結果返回          
         return true;        
       }        
       case TRANSACTION_minus: {          
         data.enforceInterface(DESCRIPTOR);          
         int _arg0;          
         _arg0 = data.readInt();          
         int _arg1;          
         _arg1 = data.readInt();          
         int _result = this.minus(_arg0, _arg1);          
         reply.writeNoException();          
         reply.writeInt(_result);          
         return true;       
        }      
      }      
      return super.onTransact(code, data, reply, flags);    
    }

呼叫完成後把結果打包返回給Poxy處理,最後返回給客戶端。

總結

由上面的例子我們可以看出,在跨程式通訊的時候,Client端使用的Poxy裡面封裝了一個binder與Server端的stub(也是一個binder物件)進行互動,兩個binder作為介面呼叫BinderDriver的transact來傳送資料包,以及onTransact接收處理資料包。

通過結合AIDL例子,我們對Android程式間的通訊機制進行了分析,如果有錯誤的地方,歡迎指正。

相關文章