Android的ADIL

轉載自:http://flrito.cc/notes-androidzhong-aidlde-ji-ben-yong-fa/

早些時候就聽說過AIDL,也常在各種Android面試題、教程甚至大牛採訪中看到過它的身影。可見AIDL在Android開發中的地位十分的重要。

於是決定先從AIDL的一些基本概念和基本用法開始着手學習它,下面是一些整理的筆記。

AIDL的全稱爲Android Interface Definition Language, 顧名思義,它主要就是用來定義接口的一種語言:

AIDL (Android Interface Definition Language) is similar to other IDLs you might have worked with. It allows you to define the programming interface that both the client and service agree upon in order to communicate with each other using interprocess communication (IPC). On Android, one process cannot normally access the memory of another process. So to talk, they need to decompose their objects into primitives that the operating system can understand, and marshall the objects across that boundary for you. The code to do that marshalling is tedious to write, so Android handles it for you with AIDL.

Android Developer的官方文檔中對AIDL做了很好的概括。當作爲客戶的一方和要和作爲服務器的一方進行通信時,需要指定一些雙方都認可的接口, 這樣才能順利地進行通信。而AIDL就是定義這些接口的一種工具。爲什麼要藉助AIDL來定義,而不直接編寫接口呢(比如直接通過Java定義一個Interface)? 這裏涉及到進程間通信(IPC)的問題。和大多數系統一樣,在Android平臺下,各個進程都佔有一塊自己獨有的內存空間,各個進程在通常情況下只能訪問自己的獨有的內存空間,而不能對別的進程的內存空間進行訪問。 進程之間如果要進行通信,就必須先把需要傳遞的對象分解成操作系統能夠理解的基本類型,並根據你的需要封裝跨邊界的對象。而要完成這些封裝工作,需要寫的代碼量十分地冗長而枯燥。因此Android提供了AIDL來幫助你完成這些工作。

從AIDL的功能來看,它主要的應用場景就是IPC。雖然同一個進程中的client-service也能夠通過AIDL定義接口來進行通信,但這並沒有發揮AIDL的主要功能。 概括來說:

如果不需要IPC,那就直接實現通過繼承Binder類來實現客戶端和服務端之間的通信。
如果確實需要IPC,但是無需處理多線程,那麼就應該通過Messenger來實現。Messenger保證了消息是串行處理的,其內部其實也是通過AIDL來實現。
在有IPC需求,同時服務端需要併發處理多個請求的時候,使用AIDL纔是必要的
在瞭解了基本的概念和使用場景之後,使用AIDL的基本步驟如下:

編寫.AIDL文件,定義需要的接口
實現定義的接口
將接口暴露給客戶端調用
下面通過實現一個簡單的遠程Bound Service來練習這幾個步驟:

1. 編寫.AIDL文件,定義需要的接口
在Android Studio下,右鍵src文件夾,選擇新建AIDL文件,並填寫名字,這裏我命名爲IRemoteService

點擊Finish按鈕之後,會發現main下多了一個名字爲AIDL的目錄,目錄下的包名和Java的包名保持一致,包下即是新建的IRemoteService.aidl文件。 內容我們編寫如下:


AIDL的寫法和Java十分類似,這裏我定義了一個sayHello()方法,用來獲取一個從服務端返回的消息HelloMsg。 
這裏的HelloMsg是我自己定義的一個類型。默認情況下,AIDL支持下列所述的數據類型:

  • 所有的基本類型(int、float等)
  • String
  • CharSequence
  • List
  • Map

其中,List和Map中的元素類型必須是上述類型之一或者由其他AIDL生成的接口類型,或者是已經聲明的Pacelable類型。 
List類型可以指定泛型類,比如寫成List<String>, 並且對方接收到的具體實例都是ArrayList 
Map類型不支持指定泛型類,比如Map<String,String>。只能Map表示類型,並且對方接收到的具體實例都是HashMap

在這個IRemoteService例子中,我們希望在進程間傳遞一個HelloMsg對象:他的定義如下:


爲了讓 HelloMsg 能夠在進程間傳遞, 它必須實現 Parcelable 接口, Parcelable 是Android提供的一種序列化方式,如果嫌手寫麻煩的話,通過插件我們可以十分快捷爲現有的類添加 Parcelable 實現:

/*HelloMsg.java*/
import android.os.Parcel;
import android.os.Parcelable;

public class HelloMsg implements Parcelable {
    private String msg;
    private int pid;

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public int getPid() {
        return pid;
    }

