1. 程式人生 > >Android使用AIDL實現跨程序通訊(IPC)

Android使用AIDL實現跨程序通訊(IPC)

前言:在還沒有做任何一件事情之前,千萬不要覺得這件事情很難,因為還沒有開始做內心就已經對這件事情產生了恐懼,這將會阻止你的進步,也許當你動手開始做了這件事後發現其實並不是很難。

一、 AIDL概述

含義:AIDL(Android Interface Definition Language),是android介面定義語言,這種語言定義了一個客戶端和伺服器通訊介面的一個標準、規範。

為什麼要有AIDL?

 我們都知道android中的四大元件Activity,Broadcast,Content Provider,Service,前面我們應該都接觸過除了Service的其他三個元件的程序間通訊的例子,比如:一個應用可以通過顯示意圖啟動另外一個Activity,一個應用傳送一個廣播,然後被其他應用所接受,一個應用對外提供一個Content Provider,然後其他應用使用ContentResolver獲取它提供的資料。這些都是程序間通訊的例子(通常情況下每個應用執行在一個獨立的Linux程序中),那麼Service這個元件也同樣也可以實現垮程序通訊,這就是本篇文章要介紹的AIDL服務。AIDL的出現除了讓Service實現垮程序提供服務外,還有一個重要的原因就是:

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

這是google官方文件對AIDL的一個提示,意思是說“只有當你允許來自不同應用通過你的service實現程序通訊,並且需要在你的service中處理多執行緒的情況下才用AIDL,如果你不需要實現不同應用間即時的程序通訊,那麼,你應該建立一個介面實現Binder,或者,如果你想實現程序通訊但是不需要處理多執行緒,那麼用一個Messenger實現你的介面,但是,無論如何,你都得先理解本地的服務在你實現AIDL之前“

通過上面的這句話我們就非常清楚了AIDL的作用就是讓兩個不同的應用間通過Service進行通訊(程序通訊IPC),並且遠端的Service可以處理多執行緒。簡單來講就是,兩個應用,一個應用對外提供一個遠端Service,其他的應用可以併發地訪問這個Service,即:C/S模式。

二、 AIDL簡單示例

實現步驟:

  1. 建立一個AIDL檔案(副檔名為.aidl);
  2. 服務端實現該AIDL檔案生成的Java介面(系統會自動生成對應的Java介面);
  3. 暴露一個介面給客戶端(通過建立一個Service,在onBind()方法中返回一個Stub類的例項);
  4. 客戶端連線繫結該遠端服務。

按照這個步驟,咋們通過程式碼理解AIDL的使用(這裡基於Android Studio這個工具,eclipse裡面也類似(更簡單))。

1. 建立一個AIDL檔案

 首先我們需要新建一個Module,這個Module是一個服務端應用,可以直接通過as提供的直接新建AIDL檔案,這樣它會自動生成aidl資料夾和預設的一個AIDL檔案(生成的AIDL預設的包名是應用的包名),這個改包名有點麻煩,因為客戶端和服務端都必須要有相同的AIDL檔案(包名也必須相同),所以,下面我們通過全手動的方式建立AIDL檔案,在main資料夾下(project檢視)下面建立一個aidl資料夾,然後在這個資料夾下面建一個包名,包名可以隨意,但是客戶端和伺服器端的AIDL檔案包名必須一致,接下來在這個包名下面建一個檔案,這裡叫IRemoteService.aidl

aidl檔案有它自己的語法(aidl:介面定義語言):

  1. 每個aidl檔案只能定義一個介面(單一介面);
  2. 預設Java中的基本資料型別都支援,如:int, long, char, boolean;
  3. 預設支援String和CharSequence;
  4. 預設支援List(可以選擇加泛型,需要引入List所在的包),但是List中儲存的資料也只能是Java基本資料型別,而且另外一邊(客戶端或服務端)接受的是ArrayList型別;
  5. 預設支援Map(可以選擇加泛型,但泛型只能是基本資料型別或String和CharSequence,需要引入Map所在的包),但同樣,Map中儲存的資料也只能是Java基本資料型別,而且另外一邊(客戶端或服務端)接受的是HashMap型別;
  6. 所有aidl檔案中的註釋都會出現在自動生成的IBinder介面java檔案中(除了匯入包語句之前的註釋);
  7. aidl介面只支援方法,不支援變數。

