1. 程式人生 > >Android進程間通信(一):AIDL使用詳解

Android進程間通信(一):AIDL使用詳解

project 實現 remove task 兩種 write sting 流向 onclick

一、概述

AIDL是Android Interface Definition Language的縮寫,即Android接口定義語言。它是Android的進程間通信比較常用的一種方式。

Android中,每一個進程都有自己的Dalvik VM實例,擁有自己的獨立的內存空間,進程與進程之間不共享內存,這就產生了進程間通信的需求。

二、語法

AIDL是Android接口定義語言,是一門語言,所以它擁有自己的語法和特性。

(一)數據類型

AIDL支持的數據類型包括以下幾種:

  1. Java的8種基本數據類型:int,short,long,char,double,byte,float,boolean;
  2. CharSequence類型,如String、SpannableString等;
  3. ArrayList
  4. HashMap<K,V>,並且K和V必須是AIDL所支持的數據類型;
  5. 所有Parceable接口的實現類,因為跨進程傳輸對象時,本質上是序列化與反序列化的過程;
  6. AIDL接口,所有的AIDL接口本身也可以作為可支持的數據類型;

有兩個需要註意的地方:

1、在Java中,如果一個對象和引用它的類在同一個package下,是不需要導包的,即不需要import,而在AIDL中,自定義的Parceable對象和AIDL接口定義的對象必須在所引用的AIDL文件中顯式import進來,不管這些對象和所引用它們的AIDL文件是否在同一個包下。

2、如果AIDL文件中使用到自定義的Parceable對象,則必須再創建一個與Parceable對象同名的AIDL文件,聲明該對象為Parceable類型,並且根據上一條語法規定,在AIDL文件中進行顯式import。

(二)文件類型
  1. 所有AIDL文件都是以.aidl作為後綴的;
  2. 根據用途區分,AIDL文件的有兩種,一種是用於定義接口,另一種是用於聲明parceable對象,以供其他AIDL文件使用;
(三)定向tag

AIDL中,除了基本數據類型,其他類型的方法參數都必須標上數據在跨進程通信中的流向:in、out或inout:

1、in表示輸入型參數:只能由客戶端流向服務端,服務端收到該參數對象的完整數據,但服務端對該對象的後續修改不會影響到客戶端傳入的參數對象;

2、out表示輸出型參數:只能由服務端流向客戶端,服務端收到該參數的空對象,服務端對該對象的後續修改將同步改動到客戶端的相應參數對象;

3、inout表示輸入輸出型參數:可在客戶端與服務端雙向流動,服務端接收到該參數對象的完整數據,且服務端對該對象的後續修改將同步改動到客戶端的相應參數對象;

定向tag需要一定的開銷,根據實際需要去確定選擇什麽tag,不能濫用。

深入理解tag:你真的理解AIDL中的in,out,inout麽?

(四)其他

1、所有AIDL接口都是繼承自IInterface接口的,IInterface接口中只聲明了一個asBinder方法:

public interface IInterface
{
    /**
     * Retrieve the Binder object associated with this interface.
     * You must use this instead of a plain cast, so that proxy objects
     * can return the correct result.
     */
    public IBinder asBinder();
}

2、系統會幫我們為所有用於定義接口的AIDL文件生成相應的java代碼,手寫這份java代碼與用AIDL系統生成實際上是一樣的,AIDL可以方便系統為我們生成固定格式的java代碼。

三、基本用法

在AndroidStudio中工程目錄的Android視圖下,右鍵new一個AIDL文件,默認將創建一個與java文件夾同級的aidl文件夾用於存放AIDL文件,且aidl文件夾下的包名與build.gradle中配置的applicationId一致,而applicationId默認值是應用的包名。

AIDL的底層是基於Binder實現的,而Binder機制也是一種請求-響應式的通信模型,請求方一般稱為Client,響應方稱為Server。

Demo介紹:在一個應用內部新起一個進程作為服務端,服務端提供addStudent和getStudentList兩個方法,分別用於客戶端向服務端添加Student數據和獲取Student列表,Student是自定義對象,只有id和name兩個屬性。源碼下載鏈接。

(一)服務端

新建AIDL文件,定義一個接口,在這個接口裏聲明兩個方法,分別用於添加Student數據和獲取所有Student數據,因為AIDL是接口定義語言,所以不能在AIDL文件裏對方法進行實現:

