1. 程式人生 > >Android 中如何使用 AIDL 詳細解析

Android 中如何使用 AIDL 詳細解析

目錄

一、AIDL定義及使用場景

二、AIDL 的使用步驟

1. 先建立一個客戶端,在 aidlclient 模組中建立 Activity:

2. 搭建服務端,在 aidlservice 模組中建立 AIDL 檔案

3. 在 aidlservice 模組中建立服務

4. 在客戶端:aidlclient 模組的 MainActivity 中繫結服務

5. 傳輸複雜資料(自定義類)

三、總結

1. 服務端 

2. 客戶端

四、原始碼下載地址 


一、AIDL定義及使用場景

AIDL(Android Interface definition language):Android 介面定義語言。它的作用就是主要用於不同程序之間的通訊。

我們知道,Android 為每一個應用都分配了一個獨立的虛擬機器,或者說為每一個程序都分配了一個獨立的虛擬機器,通常情況下,一個 App 就是一個程序(當然,我們也可以在 AndroidManifest 中通過配置 android:process 屬性,使某一個元件單獨執行在一個程序中,這樣在一個 App 中就會包含多個程序)。執行在同一個程序中的元件是屬於同一個虛擬機器和同一個 Application 的,同理,執行在不同程序中的元件是屬於兩個不同的虛擬機器和 Application 的。那麼,要使這些程序相互通訊,就可以使用 AIDL。

Android 推出來了 Messenger,它是用來完成應用之間的通訊的,它的底層其實也是 AIDL。那它和 AIDL 有何區別呢?官方文件介紹 AIDL 中有這麼一句話:

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.

翻譯過來的的意思就是:“只有當你允許來自不同的客戶端訪問你的服務並且需要處理多執行緒問題時你才必須使用 AIDL ”,其他情況下你都可以選擇其他方法,如使用 Messenger,也能跨程序通訊。可見 AIDL 是處理多執行緒、多客戶端併發訪問的。而 Messenger 是單執行緒處理。此外,Messager 主要是為了傳遞訊息的,如果我們想跨程序呼叫服務端的方法,Messager 就無法做到了,可以用 AIDL 來解決。

常見的是將 Service 獨立執行在一個程序當中,例如我們經常接觸的音樂服務、導航服務等。由於不同的虛擬機器(不同程序對應不同的虛擬機器)在記憶體分配上有不同的地址空間,所以它可以保證服務不被程式的其他部分所幹擾。也正是因為在不同的程序中,要使它們之間進行通訊,就要藉助 AIDL 了。


二、AIDL 的使用步驟


此處,為了直觀一點兒,我們建立一個服務端、一個客戶端。讓它們執行在兩個程序當中。


1. 先建立一個客戶端,在 aidlclient 模組中建立 Activity:

 建立的 Activity 及其佈局檔案如下所示:

 activity_main.xml:

<?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"
    android:gravity="center_horizontal"
    tools:context=".MainActivity">

        <Button
            android:id="@+id/btn_bind"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="繫結服務-Client" />

        <Button
            android:id="@+id/btn_simple"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="傳輸基本型別資料" />

        <Button
            android:id="@+id/btn_complicated"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="傳輸複雜資料" />
</LinearLayout>

Activity 程式碼:

package com.example.aidl_test;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity implements View.OnClickListener{
    private Button bindBtn,simpleBtn,complicated;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }
    @Override
    public void onClick(View v){
        switch (v.getId()){
            case R.id.btn_bind: //繫結服務
                break;
            case R.id.btn_simple:   //傳輸基本型別的資料,如int、double等
                break;
            case R.id.btn_complicated:  //傳輸複雜資料型別,如自定義類
                break;
            default:
                break;
        }
    }

    private void initView() {
        bindBtn = (Button)findViewById(R.id.btn_bind);
        simpleBtn = (Button)findViewById(R.id.btn_simple);
        complicated = (Button)findViewById(R.id.btn_complicated);

        bindBtn.setOnClickListener(this);
        simpleBtn.setOnClickListener(this);
        complicated.setOnClickListener(this);
    }
}