aidl介面檔案寫法和Java介面的寫法非常類似,就是不要Public修飾符,所以,我們這樣寫:

IRemoteService.aidl

// IRemoteService.aidl
package com.lt.aidl;
// Declare any non-default types here with import statements
interface IRemoteService {
    /**
     * 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);

    /** Request the process ID of this service, to do evil things with it. */
    int getPid();

    /** get name by id */
    String getName(int id);
}

Ctrl + S儲存後我們可以看到編譯器會自動給我們生成(如果沒自動生成,可以重新構建一下這個專案)一個對應的IRemoteService.java檔案,這個檔案在Packages檢視下可以看到:

這裡寫圖片描述

這個自動生成的java檔案我們不需要看懂,所以,不管它,接下來我們進行第二步和第三步,也就是服務端實現該AIDL檔案生成的Java介面(系統會自動生成對應的Java介面)。

2(3). 服務端實現該AIDL檔案生成的Java介面

 開啟這個AIDL生成的Java介面,我們可以發現,裡面有一個內部靜態抽象類Stub,這個類繼承了Binder並實現了這個介面,所以,我們可以直接使用這個Stub類完成遠端服務的搭建。

新建一個Sevice,在onBind方法中返回實現了AIDL Java介面的那個Binder類(new一個Stub類正好),這個類將作為這個Service代理類:

package com.lt.remoteservice.service;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;

import com.lt.aidl.IRemoteService;

/**
 * Created by lt on 2016/3/6.
 */
public class RemoteService extends Service{

    private String[] names = {"呂布","關羽","趙子龍","張飛"};

    /**
     * 返回一個RemoteService代理物件IBinder給客戶端使用
     * @param intent
     * @return
     */
    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }

    private final IRemoteService.Stub mBinder = new IRemoteService.Stub(){

        @Override
        public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {
            System.out.println("Thread: " + Thread.currentThread().getName());
            System.out.println("basicTypes aDouble: " + aDouble +" anInt: " + anInt+" aBoolean " + aBoolean+" aString " + aString);
        }

        @Override
        public int getPid() throws RemoteException {
            System.out.println("Thread: " + Thread.currentThread().getName());
            System.out.println("RemoteService getPid ");
            return android.os.Process.myPid();
        }

        @Override
        public String getName(int id) throws RemoteException {
            return names[id];
        }
    };
}

注意:不要忘了在清單檔案中註冊該Service,並且我們還需要提供一個包含action屬性的intent-filter(客戶端通常是通過隱式意圖來啟動該服務),這個action屬性值可以任意。

4. 客戶端繫結該服務

 新建另一個Module,同樣需要建立aidl檔案,這個檔案要和伺服器端的aidl檔案一模一樣(注意包名),這裡直接複製伺服器端的aidl資料夾在這個Module中。

為了直觀展示通訊效果,我們做一個互動介面,activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:layout_width="match_parent"
                android:orientation="vertical"
                android:layout_height="match_parent"
                >

    <EditText
        android:id="@+id/editText"
        android:hint="輸入查詢ID"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

    <Button
        android:onClick="search"
        android:layout_width="wrap_content"
        android:text="查詢"
        android:layout_height="wrap_content"/>

    <LinearLayout
        android:layout_width="wrap_content"
        android:orientation="horizontal"
        android:layout_height="wrap_content">
        <TextView
            android:text="結果:"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

        <TextView
            android:id="@+id/tv_result"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    </LinearLayout>
</LinearLayout>

重點來了,客戶端繫結遠端服務,先建立一個服務連線

 private ServiceConnection conn = new ServiceConnection() {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            // 這裡的IBinder物件service是代理物件,所以必須呼叫下面的方法轉換成AIDL介面物件
            mRemoteService = IRemoteService.Stub.asInterface(service);
            int pid = 0;
            try {
                pid = mRemoteService.getPid();
                int currentPid = android.os.Process.myPid();
                System.out.println("currentPID: " + currentPid +"  remotePID: " + pid);
                mRemoteService.basicTypes(12, 1223, true, 12.2f, 12.3, "有夢就要去追,加油!");
            } catch (RemoteException e) {
                e.printStackTrace();
            }
            System.out.println("bind success! " + mRemoteService.toString());
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            mRemoteService = null;
            System.out.println(mRemoteService.toString() +" disconnected! ");
        }
    };