/aidl/com/sqchen/aidltest/IStudentService.aidl

package com.sqchen.aidltest;

//顯式import
import com.sqchen.aidltest.Student;

interface IStudentService {

    List<Student> getStudentList();

    //定向tag
    void addStudent(in Student student);
}

因為IStudentService.aidl接口中使用到的Student是自定義對象,不屬於Java基本數據類型和CharSequence類型,所以按照語法規定,在IStudentService.aidl中需要顯式import,同時我們要讓Student實現Parceable接口,並且新建一個AIDL文件用於聲明Student類是Parceable類型:

/aidl/com/sqchen/aidltest/Student.java

public class Student implements Parcelable {

    private int id;

    private String name;

    public Student(int id, String name) {
        this.id = id;
        this.name = name;
    }

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

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(id);
        dest.writeString(name);
    }

    public void readFromParcel(Parcel parcel) {
        this.id = parcel.readInt();
        this.name = parcel.readString();
    }

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

        @Override
        public Student[] newArray(int size) {
            return new Student[0];
        }
    };

    private Student(Parcel in) {
        this.id = in.readInt();
        this.name = in.readString();
    }

}

/aidl/com/sqchen/aidltest/Student.aidl

package com.sqchen.aidltest;

parcelable Student;

這裏,我們是在src/main/aidl文件夾下創建Student.java的,實際上這將因為找不到Student.java而報錯,因為在AndroidStudio中使用Gradle構建項目時,默認是在src/main/java文件夾中查找java文件的,如果把Student.java放在src/main/aidl對應包名下,自然就會找不到這個文件了,所以需要修改app的build.gradle文件,在sourceSets下添加對應的源文件路徑,即src/main/aidl

android {
    compileSdkVersion 28
    ...
    sourceSets {
        main {
            java.srcDirs = ["src/main/java", "src/main/aidl"]
        }
    }
}

在將src/main/aidl添加到sourceSets中重新構建項目後,在AndroidStudio的Android視圖下,項目的目錄結構將發生變化,此時會發現aidl文件夾不見了,而在java文件夾下,將出現兩個一樣包名的目錄結構,但這只是在當前視圖下的一種展示方式,將src/main/aidl下的文件也看作是java文件的存放位置,實際上當切換到Project視圖時,會發現AIDL文件還是存在於aidl文件夾下,與java文件夾同級。

如果Student.java是放在src/main/java對應的包名路徑下,則不需要這個步驟。

接著,創建一個Service用來響應Client端的請求:

/java/com/sqchen/aidltest/StudentService.java

public class StudentService extends Service {

    private static final String TAG = "StudentService";

    private CopyOnWriteArrayList<Student> mStuList;