2. 搭建服務端,在 aidlservice 模組中建立 AIDL 檔案

在包名上選擇 File->New->AIDL->AIDL File,如下所示:

這裡就用預設的名稱了,點選 finish,自動生成的程式碼如下所示:

// IMyAidlInterface.aidl
package com.example.aidlservice;

// Declare any non-default types here with import statements

interface IMyAidlInterface {
    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
}

在介面內部,系統幫我們自動生成了一個方法,通過註釋,我們知道,利用 AIDL 傳遞 int,long,boolean,float,double,String 這些資料的時候是不需要別的工作的,直接傳就好了。

在這個介面內宣告的方法,就是服務端可以向外提供的,供客戶端呼叫的方法。這裡我們先把系統的這個方法刪除,先以傳輸上述基本資料型別為例,寫一個自己的方法。

注意:這個介面內的方法是不能帶有許可權修飾符的。

interface IMyAidlInterface {
    void transmitSimpleData(int num);
}

隨後,我們需要編譯一下,只有編譯了,系統才會根據這個 AIDL 檔案生成對應的 Java 程式碼,不然是用不了的。編譯步驟為:Build->Make Moudle 'aidlservice'。如下圖所示:

編譯成功後,會在該模組的 “build->generated->source->aidl->debug->包名” 下生成相同名稱的 Java 檔案。如下圖所示:

3. 在 aidlservice 模組中建立服務

有了 AIDL 檔案中,我們聲明瞭一個自己的方法,這就等同於服務端對外丟擲了承諾,隨後客戶端就會要求你實現承諾,因此必須實現 AIDL 檔案中宣告的方法(承諾)。在 aidlservice 的 Java 包中建立一個自定義的 Service。我們利用 IDE 直接生產,如下:

在下一步,注意要勾選 “Exported” 和 “Enabled” 這兩個選項屬性。“Exported” 屬性表示是否允許除了當前程式之外的其它程式訪問這個服務,若該屬性為 false,就會導致客戶端呼叫不起來;“Enabled” 屬性表示是否啟用這個服務。

接著,我們修改生成的這個 MyService,有兩處需要改動,其一是 AndroidManifest 中,修改如下:

<service
    android:name=".MyService"
    android:enabled="true"
    android:exported="true"
    android:process=":remote">
    <intent-filter>
        <action android:name="com.example.aidlservice.MyService"/>
    </intent-filter>
</service>

我們將這個服務設定為隱式啟動的,因此設定了 action 屬性。android:process=":remote" 則表明該服務將在一個獨立的程序中執行。這裡的“:remote”是可以寫別的名稱的,只是大家都習慣寫它了。

這裡補充一個知識點,不理解的可以忽略。當開啟一個獨立的程序時,有兩種寫法,如下所示:

android:process=":remote"
android:process="com.example.aidlservice.remote"

這兩種寫法是有區別的, ":” 的含義是要在當前的程序名前面附加上當前的包名,是簡寫方式,在此其完整的程序名為 “com.example.aidlservice:remote”, 這種程序是屬於當前程序的私有程序,其他應用的元件不可以和它跑在同一個程序中;而程序名不以“:”開頭的程序屬於全域性程序,其他應用通過ShareUID方式可以和它跑在同一個程序中。(Android 系統會為每個應用分配一個唯一的 UID,具有相同 UID 的應用才能共享資料。具體要求的話,需要兩個應用擁有相同的 ShareUID 並且簽名相同才可以。)

MyService 第二處需要改動的地方,就是其自身的實現了。程式碼如下:

public class MyService extends Service {

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

    IMyAidlInterface.Stub mBinder = new IMyAidlInterface.Stub() {
        @Override
        public void transmitSimpleData(int num) throws RemoteException {
            Log.i("MyService", "MyService 傳輸的基本型別資料為:" + num);
        }
    };
}