注意:

  1. 連線的兩個方法,一個是服務連線成功後回撥,一個是服務端開連線時回撥;
  2. 連線成功後在onServiceConnected方法中返回了一個IBinder物件service,這個Service物件必須通過IRemoteService.Stub.asInterface(service);轉換成AIDL物件,這個物件將作為遠端服務的代理物件。

通過隱式意圖繫結遠端服務

// 連線繫結遠端服務
Intent intent = new Intent();
// action值為遠端服務的action,即上面我們在服務端應用清單檔案的action
intent.setAction("lt.test.aidl");
intent.setPackage("com.lt.remoteservice");
isConnSuccess = bindService(intent, conn, Context.BIND_AUTO_CREATE);

注意:

  1. android 5.0 中對service隱式啟動做了限制,必須通過設定action和package;
  2. package是指要啟動的那個服務所在的包名,這裡即伺服器端的那個應用的包名;
  3. 繫結服務有可能會失敗(如客戶端和伺服器端的AIDL檔案不一致)。

介面互動,響應button事件:

public void search(View view){
    if(isConnSuccess){
        // 連線成功
        int id = Integer.valueOf(mEditText.getText().toString());
        try {
            String name = mRemoteService.getName(id);
            mTv_result.setText(name);
        }catch (RemoteException ex) {
            ex.printStackTrace();
        }
    }else{
        System.out.println("連線失敗!");
    }
}

客戶端activity完整程式碼:

package com.lt.remoteclient;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;

import com.lt.aidl.IRemoteService;

public class MainActivity extends Activity {

    private IRemoteService mRemoteService;
    private TextView mTv_result;
    private EditText mEditText;
    private boolean isConnSuccess;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mEditText = (EditText) findViewById(R.id.editText);
        mTv_result = (TextView) findViewById(R.id.tv_result);

        // 連線繫結遠端服務
        Intent intent = new Intent();
        intent.setAction("lt.test.aidl");
        intent.setPackage("com.lt.remoteservice");
        isConnSuccess = bindService(intent, conn, Context.BIND_AUTO_CREATE);
    }

    public void search(View view){
        if(isConnSuccess){
            // 連線成功
            int id = Integer.valueOf(mEditText.getText().toString());
            try {
                String name = mRemoteService.getName(id);
                mTv_result.setText(name);
            }catch (RemoteException ex) {
                ex.printStackTrace();
            }
        }else{
            System.out.println("連線失敗!");
        }
    }

    private ServiceConnection conn = new ServiceConnection() {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            // 這裡的IBinder物件service是代理物件,所以必須呼叫下面的方法轉換成AIDL介面物件
            mRemoteService = IRemoteService.Stub.asInterface(service);
            int pid = 0;
            try {
                pid = mRemoteService.getPid();
                int currentPid = android.os.Process.myPid();
                System.out.println("currentPID: " + currentPid +"  remotePID: " + pid);
                mRemoteService.basicTypes(12, 1223, true, 12.2f, 12.3, "有夢就要去追,加油!");
            } catch (RemoteException e) {
                e.printStackTrace();
            }
            System.out.println("bind success! " + mRemoteService.toString());
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            mRemoteService = null;
            System.out.println(mRemoteService.toString() +" disconnected! ");
        }
    };

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbindService(conn);
    }
}

執行服務端應用,然後在執行客戶端應用,測試結果:

這裡寫圖片描述

觀察後臺日誌列印:

這裡寫圖片描述

可以看到,服務端和客戶端應用處在兩個不同的程序中,並且客戶端可以像傳遞基本型別的資料,同時客戶端也可以從遠端服務端應用取得資料(查詢結果),這就通過遠端Service完成了兩個應用間的通訊(垮程序通訊IPC)。但是,出現了一個問題,上面我們只是傳遞了基本型別的資料,而沒有傳遞物件這種非基本型別的資料,難道不能傳遞非基本型別資料嗎?答案是當然可以,下面我們實現垮程序傳遞一個物件。

