Notes: Android中AIDL的基本用法

ConsueloATL 7年前發布 | 10K 次閱讀 AIDL Android開發 移動開發

早些時候就聽說過 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 的主要功能。

概括來說:

  1. 如果不需要IPC,那就直接實現通過 繼承Binder類 來實現客戶端和服務端之間的通信。
  2. 如果確實需要IPC,但是無需處理多線程,那么就應該通過 Messenger 來實現。Messenger保證了消息是串行處理的,其內部其實也是通過 AIDL 來實現。
  3. 在有IPC需求,同時服務端需要并發處理多個請求的時候,使用 AIDL 才是必要的

在了解了基本的概念和使用場景之后,使用 AIDL 的基本步驟如下:

  1. 編寫.AIDL文件,定義需要的接口
  2. 實現定義的接口
  3. 將接口暴露給客戶端調用

下面通過實現一個簡單的遠程Bound Service來練習這幾個步驟:

1. 編寫.AIDL文件,定義需要的接口

在Android Studio下,右鍵src文件夾,選擇新建 AIDL 文件,并填寫名字,這里我命名為 IRemoteService

new_aidl.png

點擊Finish按鈕之后,會發現main下多了一個名字為 AIDL 的目錄,目錄下的包名和Java的包名保持一致,包下即是新建的 IRemoteService.aidl 文件。

內容我們編寫如下:

// IRemoteService.aidl
    package learn.android.kangel.learning;
    // Declare any non-default types here with import statements
    import learn.android.kangel.learning.HelloMsg;

    interface IRemoteService {

        HelloMsg sayHello();
    }

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.java*/
    public class HelloMsg {
        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;
        }
    }

為了讓 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.aidl
package learn.android.kangel.learning;

parcelable HelloMsg;

注意到 parcelable 的首字母是小寫的,這算是 AIDL 一個特殊的地方。

接下來還需要再 IRemoteService.aidl 文件中使用 import 關鍵字導入這個 HelloMsg 類型。詳細的寫法參考上面的 IRemoteService.aidl 代碼。

即便 IRemoteService.aidl 和 HelloMsg.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. 將接口暴露給客戶端調用

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

在這個例子中我們編寫一個名為 RemoteService 的 Service 類,并在 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;

/**
 * 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屬性,讓它運行在與默認進程不同的進程中。

接下來運行我們的應用:

client_activity.png

可以看到客戶端進程id為31704

嘗試點擊兩個按鈕,查看Log:

log_1.png

可以看到服務端的進程id為31720,不同于客戶端進程。

而且可以看到,service所在的主線程id為1,而處理該請求的線程id為4621。

來自遠程進程的調用分發自系統為你的進程所維持的一個線程池中。這也許有點難理解。假如你通過AIDL實現了一個遠程服務端的接口,然后有另外一個客戶端進程調用了該接口中的方法,因為客戶端和你所實現的服務端處于兩個不同的進程,

因此客戶端對于你而言,就是一個遠程進程。當客戶端對接口進行調用時,調用過程并不是由客戶端進程進行處理的。而是由系統進行封裝后,傳遞到服務端進程所持有的一個線程池中進行處理。最終線程池中的其中一個線程會被用來執行調用的具體邏輯。

而具體選擇哪個線程來進行處理,是無法提前預知的。

因此作為服務端接口的實現者,應該能夠處理多線程并發的情況,時刻準備好處理來自未知線程的調用,并能保證 AIDL 接口的實現是線程安全的。

如果服務端和客戶端處于同一個進程,那么服務端將會在與發起請求的客戶端所處的相同線程上處理該請求。把上述 android:process=":remote" 屬性去掉,則可以對其進行驗證。

但這種單進程的情況, AIDL 的使用實際上是完全沒必要的。

 

 

來自:http://www.jianshu.com/p/3303752057be

 

 本文由用戶 ConsueloATL 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!