Android跨進程IPC通信AIDL
簡介
AIDL:Android Interface Definition Language,即Android介面定義語言,用於生成Android不同進程間進行進程通信(IPC)的代碼,一般情況下一個進程是無法訪問另一個進程的內存的。如果某些情況下仍然需要跨進程訪問內存數據,這時候Android系統就要將其對象分解成能夠識別的原數據,編寫這一組操作的代碼是一項繁瑣的工作,但是AIDL對底層進行了抽象的封裝,簡化了跨進程操作。
在Android中跨進程操作的方式不止一種,四大組件中ContentProvider天生就是為跨進程操作而存在的,但是ContentProvider所謂的跨進程操作數據,這些數據不一定是存放在內存中的,如通訊錄數據時存放在Sqlite資料庫中的。AIDL支持的跨進程操作的數據是要存放在內存中的,AIDL底層實際上也是使用的Binder進行的跨進程操作,後續另起一篇博文繼續介紹Binder的跨進程機制。
使用場景
只有不同應用之間需要進行IPC,並且想要在Service中處理多線程時,這種場景才有必要使用AIDL。如果僅僅需要跨進程但是不是跨應用,這時候應該通過Binder進行數據交互;另外如果僅僅是需要跨進程IPC,但是不需要處理多線程,這時候應該通過Messenger類進行數據交互。
定義AIDL介面
在Android Studio中使用AIDL的項目的目錄結構跟eclipse中有很大差異,下圖是使用AIDL的項目的目錄結構。
在Android Studio中只需要在某個Module中使用右鍵菜單中new就會顯示創建AIDL文件的菜單,當新建成功後AIDL文件位於工程的同java同一級的aidl目錄文件夾下面。在 .aidl 文件中存放的就是AIDL介面。
定義.aidl文件
.aidl文件名稱必須同介面名稱保持一致,必須使用Java語言的語法定義AIDL文件。AIDL使用簡單語法,通過可帶參數和返回值的一個或多個方法來聲明介面。參數和返回值可以是任意類型,甚至可以是其他 AIDL 生成的介面。每個.aidl文件都必須定義單個介面,並且只需包含介面聲明和方法簽名,也意味著在.aidl文件中介面名稱和方法名稱都不可以使用許可權修飾符。
默認情況下,AIDL 支持下列數據類型:
Java語言中所有的基本數據類型,字元類型char,布爾類型boolean以及數值類型byte、short、int、long、float、double;
String和CharSequence類型;
集合List類型,List中的所有元素都必須是以上列表中支持的數據類型、其他 AIDL 生成的介面或您聲明的可打包類型。可選擇將 List用作泛型類型(例如,List )。另一端實際接收的具體類始終是 ArrayList,但生成的方法使用的是List介面, 在AIDL中不可以使用ArrayList類型進行定義,只能使用List介面定義 ,否則會報unknown type編譯錯誤。
集合Map類型,中的所有元素都必須是以上列表中支持的數據類型、其他 AIDL 生成的介面或您聲明的可打包類型。不同於集合List介面, 在AIDL中不支持泛型Map(如 Map 形式的 Map)。 另一端實際接收的具體類始終是 HashMap,但生成的方法使用的是 Map 介面。類似List介面, 在.aidl文件中不能使用HashMap,只能使用Map介面 。
自定義類型必須實現Parcelable介面,並且在aidl文件夾下有對應類型的aidl文件;
非JDK中定義的類型,類似於Java語法,必須使用import進行引入。
定義AIDL介面時需要注意如下:
方法可帶零個或多個參數,返回值或空值,但是方法名稱不能相同;
所有非基本數據類型參數都需要指示數據走向的方向標記。可以是 in、out 或 inout。基本數據類型默認為 in,不能是其他方向;
.aidl 文件中包括的所有代碼注釋都包含在生成的 IBinder 介面中(import 和 package 語句之前的注釋除外);
只支持方法,不應在AIDL中定義靜態欄位。
如下是定義是IRemoteService.aidl:
interface IRemoteService {
void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString);
String getName();
void setListData(in List inList,out List outList);
void setMapData(in Map map);
void setUser(in User user);
}
下面是自定義的JavaBean類型User.aidl
parcelable User;
User類在使用的時候必須實現Parcelable介面,代碼這裡就不再貼出來了。
Service實現AIDL介面
在定義的AIDL介面編譯後實際上會生成一個跟.aidl同名的Java類文件,裡面包含了所有的AIDL文件中聲明的方法,並且包含了一個默認的實現類Stub,該類是抽象類,繼承了Binder類實現了AIDL介面。在Stub類中有兩個方法一個是asInterface()方法,該方法返回的是AIDL文件生成的介面,另外一個方法是asBinder(),該方法返回的是一個IBinder類型的實例。
asInterface()和asBinder()方法非常有用,asInterface()方法可以用於客戶端的IPC方法調用,另外一個方法可以用於在服務端返回Binder實例,並在服務端實現響應的介面方法。
上面介紹過在定義非基本數據類型的時候必須定義數據走向,聲明in或out或者inout,在AIDL生成的Java文件中就可以看出來究竟了,這裡可以參看setListData()方法的生成實現。
@Override
public void setListData(java.util.List inList, java.util.List outList) throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeStringList(inList);
mRemote.transact(Stub.TRANSACTION_setListData, _data, _reply, 0);
_reply.readException();
_reply.readStringList(outList);
} finally {
_reply.recycle();
_data.recycle();
}
}
如果聲明數據時是in,在生成相對應的方法的時候調用的實際上是Parcel的writeXXX方法,如果聲明的是out,在實現上面採用的是readXXX,所以在定義的時候一定要明確調用邏輯。
接下來看一下服務端中MyService類的實現。
ublic class MyService extends Service {
private static final String TAG = "AIDL_Server";
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
private IRemoteService.Stub mBinder = new IRemoteService.Stub() {
@Override
public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {
Log.d(TAG, "anInt:" + anInt + " aLong:" + aLong + " aBoolean:" + aBoolean + " aFloat:" + aFloat + " aDouble:" + aDouble + " aString:" + aString);
}
@Override
public String getName() throws RemoteException {
return "admin";
}
@Override
public void setListData(List inList, List outList) throws RemoteException {
Log.d(TAG, "inList:" + inList.toString());
setOutList(outList);
}
@Override
public void setMapData(Map map) throws RemoteException {
Log.d(TAG, "map:" + map.toString());
}
@Override
public void setUser(User user) throws RemoteException {
Log.d(TAG, "user:" + user.toString());
}
};
private void setOutList(List list) {
list.add("out_01");
list.add("out_02");
list.add("out_03");
}
}
在MyService類中,除了getName是一個有返回值的方法,其餘的方法都是void類型的,另外在數據走向方面,除了setListData方法的第二個參數outList是輸出類型的參數,其餘的參數都是輸入類型參數,所以這裡將其它參數直接列印出來了。
調用IPC方法
在客戶端想要調用Android的AIDL中定義的IPC方法,可以通過如下步驟實現:
首先需要定義一個相同包名相同目錄的AIDL文件夾;
聲明一個AIDL文件生成的介面實例;
實現ServiceConnection介面;
調用bindService綁定服務,傳入生成的ServiceConnection實例;
在onServiceConnected()實現中,將收到的IBinder實例(名為 service)。調用 XXX.Stub.asInterface((IBinder)service),以將返回的參數轉換為 AIDL生成的介面類型。
通過調用生成的AIDL介面實例中對應的方法就可以實現IPC調用了;
在不使用的時候解除服務的綁定Context.unbindService()。
如下是客戶端Activity中代碼的實現:
public class MainActivity extends AppCompatActivity {
private static final String TAG = "AIDL_Client";
private MyConnection conn;
private IRemoteService service;
private List outList=new ArrayList();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void startBind(View v) {
Intent intent = new Intent();
conn=new MyConnection();
bindService(intent, conn, Context.BIND_AUTO_CREATE);
}
public void startExecute(View v) {
try {
service.basicTypes(1, 10000L, true, 1.5f, 300.3, "Hello World");
Log.d(TAG, "getName:" + service.getName());
List inList = new ArrayList();
inList.add("inList01");
inList.add("inList02");
service.setListData(inList, outList);
Log.d(TAG, "outList:" + outList.toString());
Map map = new HashMap();
map.put("key01", "value01");
map.put("key02", "value02");
service.setMapData(map);
User user = new User();
user.setId(1001);
user.setName("admin");
service.setUser(user);
} catch (RemoteException e) {
e.printStackTrace();
}
}
private class MyConnection implements ServiceConnection {
public void onServiceConnected(ComponentName name, IBinder binder) {
service = IRemoteService.Stub.asInterface(binder);
}
public void onServiceDisconnected(ComponentName name) {
}
}
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(conn);
}
}
其它
上述示例只是為了介紹AIDL如何跨進程通信的,所以在客戶端接收到的數據直接就在主線程中處理了。但是實際上客戶端調用服務端的遠程方法,被調用的方法運行在服務端的Binder線程池中的,同時客戶端線程會被掛起,這時候如果服務端方法執行比較耗時,就會導致客戶端長時間阻塞在這裡,如果客戶端方法位於UI線程中,可能會引起ANR。在實際開發的時候注意,客戶端進行IPC通信的時候盡量放在子線程中。由於服務端的方法本身就是運行在服務端的Binder線程池中,所以即使服務端需要執行大量耗時的工作也不需要開啟新的線程去執行。
另外一定要注意的就是安全性,默認情況下遠程服務任何人都可以連接,這應該不是我們所需要的,所以還需要考慮一下許可權驗證。一般情況下有兩種處理方法,第一種是通過自定義許可權的方法,我們在服務端Service方法的onBinder()方法中添加許可權驗證,如果許可權驗證不通過直接返回null。另外一種就是在服務端的onTransact()方法中做驗證,也是做許可權驗證,如果不通過直接返回false。除了上面講的許可權驗證之外,可以通過getCallingPid()和getCallingUid()拿到客戶端應用的Pid和Uid進行校驗。
有關AIDL的介紹就先到這裡了,後續繼續介紹一下Binder有關內容。


TAG:殘殤 |