    private Binder mBinder = new IStudentService.Stub() {

        @Override
        public List<Student> getStudentList() throws RemoteException {
            return mStuList;
        }

        @Override
        public void addStudent(Student student) throws RemoteException {
            mStuList.add(student);
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        init();
    }

    private void init() {
        mStuList = new CopyOnWriteArrayList<>();
    }


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

StudentService中,我們創建了一個Binder對象並在onBind方法中返回它,這個Binder對象繼承自IStudentService.Stub,並實現了內部的AIDL方法。

我們用CopyOnWriteArrayList來存放mStuList對象,是因為AIDL方法是在服務端的Binder線程池中執行的,當有多個客戶端同時連接時,可能存在多個線程同時訪問mStuList對象的情況,而CopyOnWriteArrayList支持並發讀寫,可以保證線程安全。

按照AIDL的語法規定,只支持傳輸ArrayList對象,而CopyOnWriteArrayList不是繼承自ArrayList,為什麽也可以傳輸呢?這是因為AIDL中所支持的是抽象的List,而List只是一個接口,雖然服務端返回的是CopyOnWriteArrayList,但在Binder中,它會按照List的規範去訪問數據並最終形成一個新的ArrayList給客戶端。類似的還有ConcurrentHashMap。

為StudentService服務端另起一個進程,在AndroidManifest.xml配置文件中,聲明android:process=":remote",即可創建一個新的進程實現單應用多進程,從而模擬進程間通信。這個進程的名字就是remote

<service
    android:name="com.sqchen.aidltest.StudentService"
    android:process=":remote"
    android:enabled="true"
    android:exported="true"></service>
(二)客戶端

因為客戶端和服務端是在不同的進程中,所以客戶端要想通過AIDL與遠程服務端通信,那麽必須也要有服務端的這份AIDL代碼。

這裏分為兩種情況:

1、服務端與客戶端是兩個獨立應用

把服務端的aidl文件夾整個復制到客戶端的與java文件夾同級的目錄下,保持客戶端和服務端的aidl文件夾的目錄結構一致。這種情況下需要註意的是,如果前面的Student.java文件是放置src/main/java對應包名路徑下,則在拷貝aidl文件夾到客戶端的同時,也要將對應的Student.java一並拷貝到客戶端相同的包名路徑下。

2、服務端與客戶端是同一應用的不同進程

這種情況下因為客戶端與服務端同屬一個應用,兩個進程都可以使用這份AIDL代碼,則不需要拷貝。

客戶端進程即主進程,在MainActivity.java中綁定遠程StudentService,就可以向服務端進程remote發起請求了:

/java/com/sqchen/aidltest/MainActivity.java

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private static final String TAG = "MainActivity";

    private final static String PKG_NAME = "com.sqchen.aidltest";

    private Button btnBind;
    private Button btnAddData;
    private Button btnGetData;
    private Button btnUnbind;

    private IStudentService mStudentService;

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mStudentService = IStudentService.Stub.asInterface(service);
            if (mStudentService == null) {
                Log.i(TAG, "mStudentService == null");
                return;
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        initData();
    }

    private void initView() {
        btnBind = findViewById(R.id.btn_bind);
        btnAddData = findViewById(R.id.btn_add_data);
        btnGetData = findViewById(R.id.btn_get_data);
        btnUnbind = findViewById(R.id.btn_unbind);
        initListener();
    }

    private void initListener() {
        btnBind.setOnClickListener(this);
        btnAddData.setOnClickListener(this);
        btnGetData.setOnClickListener(this);
        btnUnbind.setOnClickListener(this);
    }

    private void initData() {
        mCallback = new ITaskCallback.Stub() {
            @Override
            public void onSuccess(String result) throws RemoteException {
                Log.i(TAG, "result = " + result);
            }

            @Override
            public void onFailed(String errorMsg) throws RemoteException {
                Log.e(TAG, "errorMsg = " + errorMsg);
            }
        };
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_bind:
                bindStudentService();
                break;
            case R.id.btn_add_data:
                addData();
                break;
            case R.id.btn_get_data:
                getData();
                break;
            case R.id.btn_unbind:
                unbindStudentService();
                break;
            default:
                break;
        }
    }

    private void bindStudentService() {
        Intent intent = new Intent(this, StudentService.class);
        intent.setPackage(PKG_NAME);
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
    }