其中,必須實現 onBind() 方法,並在其中返回 IBinder 物件。下方建立了一個 Stub 物件(即 mBinder),在其內部我們就可以重寫剛剛在 AIDL 裡宣告的方法了,這樣就對外具備了履行承諾的能力,最後將這個 Stub 物件通過 onBind() 方法返回,這樣服務端就寫好了。至於 Service 中的其它方法,則可以按需重寫。

4. 在客戶端:aidlclient 模組的 MainActivity 中繫結服務

首先需要將 aidlservice 模組中的整個 aidl 資料夾拷貝到 aidlclient 的 mian 目錄下(客戶端的 aidl 包必須和服務端一致,否則會序列化失敗,無法 bindservice),然後執行 "Build->Make Moudle 'aidlclient",編譯一下,生成 AIDL 對應的 Java 檔案(類似第2步)。

接著修改 aidlclient 模組中的 MainActivity,如下所示:

package com.example.lichaoqiang.aidl_test;

import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

import com.example.aidlservice.IMyAidlInterface;

public class MainActivity extends AppCompatActivity implements View.OnClickListener{
    private Button bindBtn,simpleBtn,complicated;

    private IMyAidlInterface mMyAidlInterface;
    private ServiceConnection connection = new ServiceConnection(){
        @Override
        public void onServiceConnected(ComponentName name, IBinder service){
            mMyAidlInterface = IMyAidlInterface.Stub.asInterface(service);
        }

        @Override
        public void onServiceDisconnected(ComponentName name){
            mMyAidlInterface = null;
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }
    @Override
    public void onClick(View v){
        switch (v.getId()){
            case R.id.btn_bind: //繫結服務
                Intent intent = new Intent("com.example.aidlservice.MyService");
                intent.setPackage("com.example.aidlservice");
                bindService(intent,connection,BIND_AUTO_CREATE);
                break;
            case R.id.btn_simple:   //傳輸基本型別的資料,如int、double等
                if (mMyAidlInterface != null){
                    try{
                        mMyAidlInterface.transmitSimpleData(1000);
                    }catch (RemoteException e){
                        e.printStackTrace();
                    }
                }
                break;
            case R.id.btn_complicated:  //傳輸複雜資料型別,如自定義類
                break;
            default:
                break;
        }
    }

    private void initView() {
        bindBtn = (Button)findViewById(R.id.btn_bind);
        simpleBtn = (Button)findViewById(R.id.btn_simple);
        complicated = (Button)findViewById(R.id.btn_complicated);

        bindBtn.setOnClickListener(this);
        simpleBtn.setOnClickListener(this);
        complicated.setOnClickListener(this);
    }

    @Override
    protected void onDestroy () {
        super.onDestroy();
        if (connection != null) {
            unbindService(connection);
        }
    }
}

首先,我們建立一個 AIDL 物件(此處指 mMyAidlInterface),然後在 ServiceConnection 的連線成功方法裡,將 IBinder 通過 Stub 的 asInterface 來給 AIDL 賦值,然後在連線服務的按鈕裡建立一個 Intent 物件,因為是利用隱式 Intent 啟動的,所以需要給它 action 標籤裡的內容,另外需要注意的是,Android5.0 之後 Intent 還必須加上目標的包名,不然會報錯的。最後通過 bindService 方法將 Activity 也連線上。再在  simpleBtn 按鈕的回撥裡,呼叫 AIDL 物件裡  transmitSimpleData 方法,就可以呼叫服務,並將值傳給 Service。最後,不用的時候解除繫結。

小功告成,現在可以測試一下了。注意檢視日誌時,繫結服務後,程序選擇為 “com.example.aidlservice:remote”。接下來我們介紹複雜資料(自定義類)的傳輸。


在此需要補充說明:因為我們在此處是客戶端和服務端分別處於兩個模組中,所以需要將服務端的整個 aidl 資料夾拷貝到 客戶端,但如果客戶端和服務端在一個包中時,例如,我們可以在 aildservice 模組中建立一個 MainActivity,程式碼和 aidlclient 模組中的 MainActivity 一樣。由於我們已經配置 MyService 執行在獨立程序中了(android:process屬性),因此,此時 aidlservice 模組中的 MainActivity 和 MyService 也是執行在兩個程序中的。這時,我們就不需要執行復制 aidl 資料夾的操作了,因為它們已經在一個包中了。我會在後面給出原始碼。

5. 傳輸複雜資料(自定義類)

為了方便介紹,我們在此處在 aidlservice 模組的 MainActivity 中編寫客戶端的相關程式碼,當然,你也可以在 aidlclient 的 MainActivity 中實現同樣的功能(我在原始碼中都會給出),它們的區別也只在於是否需要複製上述提到的那個 aidl 資料夾。

首先,我們建立一個自定義類 Book.java,這個類需要實現 Parcelable 介面(以實現序列化,AIDL 預設只能傳遞基本型別, 如果想傳遞自己的物件, 需要利用 Parcelable)。如果熟悉 Parcelable 的書寫方法,那當然可以直接寫,但 Android Studio 為我們提供了一個更加簡便的外掛:Android parcelable code generator,它可以自動生成序列化 Parcelable 對應的程式碼。在 Settings->Plugins 中搜索、安裝這個外掛即可。安裝完成後,重啟  Android Studio,接著在 aidlservice 模組的 aidl 資料夾下,建立 Book 類,並實現 Parcelable 介面,至於為什麼在這個資料夾下建立,我會在後面說明,如圖:

現在標紅是因為我們還沒實現 Parcelable 的方法。接下來定義 Book 類的變數,程式碼如下:

package com.example.aidlservice;

import android.os.Parcelable;

public class Book implements Parcelable {
    public int bookId;
    public String bookName;
}

定義了一個 int 型的 bookId 和 String 型別的 bookName,接著就可以使用我們剛才提到的外掛了,在該類視窗點選滑鼠右鍵,選擇 Generate...,再選擇 Parcelable,

在接下來的視窗中,選擇需要包含進 Parcelable 的變數,此處我們都選(按住 ctr 鍵,全部選擇),點選 OK,就可以看到外掛自動為我們生產的程式碼了,是不是很簡單呢。

此外,為了方便我們列印傳輸 Book 物件,我們還重寫了 toString() 方法,並添加了一個帶引數的構造方法,詳細程式碼如下:

package com.example.aidlservice;

import android.os.Parcel;
import android.os.Parcelable;

public class Book implements Parcelable {
    public int bookId;
    public String bookName;

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

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