    public void setPid(int pid) {
        this.pid = pid;
    }

    public HelloMsg(String msg, int pid) {

        this.msg = msg;
        this.pid = pid;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(this.msg);
        dest.writeInt(this.pid);
    }

    protected HelloMsg(Parcel in) {
        this.msg = in.readString();
        this.pid = in.readInt();
    }

    public static final Parcelable.Creator<HelloMsg> CREATOR = new Parcelable.Creator<HelloMsg>() {
        @Override
        public HelloMsg createFromParcel(Parcel source) {
            return new HelloMsg(source);
        }

        @Override
        public HelloMsg[] newArray(int size) {
            return new HelloMsg[size];
        }
    };
}
定義好 HelloMsg.java 之後,還需要新增一個與其同名的 AIDL 文件。那麼同樣按照剛纔的步驟右鍵src文件夾,添加一個名爲HelloMsg的 AIDL 文件。 
這個 AIDL 的編寫十分簡單,只需要簡單的聲明一下要用到的Pacelable類即可,有點類似C語言的頭文件,這個 AIDL 文件是不參與編譯的:

// HelloMsg.aidlpackage 

learn.android.kangel.learning;
parcelable HelloMsg;

注意到parcelable的首字母是小寫的,這算是AIDL一個特殊的地方。 
接下來還需要再IRemoteService.aidl文件中使用import關鍵字導入這個HelloMsg類型。詳細的寫法參考上面的IRemoteService.aidl代碼。 
即便IRemoteService.aidlHelloMsg.aidl位於同一個包下,這裏的import是必須要有的。這也是AIDL一個特殊的地方。

好了,至此編寫.AIDL文件的步驟就基本結束了,這個時候需要make project或者make對應的module,Android SDK就會根據我這裏編寫的.AIDL文件生成對應的Java文件。 
在Android Studio下,可以在build/generated/aidl目錄下找到這些Java文件。

查看IRemoteService.java,可以看到其內部有一個靜態抽象類Stub,這個Stub繼承自Binder類,並抽象實現了其父接口,這裏對應的是IRemoteService這個接口:

public static abstract class Stub extends android.os.Binder implements learn.android.kangel.learning.IRemoteService  

Stub類除了聲明瞭IRemoteService.aidl中的所有方法,還提供了一些有用的helper方法,比如asInterface():

public static learn.android.kangel.learning.IRemoteService asInterface(android.os.IBinder obj)  

這個方法接受一個Binder對象,並將其轉化成Stub對應的接口對象(也就是這裏的IRemoteService)並返回。

對於這些生成的Java文件的進一步研究和學習可以幫助我們更好地理解Android的Binder,我會在之後發佈的學習筆記中做相應的記錄(挖坑233)

2. 實現定義的接口

要實現定義的接口,只需要繼承自生成的Binder類,並實現其中的方法即可:

IRemoteService.Stub mBinder = new IRemoteService.Stub() {
    @Override
    public HelloMsg sayHello() throws RemoteException {
        return new HelloMsg("msg from service at Thread " + Thread.currentThread().toString() + "\n" +
                "tid is " + Thread.currentThread().getId() + "\n" +
                "main thread id is " + getMainLooper().getThread().getId(), Process.myPid());
    }
};

這裏的實現十分簡單,返回一個HelloMsg,消息部分是當前線程的信息,當前線程的id,以及主線程的id,Process Id部分就是當前進程的Id

3. 將接口暴露給客戶端調用

在此之前需要了解Bound Service,關於Bound Service的具體細節可以參考這裏,本次筆記不再做額外記錄。

需要注意一點,如果希望多個Application都能夠通過這個接口與服務端通信,那麼所有使用這個接口的Application的src目錄下都要有對應.aidl文件的副本。

在這個例子中我們編寫一個名爲RemoteServiceService類,並在onBind()方法中返回上述第二步中實現的接口,這樣就把接口傳給了客戶端供其調用:

package learn.android.kangel.learning;

import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.os.Process;
import android.os.RemoteException;
import android.support.annotation.Nullable;
import android.widget.Toast;

/**  * Created by Kangel on 2016/7/21.  */  public class RemoteService extends Service {

    IRemoteService.Stub mBinder = new IRemoteService.Stub() {
        @Override
        public HelloMsg sayHello() throws RemoteException {
            return new HelloMsg("msg from service at Thread " + Thread.currentThread().toString() + "\n" +
                    "tid is " + Thread.currentThread().getId() + "\n" +
                    "main thread id is " + getMainLooper().getThread().getId(), Process.myPid());
        }
    };

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
}

