Binder AIDL的使用
參考Demo:https://github.com/gqq519/BinderAIDL
- Binder是Android的一個類,實現了IBinder接口
- IPC角度來說,Binder是Android的一種跨進程通信方式,可以理解爲一種虛擬的物理設備,設備驅動是/dev/binder。
- 從Framework角度說,Binder是ServiceManager連接各種Manager(ActivityManager、WindowManager等)和相應的ManagerService的橋樑。
- 從應用層來說,Binder是客戶端和服務端進行通信的媒介,當bindService的時候,服務端會返回服務端業務調用的Binder對象,客戶端由此獲取服務端提供的服務或數據。服務包括普通服務和基於AIDL的服務。
Android中,Binder主要用於Service中,包括AIDL和Messenger,普通Service不涉及進程間通信,不涉及Binder的核心,而Messenger底層其實也是AIDL實現的,所以拿AIDL來了解Binder的工作機制。
AIDL簡述
AIDL:Android Interface Definition Language,通過編寫aidl文件,系統會編譯生成Binder接口,用於進程間通信。
AIDL支持的數據格式:
- Java的基本數據類型
- String和CharSequence
- List和Map:
- 元素必須是 AIDL 支持的數據類型
- 具體的類裏則必須是 ArrayList 或者 HashMap
- 其他AIDL生成的接口
- 實現Parcelable的類
創建AIDL示例
1. 創建工程
2. 創建要操作的實體類,需要實現Parcelable接口,跨進程使用
public class User implements Parcelable {
private int id;
private String name;
public User(int id, String name) {
this.id = id;
this.name = name;
}
// 反序列化方法
protected User(Parcel in) {
id = in.readInt();
name = in.readString();
}
// 序列化
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(id);
dest.writeString(name);
}
// 內容描述
@Override
public int describeContents() {
// 一般返回0,另一個特殊返回CONTENTS_FILE_DESCRIPTOR,爲有FileDescriptor,放入Parcelable需指定。。
// 然而。。好像並沒有什麼用,所以返回0就好了
return 0;
}
// 反序列化
public static final Creator<User> CREATOR = new Creator<User>() {
@Override
public User createFromParcel(Parcel in) {
// 反序列化對象
return new User(in);
}
@Override
public User[] newArray(int size) {
// 反序列化數組
return new User[size];
}
};
}
3. 創建實體類的映射aidl文件
右鍵新建AIDL文件User.aidl(名稱與實體類保持一致),會在main下生成aidl目錄,包名與java包名一致。
User.aidl文件爲實體類的映射文件,需要聲明映射的實體類和類型:
// User.aidl
// 包名與實體類包名一致
package com.gqq.binderaidl;
parcelable User;
4. 創建操作接口aidl文件
在aidl目錄的包名下創建AIDL文件IUserManager.aidl,內部是一個接口,主動實現了void basicTypes()
方法,在接口中定義需要跨進程操作的接口:
// IUserManager.aidl
package com.gqq.binderaidl;
// Declare any non-default types here with import statements
import com.gqq.binderaidl.User;
interface IUserManager {
/**
* 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);
void addUser(in User user);
List<User> getUserList();
}
比如我們定義了兩個方法用於操作:
- addUser()添加User
- getUserList()獲取用戶列表
注意:
- 定義的Parcelable實體類型,需要導入它的全路徑,比如User,需要導入
import com.gqq.binderaidl.User;
- 方法參數中,除了基本數據類型外都需要標上類型:in(輸入), out(輸出), inout(輸入輸出)
5. Make Project,生成Binder的java文件
上述操作完成後,點擊Build
-> Make Project
,完成後可以在build/generated/source/aidl/packageName/
下找到生成的java文件。
IUserManager的大致預覽:
生成的代碼主要給客戶端使用,後續再介紹裏面的內容吧~
至此,通信的媒介我們已經完成了。
注意:Make Project 出現錯誤可能的原因
- 映射的aidl:User.aidl和實體類User,名稱要保持一致,包名要保持一致。
- 定義的AIDL接口文件的方法參數需要標上類型。
- AIDL接口文件中導入實體類的包名。
編寫服務端代碼
1. 創建Service
在項目中創建Service,需要實現onBind()方法,返回值爲IBinder,根據上述生成的IUserManager.java,內部Stub繼承自Binder,所以,onBind() 的返回值設置爲AIDL的接口的實例。
public class UserService extends Service {
private List<User> users;
@Nullable
@Override
public IBinder onBind(Intent intent) {
users = new ArrayList<>();
// 返回AIDL生成的Binder實例
return new UserServiceImpl();
}
// 創建AIDL生成的Binder實例
public class UserServiceImpl extends IUserManager.Stub {
// 實現的AIDL接口的方法
@Override
public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {
}
@Override
public void addUser(User user) throws RemoteException {
user.name += "-Service";
users.add(user);
}
@Override
public List<User> getUserList() throws RemoteException {
return users;
}
}
}
2. 清單註冊Service
Service創建好之後在清單文件註冊:
<service android:name=".UserService"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.gqq.binderaidl.IUserManager" />
</intent-filter>
</service>
至此,服務端工作已完成。
編寫客戶端代碼
1. 新建Client工程
客戶端可以做爲一個單獨的App,或者跟服務端在不同的進程都可。
2. 拷生成文件到Client工程
在客戶端工程下,將服務端生成的AIDL的java文件以及實體類User.java 一起拷過來,更改client的目錄與Server保持一致(拷過來會報錯)。
3. 綁定服務
public class MainActivity extends AppCompatActivity {
private IUserManager userManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 通過Server對外的接口操作數據並顯示等
final TextView textView = findViewById(R.id.tv_show);
findViewById(R.id.btn_client).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (userManager == null) return;
try {
userManager.addUser(new User(1, "gqq"));
List<User> userList = userManager.getUserList();
textView.setText(userList.toString());
} catch (RemoteException e) {
e.printStackTrace();
}
}
});
Intent intent = new Intent();
intent.setAction("com.gqq.binderaidl.IUserManager");
// Android 5.0 以後必須顯式啓動,參考:https://blog.csdn.net/vrix/article/details/45289207
intent.setPackage("com.gqq.binderaidl");
bindService(intent, connection, Context.BIND_AUTO_CREATE);
}
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
// 綁定成功之後獲取服務對象
userManager = IUserManager.Stub.asInterface(service);
Toast.makeText(MainActivity.this, "onServiceConnected", Toast.LENGTH_SHORT).show();
}
@Override
public void onServiceDisconnected(ComponentName name) {
userManager = null;
}
};
運行
首先運行服務端,再運行客戶端,就可以通過客戶端操作了。
升級:設置監聽
假如現在用戶希望服務端當有新用戶時實時的告訴我,這個是一個典型的觀察者模式,在實際中也用到很多。
1. 提供AIDL接口,作爲監聽
提供一個AIDL接口,客戶端需要實現這個接口並註冊提醒的功能,也可以隨時取消這個提醒。使用AIDL接口是因爲AIDL中無法使用普通接口。
服務端創建一個aidl文件:
package com.gqq.binderaidl;
import com.gqq.binderaidl.User;
// Declare any non-default types here with import statements
interface IOnNewUserArrivedListener {
/**
* 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);
void onNewUserArrived(in User user);
}
2. 在原用的IUserManager.aidl接口中添加註冊和取消註冊的方法
添加註冊和反註冊的方法,以便於客戶端可以監聽。
package com.gqq.binderaidl;
// Declare any non-default types here with import statements
import com.gqq.binderaidl.User;
import com.gqq.binderaidl.IOnNewUserArrivedListener;
interface IUserManager {
/**
* 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);
void addUser(in User user);
List<User> getUserList();
void registerListener(IOnNewUserArrivedListener listener);
void unregisterListener(IOnNewUserArrivedListener listener);
}
寫完make project。
3. 完善服務端的Service
主要是實現Service中的IUserManager.Stub的實現,因爲新增了兩個方法。另外模擬場景:開啓線程,每隔5s新增一個用戶並通知客戶端。
// ------------定義的變量---------------
private List<IOnNewUserArrivedListener> listeners = new ArrayList<>();
private boolean isServiceDestoryed = false;
// ------------重寫方法,開啓工作線程---------
@Override
public void onCreate() {
super.onCreate();
new Thread(new ServiceWorker()).start();
}
@Override
public void onDestroy() {
super.onDestroy();
isServiceDestoryed = true;
}
// -------------工作的線程-----------------
public class ServiceWorker implements Runnable {
@Override
public void run() {
while (!isServiceDestoryed) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int userId = users.size() + 1;
User user = new User(userId, "new User:"+userId);
try {
onNewUserArrived(user);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
}
private void onNewUserArrived(User user) throws RemoteException {
users.add(user);
for (int i = 0; i < listeners.size(); i++) {
IOnNewUserArrivedListener onNewUserArrivedListener = listeners.get(i);
onNewUserArrivedListener.onNewUserArrived(user);
}
}
// --------------重寫的方法實現------------------
@Override
public void registerListener(IOnNewUserArrivedListener listener) throws RemoteException {
if (!listeners.contains(listener)) {
listeners.add(listener);
} else {
Log.i("TAG", "listener already exists");
}
}
@Override
public void unregisterListener(IOnNewUserArrivedListener listener) throws RemoteException {
if (listeners.contains(listener)) {
listeners.remove(listener);
} else {
Log.i("TAG", "not found");
}
}
服務端的修改已經完成。
4. 客戶端註冊監聽並處理接收
把服務端新加的aidl文件生成的java文件複製到客戶端項目中,客戶端註冊監聽,並在頁面退出時解除註冊。同時在onUserArrived方法中接收到數據後要回到主線程顯示等,所以藉助Handler實現。
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Toast.makeText(MainActivity.this, "onServiceConnected", Toast.LENGTH_SHORT).show();
userManager = IUserManager.Stub.asInterface(service);
try {
// 註冊監聽
userManager.registerListener(listener);
service.linkToDeath(deathRecipient, 0);
} catch (RemoteException e) {
Log.i("TAG", "RemoteException");
e.printStackTrace();
}
}
// 註冊的監聽
private IOnNewUserArrivedListener listener = new IOnNewUserArrivedListener.Stub() {
@Override
public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {
}
@Override
public void onNewUserArrived(User user) throws RemoteException {
handler.obtainMessage(MESSAGE_WHAT_ARRIVED, user).sendToTarget();
}
};
// Handler的處理
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_WHAT_ARRIVED:
Log.i("TAG", "received new user" + msg.obj);
List<User> userList = null;
try {
userList = userManager.getUserList();
textView.setText(userList.toString());
} catch (RemoteException e) {
e.printStackTrace();
}
break;
default:
super.handleMessage(msg);
}
}
};
// 取消監聽
@Override
protected void onDestroy() {
if (userManager != null && userManager.asBinder().isBinderAlive()) {
try {
userManager.unregisterListener(listener);
} catch (RemoteException e) {
e.printStackTrace();
}
}
unbindService(connection);
super.onDestroy();
}
升級:取消監聽
在上述的場景設置中,當退出客戶端頁面,取消註冊,通過日誌可以查看到,當取消註冊的時候會發現unregisterListener中remove的時候發生了異常,因爲在多線程中,Binder會把客戶端傳遞過來的對象重新轉化並生成一個新的對象。對象是不能跨進程傳遞的,我們跨進程傳遞的時候都是把對象進行序列化和反序列化。那如何實現取消註冊呢?需要藉助RemoteCallbackList
。
RemoteCallbackList是系統專門提供的用於刪除跨進程listener的接口,可以從源碼中看出:
public class RemoteCallbackList<E extends IInterface>
// Callback中封裝了真正的遠程listener
ArrayMap<IBinder, Callback> mCallbacks = new ArrayMap<IBinder, Callback>();
跨進程的對象雖然不一樣,但Binder是同一個,利用Binder來實現,客戶端解除註冊的時候遍歷所有的服務端的listener,將具有相同Binder 的listener刪除即可,這個是RemoteCallbackList所做的事情。同時,還有一個功能,當客戶端進行終止後,它會自動解除客戶端所註冊的listener。
代碼改善
利用RemoteCallbackList實現解除註冊:用RemoteCallbackList代替List<>
private RemoteCallbackList<IOnNewUserArrivedListener> listeners = new RemoteCallbackList<>();
修改註冊和解註冊的方法:
@Override
public void registerListener(IOnNewUserArrivedListener listener) throws RemoteException {
listeners.register(listener);
// 在此處打印下注冊的監聽的數量
int i = listeners.beginBroadcast();
listeners.finishBroadcast();
Log.i("TAG", "registerListener listener size:"+ i);
}
@Override
public void unregisterListener(IOnNewUserArrivedListener listener) throws RemoteException {
listeners.unregister(listener);
// 注意:在此處打印下注冊的監聽的數量
int i = listeners.beginBroadcast();
listeners.finishBroadcast();
Log.i("TAG", "unregisterListener listener size:"+ i);
}
修改onNewUserArrived方法:
private void onNewUserArrived(User user) throws RemoteException {
users.add(user);
int size = listeners.beginBroadcast();
for (int i = 0; i < size; i++) {
IOnNewUserArrivedListener onNewUserArrivedListener = listeners.getBroadcastItem(i);
if (onNewUserArrivedListener != null) {
onNewUserArrivedListener.onNewUserArrived(user);
}
}
listeners.finishBroadcast();
}
注意:RemoteCallbackList並不是一個List,無法像操作List一樣操作它,要像上述的方式一樣去遍歷它,其中beginBroadcast()
與finishBroadcast()
必須配對使用,哪怕是想要獲取RemoteCallbackList的元素個數。
升級:耗時處理
客戶端調用服務端的方法,被調用的方法運行在服務端的Binder線程池中,同時客戶端掛起。如果服務端的方法是耗時的操作,客戶端在UI線程的話,就會導致ANR。客戶端的onServiceConnected和onServiceDisconnected運行在UI線程中,不能直接在裏面調用服務端的耗時方法。服務端的方法本身運行在Binder線程池中,可以做大量的耗時操作,不用再開啓線程去進行異步操作。模擬一下耗時的操作:
// 服務端模擬耗時操作的方法
@Override
public List<User> getUserList() throws RemoteException {
SystemClock.sleep(5 * 1000);
return users;
}
客戶端點擊按鈕去直接調用服務端的方法獲取list數據,多次點擊就會出現ANR,那麼就需要把調用放到非UI線程,比如:
findViewById(R.id.btn_client).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (userManager == null) return;
new Thread(new Runnable() {
@Override
public void run() {
try {
List<User> userList = userManager.getUserList();
} catch (RemoteException e) {
e.printStackTrace();
}
}
}).start();
}
}
同樣,我們不可以在服務端UI線程中調用客戶端耗時的操作,另外AIDL接口方法都運行在Binder線程池中,訪問UI需要切換線程。例如客戶端的onNewUserArrived方法。
升級:服務重連
Binder在通信中可能意外死亡,往往由於服務端進程意外停止,需要重新連接服務。
1. Binder設置DeathRecipient監聽
設置DeathRecipient監聽,當Binder死亡時,會收到binderDied方法的回調,可以在binderDied方法中重連服務。
2. 在OnServiceDisconnected中重連遠程服務
區別:onServiceDisconnected在客戶端UI線程被回調,binderDied在客戶端的binder線程池中被回調,即binderDied中不能訪問UI。
升級:權限驗證
定義權限非本節重點:定義權限參考
首先在服務端的AndroidMenifest中聲明所需權限
<permission
android:name="com.gqq.binderaidl.permission.ACCESS_USER_SERVICE"
android:protectionLevel="normal" />
<uses-permission android:name="com.gqq.binderaidl.permission.ACCESS_USER_SERVICE" />
第一種方法:在onBind中進行驗證(permission驗證)
@Override
public IBinder onBind(Intent intent) {
int check = checkCallingOrSelfPermission("com.gqq.binderaidl.permission.ACCESS_USER_SERVICE");
if (check == PackageManager.PERMISSION_DENIED) {
Log.i(TAG, "onbind check=" + check);
return null;
}
return mBinder;
}
第二種方法:在onTransact中進行驗證(包名驗證)
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws RemoteException {
// 權限驗證
int check = checkCallingOrSelfPermission("com.ryg.chapter_2.permission.ACCESS_BOOK_SERVICE");
if(check == PackageManager.PERMISSION_DENIED){
L.d("Binder 權限驗證失敗");
return false;
}
// 包名驗證
String packageName=null;
String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
if(packages!=null && packages.length>0){
packageName = packages[0];
}
if(!packageName.startsWith("com.ryg")){
L.d("包名驗證失敗");
return false;
}
return super.onTransact(code, data, reply, flags);
};
客戶端AndroidMenifest中聲明:
<uses-permission android:name="com.gqq.binderaidl.permission.ACCESS_USER_SERVICE" />
總結
主要是AIDL的整體使用流程。
再貼一遍代碼地址,歡迎指正!