    public Book() {
    }

    public Book(int mBookId,String mBookName){
        this.bookId = mBookId;
        this.bookName = mBookName;
    }

    protected Book(Parcel in) {
        this.bookId = in.readInt();
        this.bookName = 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];
        }
    };

    @Override
    public String toString() {
        return String.format("[bookId:%s, bookName:%s]", bookId, bookName);
    }
}

 接下來,我們還需要在 aidlservice 模組的 aidl 包中,建立一個 Book.aidl 類(注意:必須和自定義的 Parcelable 類同名)。為什麼要有這個 Book.aidl 類呢?因為只有將自定義類在 aidl 中宣告時候,AIDL 才能呼叫這個 自定義的 Book 類。還是在 aidlservice 模組的 aidl 包中建立 Book.aidl 檔案,建立方法同上。建立完成後,目錄結構如下:

修改 Book.aidl 為如下內容,這樣就聲明瞭 Book 類。

package com.example.aidlservice;

parcelable Book;

這裡有一個坑需要說明一下,倘若你將 Book.java 類建立在了 java 包中(即和 MainActivity 在一個包中),那麼在建立 Book.aidl 檔案,將名稱填寫為 Book 時,就會提示不讓建立,這時可以任意先寫一個名稱,待建立完成後,再將其 Rename 成 Book即可,這也是為什麼在 aidl 資料夾中建立 Book.java 的原因之一。