    private void addData() {
        if (mStudentService == null) {
            Log.i(TAG, "mStudentService = null");
            return;
        }
        try {
            mStudentService.addStudent(new Student(1, "陳賢靖"));
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    private void getData() {
        if (mStudentService == null) {
            Log.i(TAG, "mStudentService = null");
            return;
        }
        try {
            List<Student> studentList = mStudentService.getStudentList();
            Log.i(TAG, "studentList = " + studentList);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    private void unbindStudentService() {
        unbindService(mConnection);
        mStudentService = null;
    }

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

在MainActivity.java中,創建4個按鈕,分別用於綁定服務、添加數據、獲取數據、解綁服務:

1、綁定服務

通過bindService方式啟動StudentService,ServiceConnection是用於監視服務端狀態的一個接口,內部方法都在主線程被調用,所以不能在該接口的方法中進行耗時操作。

/**
 * Called when a connection to the Service has been established, with
 * the {@link android.os.IBinder} of the communication channel to the
 * Service.
 *
 * <p class="note"><b>Note:</b> If the system has started to bind your
 * client app to a service, it‘s possible that your app will never receive
 * this callback. Your app won‘t receive a callback if there‘s an issue with
 * the service, such as the service crashing while being created.
 *
 * @param name The concrete component name of the service that has
 * been connected.
 *
 * @param service The IBinder of the Service‘s communication channel,
 * which you can now make calls on.
 */
void onServiceConnected(ComponentName name, IBinder service);

onServiceConnected方法是在與Service建立連接時被調用,通過註釋可以發現,如果綁定服務的過程中,Service端如果發生崩潰,該方法將不會被回調。

/**
 * Called when a connection to the Service has been lost.  This typically
 * happens when the process hosting the service has crashed or been killed.
 * This does <em>not</em> remove the ServiceConnection itself -- this
 * binding to the service will remain active, and you will receive a call
 * to {@link #onServiceConnected} when the Service is next running.
 *
 * @param name The concrete component name of the service whose
 * connection has been lost.
 */
void onServiceDisconnected(ComponentName name);

onServiceDisconnected方法是在與Service的連接斷開時被調用,通過註釋可以發現,當Service發生崩潰或者由於某種原因被殺死時,將觸發該回調,但客戶端與Service之間的綁定關系還是存在的,且ServiceConnection對象不會被移除,當Service在下一次被運行起來,那麽還會再次觸發onServiceConnected方法。

通過查看ServiceConnection源碼可以知道,在onServiceConnected方法被觸發之後,就可以對服務端Service進行操作了,但是服務端通過onServiceConnected返回給客戶端的是IBinder對象,我們需要通過mStudentService = IStudentService.Stub.asInterface(service)將IBinder類型的service對象轉化為IStudentService類型對象,然後就可以調用IStudentService的addStudentgetStudentList方法了。

2、添加數據

private void addData() {
    if (mStudentService == null) {
        Log.i(TAG, "mStudentService = null");
        return;
    }
    try {
        mStudentService.addStudent(new Student(1, "陳賢靖"));
    } catch (RemoteException e) {
        e.printStackTrace();
    }
}

先判斷mStudentService對象是否初始化,不為空,則調用addStudent向服務端添加一個Student對象。

3、獲取數據

private void getData() {
    if (mStudentService == null) {
        Log.i(TAG, "mStudentService = null");
        return;
    }
    try {
        List<Student> studentList = mStudentService.getStudentList();
        Log.i(TAG, "studentList = " + studentList);
    } catch (RemoteException e) {
        e.printStackTrace();
    }
}

先判斷mStudentService對象是否初始化,不為空,則調用getStudentList方法獲取服務端的Student列表數據。

4、解綁服務

private void unbindStudentService() {
    unbindService(mConnection);
    mStudentService = null;
}

MainActivity的onDestory中或其他需要的地方調用該方法進行解綁服務。

以上就是AIDL的基本使用,流程可以概括為:

(1)創建服務端的AIDL文件,進行服務端方法的接口定義(IStudentService);

(2)創建服務端的Service,實現AIDL接口定義的方法,並將Binder對象通過onBind方法返回給客戶端;

(3)創建客戶端的AIDL文件,從服務端拷貝即可,但要保持AIDL文件的包名結構在服務端和客戶端是一致的;

(4)客戶端綁定服務端Service,在成功建立與Service的連接之後,拿到服務端返回的Binder對象,並將Binder對象轉為AIDL接口類型的對象(IStudentService);

(5)通過IStudentService類型對象調用Service中的實現方法;

(6)在需要結束與服務端連接的時候,調用unbindService方法進行解綁;

在創建AIDL文件時,如果有報錯,通常說明某個AIDL文件書寫不規範,需要檢查的點有:

1、自定義對象是否實現Parceable接口;

2、引用的AIDL對象是否顯式import;

3、定向tag的使用是否正確;

4、定向tag為inout時,自定義對象是否同時實現writeToParcelreadFromParcel

5、如果有修改過java文件的包名,檢查AIDL文件的包名是否正確(是否與applicationId一致);

當發現問題並修改後,可以嘗試Build->Clean ProjectBuild -> Rebuild以重新刷新或構建項目;

三、回調機制

在基本用法中,只實現了客戶端向服務端發送調用請求的單向通信,但在很多場景下,同時也需要實現服務端主動向客戶端發送數據進行雙向通信,比如在觀察者模式中,當有多個客戶端綁定服務端,如果想要實現在服務端數據變化時主動通知所有與它建立綁定的客戶端時,這個時候就需要用到AIDL的回調機制了。

在服務端aidl文件夾下新建一個AIDL文件,用於定義回調接口,並聲明onSuccess和onFailed方法,這兩個方法是用於業務層的,比如服務端添加數據失敗時調用onFailed,取決於具體場景:

// ITaskCallback.aidl
package com.sqchen.aidltest;

interface ITaskCallback {

    void onSuccess(String result);

    void onFailed(String errorMsg);
}

修改IStudentService.aidl,添加registerunregister方法用於客戶端註冊回調和解除回調:

// IStudentService.aidl
package com.sqchen.aidltest;

import com.sqchen.aidltest.Student;
//註意:aidl接口也要顯式import
import com.sqchen.aidltest.ITaskCallback;

interface IStudentService {

    List<Student> getStudentList();

    void addStudent(inout Student student);

    void register(ITaskCallback callback);

    void unregister(ITaskCallback callback);
}

修改StudentService.java

package com.sqchen.aidltest;

import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.util.Log;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class StudentService extends Service {

    private static final String TAG = "StudentService";

    private CopyOnWriteArrayList<Student> mStuList;

    private static RemoteCallbackList<ITaskCallback> sCallbackList;

    private Binder mBinder = new IStudentService.Stub() {

        @Override
        public void register(ITaskCallback callback) throws RemoteException {
            if (callback == null) {
                Log.i(TAG, "callback == null");
                return;
            }
            sCallbackList.register(callback);
        }

        @Override
        public void unregister(ITaskCallback callback) throws RemoteException {
            if (callback == null) {
                return;
            }
            sCallbackList.unregister(callback);
        }

        @Override
        public List<Student> getStudentList() throws RemoteException {
            return mStuList;
        }

        @Override
        public void addStudent(Student student) throws RemoteException {
            if (mStuList == null) {
                dispatchResult(false, "add student failed, mStuList = null");
            } else {
                mStuList.add(student);
                dispatchResult(true, "add student successfully");
            }
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        Log.i(TAG, "onCreate");
        init();
    }

    private void init() {
        mStuList = new CopyOnWriteArrayList<>();
        sCallbackList = new RemoteCallbackList<>();
    }

    /**
     * 分發結果
     * @param result
     * @param msg
     */
    private void dispatchResult(boolean result, String msg) {
        int length = sCallbackList.beginBroadcast();
        for (int i = 0; i < length; i++) {
            ITaskCallback callback = sCallbackList.getBroadcastItem(i);
            try {
                if (result) {
                    callback.onSuccess(msg);
                } else {
                    callback.onFailed(msg);
                }
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        sCallbackList.finishBroadcast();
    }

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

StudentService.java中,Binder對象實現了IStudentService.aidl中新聲明的兩個方法,registerunregister,並創建了一個RemoteCallbackList

RemoteCallbackList<E extends IInterface> 是系統專門提供的用於跨進程傳遞callback的一種接口,這個接口是泛型,支持管理所有AIDL接口。這裏不能使用普通的List來存放callback,因為在進程間通信時,客戶端的List對象和服務端接收到的List對象不在不同的內存空間中。正是因為不是在同一個內存空間中,不同進程之間的數據不能進行共享,所以才有進程間通信這個機制。

那麽,為什麽RemoteCallbackList能實現傳輸前後都是相同對象呢?查看RemoteCallbackList源碼可以發現,其內部創建了一個ArrayMap用於保存callback:

ArrayMap<IBinder, Callback> mCallbacks = new ArrayMap<IBinder, Callback>();

這個Map的key是IBinder對象,而value是Callback對象,當客戶端通過register方法註冊回調時,將callback傳遞給服務端,服務端再通過RemoteCallbackList.register方法真正將回調進行保存:

//RemoteCallbackList
public boolean register(E callback, Object cookie) {
    synchronized (mCallbacks) {
        if (mKilled) {
            return false;
        }
        // Flag unusual case that could be caused by a leak. b/36778087
        logExcessiveCallbacks();
        IBinder binder = callback.asBinder();
        try {
            Callback cb = new Callback(callback, cookie);
            binder.linkToDeath(cb, 0);
            mCallbacks.put(binder, cb);
            return true;
        } catch (RemoteException e) {
            return false;
        }
    }
}

將我們關心的部分抽出來:

IBinder binder = callback.asBinder();
Callback cb = new Callback(callback, cookie);
mCallbacks.put(binder, cb);

將客戶端傳遞過來的Callback對象轉為IBinder對象作為key,封裝一個Callback作為value。客戶端傳遞過來的Callback對象雖然在服務端被重新序列化生成一個對象,但它們底層的Binder對象是同一個,所以可以實現Callback的跨進程傳輸。

在服務端註冊客戶端的回調後,服務端就可以通過這個回調主動向客戶端傳遞數據了。比如,在addStudent中,當添加數據成功時,將操作的執行結果或者其他數據分發給所有向該服務端註冊監聽的客戶端:

/**
 * 分發結果
 * @param result
 * @param msg
 */
private void dispatchResult(boolean result, String msg) {
    int length = sCallbackList.beginBroadcast();
    for (int i = 0; i < length; i++) {
        ITaskCallback callback = sCallbackList.getBroadcastItem(i);
        try {
            if (result) {
                callback.onSuccess(msg);
            } else {
                callback.onFailed(msg);
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
    //在調用beginBroadcast之後,必須調用該方法
    sCallbackList.finishBroadcast();
}

在客戶端中創建ITaskCallback對象:

//MainActivity.java

ITaskCallback mCallback = new ITaskCallback.Stub() {
    @Override
    public void onSuccess(String result) throws RemoteException {
        Log.i(TAG, "result = " + result);
    }

    @Override
    public void onFailed(String errorMsg) throws RemoteException {
        Log.e(TAG, "errorMsg = " + errorMsg);
    }
};

修改ServiceConnection,在建立連接、調用onServiceConnected方法時,進行Callback的註冊:

private ServiceConnection mConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        mStudentService = IStudentService.Stub.asInterface(service);
        if (mStudentService == null) {
            Log.i(TAG, "mStudentService == null");
            return;
        }
        try {
            if (mCallback != null) {
                Log.i(TAG, "mCallback != null");
                mStudentService.register(mCallback);
            } else {
                Log.i(TAG, "mCallback == null");
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {

    }
};

此時,客戶端與服務端的連接已經建立,且客戶端向服務端註冊了回調,當客戶端向服務端添加數據,服務端執行addStudent方法時,服務端會通過回調將添加數據的執行結果返回給客戶端,從而實現了雙向通信。

四、權限驗證

默認情況下,如果沒有加入權限驗證功能,那麽我們的遠程服務是所有進程都可以進行連接的,從系統安全性的角度出發,我們還需要有相應的權限驗證機制來保證系統的安全,有兩種方式:

1、在建立連接之前

在客戶端通過bindService方法綁定遠程服務時,我們會在服務端的onBind方法中將Binder對象返回給客戶端,那麽我們可以在onBind方法中對來自客戶端的請求進行權限驗證。

2、在客戶端請求執行服務端的AIDL方法時

實際上,每個AIDL方法都有一個唯一的方法標識code,服務端在Binder.onTransact中根據這個code判斷並確定客戶端想要調用的是哪個AIDL方法,所以,我們可以在Binder.onTransact中進行權限驗證,攔截非法的客戶端調用。
技術分享圖片

常用的權限驗證機制有包名驗證和權限驗證,即根據客戶端的包名或所聲明的權限是否符合服務端要求來進行驗證。

修改StudentService.java中的Binder對象:

private Binder mBinder = new IStudentService.Stub() {
    
    ...
    
    @Override
    public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
        //包名驗證
        String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
        String pkgName = null;
        if (packages != null && packages.length > 0) {
            pkgName = packages[0];
        }
        if (TextUtils.isEmpty(pkgName) || !pkgName.startsWith("com.sqchen")) {
            Log.i(TAG, "invalid pkgName : " + pkgName);
            return false;
        }
        return super.onTransact(code, data, reply, flags);
    }
};

這樣,如果客戶端的包名不是以"com.sqchen"開頭的話,則認為是非法請求,在onTranscat中返回false將使得客戶端的請求失敗,從而達到權限驗證的目的。

五、死亡回調

當客戶端與服務端之間的連接斷開,我們稱之為Binder死亡,此時雖然客戶端和服務端都在運行,但因為連接斷開,客戶端發出的請求是不會得到響應的,所以我們需要知道什麽時候連接斷開,以便進行重新綁定,或者執行其他操作。

前面在看ServiceConnection的源碼時我們發現,當連接斷開時,會調用onServiceDisconnected方法,所以,我們可以在這個方法進行重新綁定服務。

此外,Binder中還有兩個很重要的方法,linkToDeathunlinkToDeath,通過linkToDeath我們可以給Binder設置一個死亡代理IBinder.DeathRecipient,當Binder死亡時,將會調用DeathRecipient的binderDied方法。

修改MainActivity.java,創建一個死亡代理,當客戶端與服務端建立連接時,為Binder設置死亡代理:

private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
    @Override
    public void binderDied() {
        if (mStudentService == null) {
            return;
        }
        //解除死亡代理
        mStudentService.asBinder().unlinkToDeath(mDeathRecipient, 0);
        mStudentService = null;
        //重新綁定服務
        bindStudentService();
        Log.i(TAG, "binderDied, bindService again");
    }
};

private ServiceConnection mConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        mStudentService = IStudentService.Stub.asInterface(service);
        if (mStudentService == null) {
            Log.i(TAG, "mStudentService == null");
            return;
        }
        try {
            //設置死亡代理
            mStudentService.asBinder().linkToDeath(mDeathRecipient, 0);
            if (mCallback != null) {
                Log.i(TAG, "mCallback != null");
                mStudentService.register(mCallback);
            } else {
                Log.i(TAG, "mCallback == null");
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        //也可以在這裏重新綁定服務
    }
};

接著,我們模擬服務端意外死亡導致連接斷開的情況,進入adb shell,查找服務端進程remote的pid,並kill掉:

E:\Blog\src\AIDLTest>adb shell
mido:/ # ps
USER           PID  PPID     VSZ    RSS WCHAN            ADDR S NAME
root         26112 28641    8968   1900 sigsuspend 74a7005e08 S sh
root         26116 26112   10540   1992 0          7115eac768 R ps
mido:/ # ps -A | grep com.sqchen
u0_a140      26015   745 5238588  68324 SyS_epoll_wait 7269051c40 S com.sqchen.aidltest
u0_a140      26046   745 5217176  39364 SyS_epoll_wait 7269051c40 S com.sqchen.aidltest:remote
mido:/ # kill 26046

然後,查看日誌:

2019-04-05 21:43:00.530 26015-26015/com.sqchen.aidltest I/MainActivity: mCallback != null
2019-04-05 21:45:21.955 26015-26028/com.sqchen.aidltest I/MainActivity: binderDied, bindService again
2019-04-05 21:45:22.048 26015-26015/com.sqchen.aidltest I/MainActivity: mCallback != null

發現remote被kill之後,確實調用了DeathRecipientbinderDied方法,再次查看remote進程,觀察發現remote進程的pid在被kill掉前後是不一樣的,說明成功地重新綁定服務。

E:\Blog\src\AIDLTest>adb shell
mido:/ # ps -A | grep com.sqchen
u0_a140      26015   745 5239648  68328 SyS_epoll_wait 7269051c40 S com.sqchen.aidltest
u0_a140      26125   745 5217176  39604 SyS_epoll_wait 7269051c40 S com.sqchen.aidltest:remote

binderDiedonServiceDisconnected的區別:

1、binderDied早於onServiceDisconnected被調用(參考:linkToDeath機制了解和使用)

2、binderDied在客戶端的Binder線程池被調用,不能在這個方法中訪問UI,而onServiceDisconnected在客戶端的UI線程被調用;

六、需要註意的地方

1、客戶端調用遠程服務的方法,被調用的方法運行在服務端的Binder線程池中,同時,客戶端線程會被掛起,進入阻塞狀態,如果被調用的服務端方法比較耗時,那麽我們不能在客戶端的主線程去調用服務端的方法,否則將導致客戶端ANR。

2、查看ServiceConnection源碼時,發現客戶端的onServiceConnnectedonServiceDisconnected方法運行在主線程,即UI線程,所以也不能在這兩個方法中調用服務端的耗時方法。

3、服務端的方法運行在服務端的Binder線程池中,所以在編寫服務端代碼時,不需要新建線程去執行服務端方法。

源碼地址:AIDLTest

Android進程間通信(一):AIDL使用詳解