三、 AIDL垮程序傳遞物件

 要通過AIDL垮程序傳遞非基本資料型別物件,那麼這個物件需要實現Parcelable介面( android系統內部會將這個物件型別分解成基本資料型別,然後在程序間傳遞)。

實現步驟(新建一個類):

  1. 實現Parcelable介面;
  2. 覆蓋writeTowriteToParcel(Parcel dest, int flags)Parcel方法,這個方法攜帶了當前物件的狀態和將它寫入Parcel中;
  3. 新增一個靜態成員變數CREATOR,這個變數是一個實現了Parcelable.Creator interface.介面的物件;
  4. 建立一個這個類對應的aidl檔案,即檔名和類名一樣,副檔名為.aidl

1(2)(3). 新建一個類實現Parcelable介面

Person.java

package com.lt.aidl;

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

/**
 * Created by lt on 2016/3/8.
 */
public class Person implements Parcelable{

    private String name;
    private int age;

    private Person(Parcel in)
    {
        readFromParcel(in);
    }

    private void readFromParcel(Parcel in) {
        this.name = in.readString();
        this.age = in.readInt();
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    /**
     * 在想要進行序列號傳遞的實體類內部一定要宣告該常量。常量名只能是CREATOR,型別也必須是
     * Parcelable.Creator<T>  T:就是當前物件型別
     */
    public static final Creator<Person> CREATOR = new Creator<Person>() {

        /***
         * 根據序列化的Parcel物件,反序列化為原本的實體物件
         * 讀出順序要和writeToParcel的寫入順序相同
         */
        @Override
        public Person createFromParcel(Parcel in) {
            return new Person(in.readString(),in.readInt());
        }

        /**
         * 建立一個要序列化的實體類的陣列,陣列中儲存的都設定為null
         */
        @Override
        public Person[] newArray(int size) {
            return new Person[size];
        }
    };

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

    /**
     * 將物件寫入到Parcel(序列化)
     * @param dest:就是物件即將寫入的目的物件
     * @param flags: 有關物件序列號的方式的標識
     * 這裡要注意,寫入的順序要和在createFromParcel方法中讀出的順序完全相同。例如這裡先寫入的為name,
     * 那麼在createFromParcel就要先讀name
     */
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(name);
        dest.writeInt(age);
    }

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }
}

4. 建立該類對應的aidl檔案

在Person類所在的包建立一個對應的aidl檔案Person.aidl,檔案內容:

package com.lt.aidl;

// Declare Rect so AIDL can find it and knows that it implements
// the parcelable protocol.
parcelable Person;

這裡現在的專案結構為:

這裡寫圖片描述

OK,將新建的Person.javaPerson.aidl往客戶端相同的地方複製一份,然後重新構建整個專案,如果直接這樣Android Studio在構建的時候會報匯入包出錯(AIDL自定義型別匯入失敗),這個問題折騰了我幾個小時(說多了都是淚…),解決辦法:

在專案grade檔案中新增如下程式碼將AIDL也作為原始檔夾:

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']
    }
}

加入這段程式碼後整個grade檔案內容為:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.1"

    defaultConfig {
        applicationId "com.lt.remoteclient"
        minSdkVersion 11
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    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']
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:23.0.1'
}

然後重新構建一下專案,OK,完美解決(激動…),分別執行兩個應用後的測試結果:

這裡寫圖片描述

看到這個結果顯示應該知道在java程式碼中做了哪些改變,只是個測試傳遞物件資料的問題,所以這裡就不帖出程式碼了,在文章結尾將會貼上原始碼。

到這裡AIDL的使用也就介紹完了

總結:

 AIDL作為一個介面定義語言,它有它自己的語法,其語法和定義java介面類似,所以有人也通過建立一個介面,然後更改一下字尾名為aidl來建立AIDL介面檔案,需要注意的是AIDL只有當不同的應用需要即使通訊,並且需要處理多執行緒的情況下才會使用,不然可以使用其他程序通訊的方式來實現,還有,AIDL預設支援大部分資料型別,但如果要傳遞物件資料,那麼需要採取一些措施才行。