聲明瞭 Book 類,那下一步就可以使用了,修改 IMyAidlInterface.aidl,有兩處修改,一是在其內部需要匯入 Book 類,儘管 Book 類已經和 IMyAidlInterface 位於相同的包中了,這是 AIDL 的特殊之處;二是在介面中定義一個新的方法,用於傳輸複雜資料(和 transmitSimpleData() 方法地位相同)。程式碼如下,注意比較兩個方法的不同之處:

package com.example.aidlservice;

import com.example.aidlservice.Book;

interface IMyAidlInterface {
    void transmitSimpleData(int num);
    void transmitComplicatedData(in Book book);
}

在新加的方法中,其引數列表中有一個 in 符號,因為 AIDL 除了基本資料型別外,其他型別的引數都必須標上這三個方向中的一個:in、out、inout。其中,in 表示輸入型引數,out 表示輸出型引數,inout 表示輸入輸出型引數。此外,AIDL 介面中只支援方法,不支援宣告靜態常量。

接著在 MyService 中去實現這個新新增的方法(注意,引數列表中沒有那個 in 了啊):

public class MyService extends Service {

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

    IMyAidlInterface.Stub mBinder = new IMyAidlInterface.Stub() {
        @Override
        public void transmitSimpleData(int num) throws RemoteException {
            Log.i("MyService", "MyService 傳輸的基本型別資料為:" + num);
        }

        @Override
        public void transmitComplicatedData(Book book) throws RemoteException {
            Log.i("MyService", "MyService 傳輸的複雜資料為:" + book.toString());
        }
    };
}

最後,還要在對應模組的 build.gradle 檔案中,宣告我們的 AIDL 資料夾為資原始檔夾。此處即為 aidlservice 的 build.gradle,在 android 閉包中,新增如下程式碼:


android {

    //其它無關程式碼省略,不貼上了

    sourceSets {
        main {
            manifest.srcFile 'src/main/AndroidManifest.xml'
            java.srcDirs = ['src/main/java', 'src/main/aidl']
            resources.srcDirs = ['src/main/java', 'src/main/aidl']
            aidl.srcDirs = ['src/main/aidl']
            res.srcDirs = ['src/main/res']
            assets.srcDirs = ['src/main/assets']
        }
    }

}

大功告成,在 MainActivity 中編寫程式碼測試一下吧,注意檢視日誌時,繫結服務後,程序選擇為 “com.example.aidlservice:remote”。MainActivity 中程式碼為:

case R.id.btn_complicated:  //傳輸複雜資料型別,如自定義類
                Book book1 = new Book();
                Book book2 = new Book(100,"我的書籍!");
                try{
                    mMyAidlInterface.transmitComplicatedData(book1);
                    mMyAidlInterface.transmitComplicatedData(book2);
                }catch (RemoteException e){
                    e.printStackTrace();
                }
                break;

當然,AIDL 的使用還有很多坑,例如:如何在 AIDL 中定義介面,結合觀察者模式使用、跨程序刪除監聽者、服務端和客戶端執行緒阻塞問題、Binder 死亡監聽及重連方法、AIDL 許可權驗證等等,這就留給讀者繼續深入學習吧。

三、總結

1. 服務端 

服務端首先要建立一個 Service 用來監聽客戶端的連線請求,然後建立一個 AIDL 檔案,將暴露給客戶端的介面在這個 AIDL 檔案中宣告,最後在 Service 中實現這個 AIDL 介面即可。

2. 客戶端

客戶端首先要繫結服務端的 Service,繫結成功後,將服務端返回的 Binder 物件轉成 AIDL 介面所屬的型別,接著就可以呼叫 AIDL 中的方法了。

四、原始碼下載地址 

本文介紹了 AIDL 的使用方法及一些注意事項,原始碼下載地址為:

https://download.csdn.net/download/chaoqiangscu/10715472 (最低只能設定1分,麻煩有積分的就用這個下載吧,沒有積分的可以用下面的另一個連結。謝謝!)

https://github.com/chaoqiangscu/AIDL_Test