天天看點

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視圖下可以看到:

Android使用AIDL實作跨程式通訊(IPC)

這個自動生成的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 = ;
            try {
                pid = mRemoteService.getPid();
                int currentPid = android.os.Process.myPid();
                System.out.println("currentPID: " + currentPid +"  remotePID: " + pid);
                mRemoteService.basicTypes(, , true, f, , "有夢就要去追,加油!");
            } 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 = ;
            try {
                pid = mRemoteService.getPid();
                int currentPid = android.os.Process.myPid();
                System.out.println("currentPID: " + currentPid +"  remotePID: " + pid);
                mRemoteService.basicTypes(, , true, f, , "有夢就要去追,加油!");
            } 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);
    }
}
           

運作服務端應用,然後在運作用戶端應用,測試結果:

Android使用AIDL實作跨程式通訊(IPC)

觀察背景日志列印:

Android使用AIDL實作跨程式通訊(IPC)

可以看到,服務端和用戶端應用處在兩個不同的程序中,并且用戶端可以像傳遞基本類型的資料,同時用戶端也可以從遠端服務端應用取得資料(查詢結果),這就通過遠端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 ;
    }

    /**
     * 将對象寫入到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;
           

這裡現在的項目結構為:

Android使用AIDL實作跨程式通訊(IPC)

OK,将建立的

Person.java

Person.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 
    buildToolsVersion "23.0.1"

    defaultConfig {
        applicationId "com.lt.remoteclient"
        minSdkVersion 
        targetSdkVersion 
        versionCode 
        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,完美解決(激動…),分别運作兩個應用後的測試結果:

Android使用AIDL實作跨程式通訊(IPC)

看到這個結果顯示應該知道在java代碼中做了哪些改變,隻是個測試傳遞對象資料的問題,是以這裡就不帖出代碼了,在文章結尾将會貼上源碼。

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

總結:

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

demo下載下傳:http://download.csdn.net/detail/ydxlt/9455949