一、概述 AIDL是Android Interface Definition Language的縮寫,即Android介面定義語言。它是Android的進程間通信比較常用的一種方式。 Android中,每一個進程都有自己的Dalvik VM實例,擁有自己的獨立的記憶體空間,進程與進程之間不共用記憶體,這 ...
一、概述
AIDL是Android Interface Definition Language的縮寫,即Android介面定義語言。它是Android的進程間通信比較常用的一種方式。
Android中,每一個進程都有自己的Dalvik VM實例,擁有自己的獨立的記憶體空間,進程與進程之間不共用記憶體,這就產生了進程間通信的需求。
二、語法
AIDL是Android介面定義語言,是一門語言,所以它擁有自己的語法和特性。
(一)數據類型
AIDL支持的數據類型包括以下幾種:
- Java的8種基本數據類型:int,short,long,char,double,byte,float,boolean;
- CharSequence類型,如String、SpannableString等;
- ArrayList
,並且T必須是AIDL所支持的數據類型; - HashMap<K,V>,並且K和V必須是AIDL所支持的數據類型;
- 所有Parceable介面的實現類,因為跨進程傳輸對象時,本質上是序列化與反序列化的過程;
- AIDL介面,所有的AIDL介面本身也可以作為可支持的數據類型;
有兩個需要註意的地方:
1、在Java中,如果一個對象和引用它的類在同一個package下,是不需要導包的,即不需要import,而在AIDL中,自定義的Parceable對象和AIDL介面定義的對象必須在所引用的AIDL文件中顯式import進來,不管這些對象和所引用它們的AIDL文件是否在同一個包下。
2、如果AIDL文件中使用到自定義的Parceable對象,則必須再創建一個與Parceable對象同名的AIDL文件,聲明該對象為Parceable類型,並且根據上一條語法規定,在AIDL文件中進行顯式import。
(二)文件類型
- 所有AIDL文件都是以.aidl作為尾碼的;
- 根據用途區分,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的addStudent
和getStudentList
方法了。
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
時,自定義對象是否同時實現writeToParcel
和readFromParcel
;
5、如果有修改過java文件的包名,檢查AIDL文件的包名是否正確(是否與applicationId
一致);
當發現問題並修改後,可以嘗試Build->Clean Project
或Build -> 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
,添加register
和unregister
方法用於客戶端註冊回調和解除回調:
// 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
中新聲明的兩個方法,register
和unregister
,並創建了一個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中還有兩個很重要的方法,linkToDeath
和unlinkToDeath
,通過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之後,確實調用了DeathRecipient
的binderDied
方法,再次查看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
binderDied
和onServiceDisconnected
的區別:
1、binderDied早於onServiceDisconnected被調用(參考:linkToDeath機制瞭解和使用)
2、binderDied在客戶端的Binder線程池被調用,不能在這個方法中訪問UI,而onServiceDisconnected在客戶端的UI線程被調用;
六、需要註意的地方
1、客戶端調用遠程服務的方法,被調用的方法運行在服務端的Binder線程池中,同時,客戶端線程會被掛起,進入阻塞狀態,如果被調用的服務端方法比較耗時,那麼我們不能在客戶端的主線程去調用服務端的方法,否則將導致客戶端ANR。
2、查看ServiceConnection源碼時,發現客戶端的onServiceConnnected
和onServiceDisconnected
方法運行在主線程,即UI線程,所以也不能在這兩個方法中調用服務端的耗時方法。
3、服務端的方法運行在服務端的Binder線程池中,所以在編寫服務端代碼時,不需要新建線程去執行服務端方法。
源碼地址:AIDLTest