本文作者:CodingBlock 文章鏈接:http://www.cnblogs.com/codingblock/p/8436529.html
在上一篇博文中介紹了一種輕量級的跨進程通訊方案-Messenger,Messenger實現起來非常簡單,其底層原理也是AIDL,更像是一個簡易版的AIDL,但簡單的東西往往也有其侷限性,Messenger的主要作用是傳遞消息,它無法實現RPC功能也就是無法讓我們在客戶端本地就能調用遠程的方法,而且Messenger是以串行的方式處理,無法同時處理多個請求,只能一個一個的處理。而AIDL就可以很好彌補Messenger的不足,雖然實現起來相對複雜一些,但它功能強大,無疑是跨進程通訊的首選方案。接下來我們先看看AIDL是什麼,都可以傳遞哪些數據,並且本文會用一個小例子來直觀的體會AIDL的實現過程。
讀完本文你將深入掌握以下幾個知識點:
- AIDL是什麼?
- AIDL傳遞的類型。
- 怎麼創建AIDL。
- AIDL文件中的定向tag:in、out、inout的區別。
- 如何在AIDL中添加權限校驗。
一、AIDL是什麼?
AIDL全稱Android Interface Definition Language,即Android接口定義語言。AIDL是Android中可以實現跨進程通訊的一種方案,通過AIDL可以實現RPC方式,所謂RPC是指遠程過程調用(Remote Procedure Call),可以簡單的理解爲就像在本地一樣方便的調動遠程的方法。在Android的跨進程通訊的方案中,只有AIDL可以實現RPC方式。
二、AIDL文件支持哪些數據類型:
- 基本數據類型:int、long、char、boolean、double等
- String
- CharSequence
- ArrayList:裏面每個元素也需要被AIDL支持
- HashMap:裏面的每個Key和Value也都需要被AIDL支持
- Parcelable:所有實現了此接口的對象
- AIDL:所有的AIDL接口本身也可以在AIDL文件中使用
三、創建AIDL
接下類用一個小例子來說明AIDL的創建過程及用法,儘管在同一個APP內依然可以指定兩個進程,但爲了更能凸顯“跨進程”這一點,還是決定將此示例藉助於兩個APP來實現,畢竟在開發中真實的需求也是發生在兩個APP中。
在實現AIDL的過程中服務端APP和客戶端APP中要包含結構完全相同的AIDL接口文件,包括AIDL接口所在的包名及包路徑要完全一樣,否則就會報錯,這是因爲客戶端需要反序列化服務端中所有和AIDL相關的類,如果類的完整路徑不一致就無法反序列化成功。
小技巧:爲了更加方便的創建AIDL文件,我們可以新建一個lib工程,讓客戶端APP和服務端APP同時依賴這個lib,這樣只需要在這個lib工程中添加AIDL文件就可以了!
簡要說明一下將要實現的小例子的需求:是一個通訊錄,在服務端維護一個List用來存放聯繫人信息,客戶端可以通過RPC方式來添加聯繫人、獲取聯繫列表等功能。
1、新建一個承載AIDL文件的lib(在本示例中姑且叫做libaidl)
- 創建一個Android Library類型的Module,爲了與普通的java代碼作區分,在main文件夾下爲AIDL文件新建一個專門的文件夾,新建工程的結構如下:
- 然後添加AIDL接口文件:
首先新建一個Contact類,通過上面的介紹我們知道,普通的java類是不能在AIDL中使用的,必須要實現Parcelable接口,並在AIDL文件中聲明:
Contact.java
/** * Created by liuwei on 18/2/8. */ public class Contact implements Parcelable { private int phoneNumber; private String name; private String address; public Contact(int phoneNumber, String name, String address) { this.phoneNumber = phoneNumber; this.name = name; this.address = address; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(phoneNumber); dest.writeString(name); dest.writeString(address); } private final static Creator<Contact> CREATOR = new Creator<Contact>() { @Override public Contact createFromParcel(Parcel source) { return new Contact(source); } @Override public Contact[] newArray(int size) { return new Contact[size]; } }; public Contact(Parcel parcel) { phoneNumber = parcel.readInt(); name = parcel.readString(); address = parcel.readString(); } @Override public String toString() { return "Contact{" + "phoneNumber=" + phoneNumber + ", name='" + name + '\'' + ", address='" + address + '\'' + '}'; } }
聲明Contact類
Contact.aidl
package cn.codingblock.libaidl.contacts; parcelable Contact;
創建AIDL接口文件,聲明需要暴露給客戶端的方法。
IContactsManager.aidl
package cn.codingblock.libaidl.contacts; import cn.codingblock.libaidl.contacts.Contact; interface IContactsManager { int getPhoneNumber(in String name); String getName(int phoneNumeber); Contact getContact(int phoneNumber); List<Contact> getContactList(); boolean addContact(in Contact contact); }
注:在AIDL接口文件中如果引用到了某個類,即使與這個類的AIDL聲明在同一個包中也使用import導入此類。
aidl文件最終的結構如下:
- 在本次的示例中我們的客戶端APP是ipcclient工程,服務端APP是ipc工程,記得在兩個工程中添加libaidl的依賴(添加依賴的方法比較簡單,就不多說了),服務端工程、客戶端工程、lib工程的結構如下:
小問題:AIDl文件中in、out、inout的區別?
- in、out、inout稱爲AIDL接口方法參數的定向tag,代表着數據的流向。
- in:服務端收到對象後對此對象做任何修改都不會同步給客戶端。
- out:無論客戶端傳過去的對象有沒有提前設置值,在Binder傳輸過程中都會new一個空對象傳遞給服務端,服務端接收到的對象後對此對象所做的修改都會同步給客戶端。
- inout:服務端接受對象後,無論是客戶端還是服務端對此對象所做的修改都會兩端同步。
- 基本類型的參數只能是in。
對此問題感興趣的同學可以查看AIDL所生成的Stub源碼。
2、服務端實現(在ipc工程中)
- 創建一個Service,用於響應客戶端的綁定請求,我們將這個Service名爲爲ContactManagerService。
- 接着創建一個類,讓這個類繼承AIDL接口中的Stub類,並實現其抽象方法。在Service中返回這個新建這個類的對象。
詳細實現如下:ContactManagerService.java
/** * Created by liuwei on 18/2/8. */ public class ContactManagerService extends Service { private final static String TAG = ContactManagerService.class.getSimpleName(); private CopyOnWriteArrayList<Contact> contacts = new CopyOnWriteArrayList<>(); @Override public void onCreate() { super.onCreate(); contacts.add(new Contact(110, "報警電話", "派出所")); contacts.add(new Contact(119, "火警電話", "消防局")); contacts.add(new Contact(112, "故障電話", "保障局")); } @Nullable @Override public IBinder onBind(Intent intent) { return new ContactManagerBinder(); } private class ContactManagerBinder extends IContactsManager.Stub{ /** * 根據號碼返回手機號 * @param name * @return * @throws RemoteException */ @Override public int getPhoneNumber(String name) throws RemoteException { if (!TextUtils.isEmpty(name)) { for (Contact contact:contacts) { if (contact.name.equals(name)){ return contact.phoneNumber; } } } return 0; } /** * 根據號碼返回名稱 * @param phoneNumber * @return * @throws RemoteException */ @Override public String getName(int phoneNumber) throws RemoteException { for (Contact contact:contacts) { if (contact.phoneNumber == phoneNumber){ return contact.name; } } return null; } /** * 根據號碼返回聯繫人對象 * @param phoneNumber * @return * @throws RemoteException */ @Override public Contact getContact(int phoneNumber) throws RemoteException { for (Contact contact:contacts) { if (contact.phoneNumber == phoneNumber) { return contact; } } return null; } /** * 獲取聯繫人集合 * @return * @throws RemoteException */ @Override public List<Contact> getContactList() throws RemoteException { return contacts; } /** * 添加聯繫人 * @param contact * @return * @throws RemoteException */ @Override public boolean addContact(Contact contact) throws RemoteException { if (contact != null) { return contacts.add(contact); } return false; } } }
- 最後在清單文件中將此Service添加配置,並將export屬性設爲true以供外界調用:
<service android:name=".aidl.contact.ContactManagerService" android:exported="true"/>
上面代碼很簡單,值得一提的是AIDL的方法都是在服務端的Binder線程池中執行的,如果有多個客戶端同時請求,就會有多個線程來操作這些方法,本次示例將存放聯繫人的集合採用了CopyOnWriteArrayList實現,由於CopyOnWriteArrayList本身是線程安全的,所以在此我們不需要做額外的同步處理。
==從上文我們知道,在List中AIDL只支持ArrayList的傳輸,那麼在此處爲什麼可以使用CopyOnWriteArrayList呢?==
這是因爲AIDL支持的是List,之所以說AIDL只支持傳遞ArrayList ,是因爲它在傳遞其他List類型時就會自動將其他類型在傳遞之前轉換成ArrayList然後再返回給服務端,也就是說無論你在服務端使用其他的任何list的子類型,在客戶端接收到的類型都是ArrayList。
所以本次示例中雖然服務端返回的事CopyOnWriteArrayList,但是在Binder中會按照List的規範去讀取它並最終形成一個新的ArrayList返回給客戶端,類似的還有ConcurrentHashMap對應於HashMap。(其實不光CopyOnWriteArrayList,還有LinkedList等其他的List子類型也都是可以的。)
3、客戶端實現(在ipcclient工程中)
- 在客戶中綁定服務端的Service,綁定成功後就可以在ServiceConnection中的onServiceConnected方法中將返回的Binder對象轉換成AIDL接口所屬的類型。
首先向Intent指定Component,需要傳入兩個參數,一個是遠程Service所在工程包名,另一個是遠程Service的全量限定名,然後使用bindService綁定遠程Service:
Intent intent = new Intent(); intent.setComponent(new ComponentName("cn.codingblock.ipc", "cn.codingblock.ipc.aidl.contact.ContactManagerService")); bindService(intent, serviceConnection, BIND_AUTO_CREATE);
在serviceConnection中獲取返回的Binder並使用IContactsManager.Stub.asInterface()方法將Binder對象轉換成IContactsManager類型。
private ServiceConnection serviceConnection = new ServiceConnection(){ @Override public void onServiceConnected(ComponentName name, IBinder service) { mIContactsManager = IContactsManager.Stub.asInterface(service); Log.i(TAG, "onServiceConnected: mIContactsManager=" + mIContactsManager); } @Override public void onServiceDisconnected(ComponentName name) { mIContactsManager = null; Log.i(TAG, "onServiceDisconnected: "); } };
- 拿到Binder對象後就可以調用在AIDL文件中聲明的方法了,來看一下完整的代碼:
/** * Created by liuwei on 18/2/8. */ public class ContactMangerActivity extends AppCompatActivity { private static final String TAG = ContactMangerActivity.class.getSimpleName(); private IContactsManager mIContactsManager; private EditText et_contact_name; private EditText et_contact_phone_number; private EditText et_contact_address; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_contact_manger); ViewUtils.findAndOnClick(this, R.id.btn_add_contact, mOnClickListener); ViewUtils.findAndOnClick(this, R.id.btn_get_phone_number, mOnClickListener); ViewUtils.findAndOnClick(this, R.id.btn_get_name, mOnClickListener); ViewUtils.findAndOnClick(this, R.id.btn_get_contact, mOnClickListener); ViewUtils.findAndOnClick(this, R.id.btn_get_list, mOnClickListener); et_contact_name = ViewUtils.find(this, R.id.et_contact_name); et_contact_phone_number = ViewUtils.find(this, R.id.et_contact_phone_number); et_contact_address = ViewUtils.find(this, R.id.et_contact_address); Intent intent = new Intent(); intent.setComponent(new ComponentName("cn.codingblock.ipc", "cn.codingblock.ipc.aidl.contact.ContactManagerService")); bindService(intent, serviceConnection, BIND_AUTO_CREATE); } private ServiceConnection serviceConnection = new ServiceConnection(){ @Override public void onServiceConnected(ComponentName name, IBinder service) { mIContactsManager = IContactsManager.Stub.asInterface(service); Log.i(TAG, "onServiceConnected: mIContactsManager=" + mIContactsManager); } @Override public void onServiceDisconnected(ComponentName name) { mIContactsManager = null; Log.i(TAG, "onServiceDisconnected: "); } }; private View.OnClickListener mOnClickListener = new View.OnClickListener() { @Override public void onClick(View v) { switch (v.getId()) { case R.id.btn_add_contact: Contact contact = new Contact(getEtContactPhoneNumber(), getEtContactName(), getEtContactAddress()); try { mIContactsManager.addContact(contact); } catch (RemoteException e) { e.printStackTrace(); } break; case R.id.btn_get_phone_number: String name = getEtContactName(); try { Log.i(TAG, "onClick: " + name + "的電話:" + mIContactsManager.getPhoneNumber(name)); } catch (RemoteException e) { e.printStackTrace(); } break; case R.id.btn_get_name: int number = getEtContactPhoneNumber(); try { Log.i(TAG, "onClick: " + number + " 對應的名稱:" + mIContactsManager.getName(number)); } catch (RemoteException e) { e.printStackTrace(); } break; case R.id.btn_get_contact: int number1 = getEtContactPhoneNumber(); try { Contact contact1 = mIContactsManager.getContact(number1); System.out.println(contact1); } catch (RemoteException e) { e.printStackTrace(); } break; case R.id.btn_get_list: try { List<Contact> contacts = mIContactsManager.getContactList(); System.out.println(contacts); } catch (RemoteException e) { e.printStackTrace(); } break; } } }; private String getEtContactName() { String str = et_contact_name.getText().toString(); if (TextUtils.isEmpty(str)) { Toast.makeText(this, "請輸入聯繫人名稱", Toast.LENGTH_SHORT).show(); return null; } return str; } private int getEtContactPhoneNumber() { String str = et_contact_phone_number.getText().toString(); if (TextUtils.isEmpty(str)) { Toast.makeText(this, "請輸入聯繫人電話", Toast.LENGTH_SHORT).show(); return 0; } return Integer.valueOf(str); } private String getEtContactAddress() { String str = et_contact_address.getText().toString(); if (TextUtils.isEmpty(str)) { Toast.makeText(this, "請輸入聯繫人地址", Toast.LENGTH_SHORT).show(); return null; } return str; } @Override protected void onDestroy() { super.onDestroy(); unbindService(serviceConnection); } }
佈局文件也就幾個EditText和Button比較簡單,這裏就不貼出來了,接下來運行測試一下。
四、運行測試
兩端都運行後,客戶端界面如下圖:
查看ipcclient工程的log如下,發現已經成功綁定了遠程的Service:
.../cn.codingblock.ipcclient I/ContactMangerActivity: onServiceConnected: mIContactsManager=cn.codingblock.libaidl.contacts.IContactsManager$Stub$Proxy@6b60cb6
此時查看ipc工程的log如下:
.../cn.codingblock.ipc I/ContactManagerService: onCreate: ContactManagerService started... .../cn.codingblock.ipc I/System.out: 現有的聯繫人:[Contact{phoneNumber=110, name='報警電話', address='派出所'}, Contact{phoneNumber=119, name='火警電話', address='消防局'}, Contact{phoneNumber=112, name='故障電話', address='保障局'}]
通過上面兩個log說明客戶端和服務端已經鏈接成功了,接下類測試一下各按鈕遠程方法,在號碼輸入框中輸入110,依次點擊獲取聯繫人名稱按鈕和獲取聯繫人信息按鈕,log如下:
.../cn.codingblock.ipcclient I/ContactMangerActivity: onClick: 110 對應的名稱:報警電話 .../cn.codingblock.ipcclient I/System.out: Contact{phoneNumber=110, name='報警電話', address='派出所'}
接着在三個輸入框裏面分別輸入David,111,david`s home,然後點擊添加聯繫人信息將聯繫人添加到遠程列表裏面,在點擊獲取聯繫人列表,log如下:
.../cn.codingblock.ipcclient I/System.out: [Contact{phoneNumber=110, name='報警電話', address='派出所'}, Contact{phoneNumber=119, name='火警電話', address='消防局'}, Contact{phoneNumber=112, name='故障電話', address='保障局'}, Contact{phoneNumber=111, name='David', address='david`s home'}]
可以看到david的信息已經成功添加進來了。
五、如何爲AIDL添加權限驗證
其實在正式的開發工作中,我們不希望任何客戶端都能綁定我們的服務端,因爲這會存在極大安全隱患,所以當客戶端想我們發來綁定請求是我們需要做權限校驗,符合我們權限要求的客戶端纔可以與我們的服務端建立鏈接。
添加權限校驗可能會有很多方法,沒有對錯之分,在實際開發中適合就好,接下來我們介紹一種相對來說比較方便的權限驗證的方案:
- 還是用上面的示例來說明,首先在服務端工程也就是ipc工程的清單文件中聲明所需的權限,如下:
<!--聲明權限--> <uses-permission android:name="cn.codingblock.permission.ACCESS_CONTACT_MANAGER"/> <!--定義權限--> <permission android:name="cn.codingblock.permission.ACCESS_CONTACT_MANAGER" android:protectionLevel="normal"/>
- 然後在ContactManagerService的onBinde方法中進行權限驗證,驗證不通過就直接返回null。
@Nullable @Override public IBinder onBind(Intent intent) { if (checkCallingOrSelfPermission("cn.codingblock.permission.ACCESS_CONTACT_MANAGER") == PackageManager.PERMISSION_DENIED) { Log.i(TAG, "onBind: 權限校驗失敗,拒絕綁定..."); return null; } Log.i(TAG, "onBind: 權限校驗成功!"); return new ContactManagerBinder(); }
客戶端先不做修改,運行測試一下,此時在客戶端已經無法獲取服務端的Binder對象,在客戶端點擊按鈕操作時可以看到報空指針異常了:
/cn.codingblock.ipcclient E/AndroidRuntime: FATAL EXCEPTION: main Process: cn.codingblock.ipcclient, PID: 4726 java.lang.NullPointerException: Attempt to invoke interface method 'java.util.List cn.codingblock.libaidl.contacts.IContactsManager.getContactList()' on a null object reference at cn.codingblock.ipcclient.aidl.ContactMangerActivity$2.onClick(ContactMangerActivity.java:127) at android.view.View.performClick(View.java:6256) at android.view.View$PerformClick.run(View.java:24701) at android.os.Handler.handleCallback(Handler.java:789) at android.os.Handler.dispatchMessage(Handler.java:98) at android.os.Looper.loop(Looper.java:164) at android.app.ActivityThread.main(ActivityThread.java:6541) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)
- 接下來我們在客戶端上加上鍊接服務端所需的權限:
<!--聲明權限--> <uses-permission android:name="cn.codingblock.permission.ACCESS_CONTACT_MANAGER"/> <!--定義權限--> <permission android:name="cn.codingblock.permission.ACCESS_CONTACT_MANAGER" android:protectionLevel="normal"/>
注意:要在客戶端和服務端兩個工程中都加入以上聲明權限和定義權限的代碼。
經反覆測試發現:服務端工程中聲明權限和定義權限的代碼缺一不可,而客戶端工程中如果只加入聲明權限的代碼,那麼如果在安裝時,客戶端APP先於服務端APP安裝,客戶端就會由於找不到定義權限而無法成功獲取權限!
所以爲了保險起見,將兩端都同時加入定義權限的代碼和聲明權限的代碼,當然本示例中最好的方法是直接統一加入libaidl工程中,一次加入,兩端可用!
六、小結
雖然AIDL在創建的時候步驟比較繁瑣,但其功能十分強大。最後概括一下AIDL的創建步驟:
在服務端:
- 創建一個AIDL接口文件(如果用到了其他的類,要將類序列化,並在AIDL文件中聲明)
- 再創建Service用於響應客戶端的綁定請求。
- 接着創建一個類,讓這個類繼承AIDL接口中的Stub類,並實現其抽象方法。在Service的onBind方法中返回這個新建這個類的對象。
接着在客戶端:
- 在客戶中綁定服務端的Service,綁定成功後就可以在ServiceConnection中的onServiceConnected方法中將返回的Binder對象轉換成AIDL接口所屬的類型。
- 拿到Binder對象後就可以調用在AIDL文件中聲明的方法了。
最後想說的是,本系列文章爲博主對Android知識進行再次梳理,查缺補漏的學習過程,一方面是對自己遺忘的東西加以複習重新掌握,另一方面相信在重新學習的過程中定會有巨大的新收穫,如果你也有跟我同樣的想法,不妨關注我一起學習,互相探討,共同進步!
參考文獻:
- 《Android開發藝術探索》
- 《socket_百度百科》
源碼地址:本系列文章所對應的全部源碼已同步至github,感興趣的同學可以下載查看,結合代碼看文章會更好。源碼傳送門
本文作者:CodingBlock 文章鏈接:http://www.cnblogs.com/codingblock/p/8436529.html