以上三步完成之後,我們來繼續完善這個例子來進行一些測試:

編寫作爲客戶端的Activity:

import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Process;
import android.os.RemoteException;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

import com.hbss.smarterstore.R;


/**  * Created by Kangel on 2016/7/21.  */  public class ClientActivity extends AppCompatActivity {
    private IRemoteService mRemoteService = null;
    private boolean mBind = false;
    private TextView mPidText;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.acticity_client);
        mPidText = (TextView) findViewById(R.id.my_pid_text_view);
        mPidText.setText("the client pid is " + Process.myPid());
    }


    @Override
    protected void onStart() {
        super.onStart();
        Intent intent = new Intent(this, RemoteService.class);
        bindService(intent, mConnection, BIND_AUTO_CREATE);
    }

    @Override
    protected void onStop() {
        super.onStop();
        unbindService(mConnection);
        mBind = false;
    }

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mRemoteService = IRemoteService.Stub.asInterface(service);
            mBind = true;
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            mRemoteService = null;
            mBind = false;
        }
    };

    public void onButtonClick(View view) {
        switch (view.getId()) {
            case R.id.show_pid_button:
                if (mBind) {
                    try {
                        Log.i("HELLO_MSG", "the service pid is " + mRemoteService.sayHello().getPid());
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                }
                break;
            case R.id.say_hello_button:
                if (mBind) {
                    try {
                        Log.i("HELLO_MSG", mRemoteService.sayHello().getMsg());
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }

                }
                break;
        }
    }
}

佈局文件中有兩個Button和一個TextView,Button的點擊事件都在xml文件中完成了註冊。分別用來獲取服務端返回的Pid和返回的Msg。 
TextView用於展示當前Activity所在線程的id。

onServiceConnected()回調中,我們使用IRemoteService.Stub.asInterface(Binder)方法返回我們的接口的引用。接着客戶端就可以通過它來對服務端發送請求了。 
onButtonClick()方法中就是對接口的調用。

如果客戶端和服務端處於同一個進程,onServiceConnected()回調中,是可以通過強制類型轉換將返回的Binder對象轉換爲我們需要的接口對象的,像這樣:

mRemoteService = (IRemoteService) service;

但如果客戶端和服務端處於不同進程,執行這樣的強轉,系統會報錯:

java.lang.ClassCastException: android.os.BinderProxy cannot be cast to learn.android.kangel.learning.IRemoteService

我的對此理解是,由於不同進程之間的內存空間是不能夠互相訪問的,A進程中的對象當然也就不能爲B進程所理解。因此強制類型轉換隻適用於同一個進程中。

在Manifest中聲明作爲服務端的Service和作爲客戶端的Acticity

<activity android:name=".ClientActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
<service
    android:name=".RemoteService"
    android:process=":remote" />

在這裏我爲RemoteService設置了process屬性,讓它運行在與默認進程不同的進程中。

接下來運行我們的應用:


可以看到客戶端進程id爲31704 
嘗試點擊兩個按鈕,查看Log: 

可以看到服務端的進程id爲31720,不同於客戶端進程。 而且可以看到,service所在的主線程id爲1,而處理該請求的線程id爲4621。

來自遠程進程的調用分發自系統爲你的進程所維持的一個線程池中。這也許有點難理解。假如你通過AIDL實現了一個遠程服務端的接口,然後有另外一個客戶端進程調用了該接口中的方法,因爲客戶端和你所實現的服務端處於兩個不同的進程,因此客戶端對於你而言,就是一個遠程進程。當客戶端對接口進行調用時,調用過程並不是由客戶端進程進行處理的。而是由系統進行封裝後,傳遞到服務端進程所持有的一個線程池中進行處理。最終線程池中的其中一個線程會被用來執行調用的具體邏輯。 而具體選擇哪個線程來進行處理,是無法提前預知的。 
因此作爲服務端接口的實現者,應該能夠處理多線程併發的情況,時刻準備好處理來自未知線程的調用,並能保證AIDL接口的實現是線程安全的。

如果服務端和客戶端處於同一個進程,那麼服務端將會在與發起請求的客戶端所處的相同線程上處理該請求。把上述android:process=":remote"屬性去掉,則可以對其進行驗證。 但這種單進程的情況,AIDL的使用實際上是完全沒必要的。

參考鏈接:

AIDL  :  Bound Services