AIDL是一個縮寫,全稱是Android Interface Definition Language,也就是Android接口定義語言。是的,首先咱們知道的第一點就是:AIDL是一種語言。既然是一種語言,那麼相應的就很天然的衍生出了一些問題:java
接下來,咱們就一步步的來解答上面的這些問題。android
ps:1,在研究AIDL相關的東西以前,一些必要的知識儲備是要有的。一方面是關於Android中service相關的知識,要了解的比較通透才行,關於這方面的東西能夠參考 Android中的Service:默默的奉獻者 (1),Android中的Service:Binder,Messenger,AIDL(2) 這兩篇博文。另外一方面是關於Android中序列化的相關知識,這方面的東西文中會簡單說起,可是若是想要深刻的研究一下的話最好仍是去找一些這方面的資料看一下。 2,個人編譯環境爲Android Studio2.1.2,SDK Version 23,JDK 1.7。程序員
設計這門語言的目的是爲了實現進程間通訊。app
每個進程都有本身的Dalvik VM實例,都有本身的一塊獨立的內存,都在本身的內存上存儲本身的數據,執行着本身的操做,都在本身的那片狹小的空間裏過完本身的一輩子。每一個進程之間都你不知我,我不知你,就像是隔江相望的兩座小島同樣,都在同一個世界裏,但又各自有着本身的世界。而AIDL,就是兩座小島之間溝通的橋樑。相對於它們而言,咱們就好像造物主同樣,咱們能夠經過AIDL來制定一些規則,規定它們能進行哪些交流——好比,它們能夠在咱們制定的規則下傳輸一些特定規格的數據。ide
總之,經過這門語言,咱們能夠愉快的在一個進程訪問另外一個進程的數據,甚至調用它的一些方法,固然,只能是特定的方法。測試
其實AIDL這門語言很是的簡單,基本上它的語法和 Java 是同樣的,只是在一些細微處有些許差異——畢竟它只是被創造出來簡化Android程序員工做的,太複雜很差——因此在這裏我就着重的說一下它和 Java 不同的地方。主要有下面這些點:gradle
數據類型:AIDL默認支持一些數據類型,在使用這些數據類型的時候是不須要導包的,可是除了這些類型以外的數據類型,在使用以前必須導包,就算目標文件與當前正在編寫的 .aidl 文件在同一個包下——在 Java 中,這種狀況是不須要導包的。好比,如今咱們編寫了兩個文件,一個叫作 Book.java ,另外一個叫作 BookManager.aidl,它們都在 com.lypeer.aidldemo 包下 ,如今咱們須要在 .aidl 文件裏使用 Book 對象,那麼咱們就必須在 .aidl 文件裏面寫上 import com.lypeer.aidldemo.Book;
哪怕 .java 文件和 .aidl 文件就在一個包下。
默認支持的數據類型包括:ui
下面是兩個例子,對於常見的AIDL文件都有所涉及:this
// Book.aidl //第一類AIDL文件的例子 //這個文件的做用是引入了一個序列化對象 Book 供其餘的AIDL文件使用 //注意:Book.aidl與Book.java的包名應當是同樣的 package com.lypeer.ipcclient; //注意parcelable是小寫 parcelable Book;
// BookManager.aidl //第二類AIDL文件的例子 package com.lypeer.ipcclient; //導入所須要使用的非默認支持數據類型的包 import com.lypeer.ipcclient.Book; interface BookManager { //全部的返回值前都不須要加任何東西,無論是什麼數據類型 List<Book> getBooks(); Book getBook(); int getBookCount(); //傳參時除了Java基本類型以及String,CharSequence以外的類型 //都須要在前面加上定向tag,具體加什麼量需而定 void setBookPrice(in Book book , int price) void setBookName(in Book book , String name) void addBookIn(in Book book); void addBookOut(out Book book); void addBookInout(inout Book book); }
在進行跨進程通訊的時候,在AIDL中定義的方法裏包含非默認支持的數據類型與否,咱們要進行的操做是不同的。若是不包含,那麼咱們只須要編寫一個AIDL文件,若是包含,那麼咱們一般須要寫 n+1 個AIDL文件( n 爲非默認支持的數據類型的種類數)——顯然,包含的狀況要複雜一些。因此我接下來將只介紹AIDL文件中包含非默認支持的數據類型的狀況,至於另外一種簡單些的狀況相信你們是很容易從中舉一反三的。spa
因爲不一樣的進程有着不一樣的內存區域,而且它們只能訪問本身的那一塊內存區域,因此咱們不能像平時那樣,傳一個句柄過去就完事了——句柄指向的是一個內存區域,如今目標進程根本不能訪問源進程的內存,那把它傳過去又有什麼用呢?因此咱們必須將要傳輸的數據轉化爲可以在內存之間流通的形式。這個轉化的過程就叫作序列化與反序列化。簡單來講是這樣的:好比如今咱們要將一個對象的數據從客戶端傳到服務端去,咱們就能夠在客戶端對這個對象進行序列化的操做,將其中包含的數據轉化爲序列化流,而後將這個序列化流傳輸到服務端的內存中去,再在服務端對這個數據流進行反序列化的操做,從而還原其中包含的數據——經過這種方式,咱們就達到了在一個進程中訪問另外一個進程的數據的目的。
而一般,在咱們經過AIDL進行跨進程通訊的時候,選擇的序列化方式是實現 Parcelable 接口。關於實現 Parcelable 接口以後裏面具體有那些方法啦,每一個方法是幹嗎的啦,這些我就不展開來說了,那並非這篇文章的重點,我下面主要講一下如何快速的生成一個合格的可序列化的類(以Book.java爲例)。
注:若AIDL文件中涉及到的全部數據類型均爲默認支持的數據類型,則無此步驟。由於默認支持的那些數據類型都是可序列化的。
我當前用的編譯器是Android Studio 2.1.2,它是自帶了 Parcelable 接口的模板的,只須要咱們敲幾下鍵盤就能夠輕鬆的生成一個可序列化的 Parcelable 實現類。
首先,建立一個類,正常的書寫其成員變量,創建getter和setter並添加一個無參構造,好比:
public class Book{ public String getName() { return name; } public void setName(String name) { this.name = name; } public int getPrice() { return price; } public void setPrice(int price) { this.price = price; } private String name; private int price; public Book() {} }
而後 implements Parcelable ,接着 as 就會報錯,將鼠標移到那裏,按下 alt+enter(as默認的自動解決錯誤的快捷鍵,若是大家的as有修改過快捷鍵的話以修改後的爲準) 讓它自動解決錯誤,這個時候它會幫你完成一部分的工做:
在彈出來的框裏選擇全部的成員變量,而後肯定。你會發現類裏多了一些代碼,可是如今仍是會報錯,Book下面仍然有一條小橫線,再次將鼠標移到那裏,按下 alt+enter 讓它自動解決錯誤:
此次解決完錯誤以後就不會報錯了,這個 Book 類也基本上實現了 Parcelable 接口,能夠執行序列化操做了。
可是請注意,這裏有一個坑:默認生成的模板類的對象只支持爲 in 的定向 tag 。爲何呢?由於默認生成的類裏面只有 writeToParcel() 方法,而若是要支持爲 out 或者 inout 的定向 tag 的話,還須要實現 readFromParcel() 方法——而這個方法其實並無在 Parcelable 接口裏面,因此須要咱們從頭寫。具體爲何你們能夠去看看:你真的理解AIDL中的in,out,inout麼?
那麼這個 readFromParcel() 方法應當怎麼寫呢?這樣寫:
@Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(name); dest.writeInt(price); } /** * 參數是一個Parcel,用它來存儲與傳輸數據 * @param dest */ public void readFromParcel(Parcel dest) { //注意,此處的讀值順序應當是和writeToParcel()方法中一致的 name = dest.readString(); price = dest.readInt(); }
像上面這樣添加了 readFromParcel() 方法以後,咱們的 Book 類的對象在AIDL文件裏就能夠用 out 或者 inout 來做爲它的定向 tag 了。
此時,完整的 Book 類的代碼是這樣的:
package com.lypeer.ipcclient; import android.os.Parcel; import android.os.Parcelable; /** * Book.java * * Created by lypeer on 2016/7/16. */ public class Book implements Parcelable{ public String getName() { return name; } public void setName(String name) { this.name = name; } public int getPrice() { return price; } public void setPrice(int price) { this.price = price; } private String name; private int price; public Book(){} public Book(Parcel in) { name = in.readString(); price = in.readInt(); } public static final Creator<Book> CREATOR = new Creator<Book>() { @Override public Book createFromParcel(Parcel in) { return new Book(in); } @Override public Book[] newArray(int size) { return new Book[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(name); dest.writeInt(price); } /** * 參數是一個Parcel,用它來存儲與傳輸數據 * @param dest */ public void readFromParcel(Parcel dest) { //注意,此處的讀值順序應當是和writeToParcel()方法中一致的 name = dest.readString(); price = dest.readInt(); } //方便打印數據 @Override public String toString() { return "name : " + name + " , price : " + price; } }
至此,關於AIDL中非默認支持數據類型的序列化操做就完成了。
我不是很清楚 Eclipse 或者較低版本的 as 上會不會像 as 2.1.2 這樣幫咱們在實現 Parcelable 接口的過程當中作如此多的操做,可是就算不會,咱們還有其餘的招數——經過插件來幫咱們實現 Parcelable 接口。
具體的實現方式和實現過程你們能夠參見這篇文章:告別手寫parcelable
首先咱們須要一個 Book.aidl 文件來將 Book 類引入使得其餘的 AIDL 文件其中可使用 Book 對象。那麼第一步,如何新建一個 AIDL 文件呢?Android Studio已經幫咱們把這個集成進去了:
鼠標移到app上面去,點擊右鍵,而後 new->AIDL->AIDL File,按下鼠標左鍵就會彈出一個框提示生成AIDL文件了。生成AIDL文件以後,項目的目錄會變成這樣的:
比起之前多了一個叫作 aidl 的包,並且他的層級是和 java 包相同的,而且 aidl 包裏默認有着和 java 包裏默認的包結構。那麼若是你用的是 Eclipse 或者較低版本的 as ,編譯器沒有這個選項怎麼辦呢?不要緊,咱們也能夠本身寫。打開項目文件夾,依次進入 app->src->main,在 main 包下新建一個和 java 文件夾平級的 aidl 文件夾,而後咱們手動在這個文件夾裏面新建和 java 文件夾裏面的默認結構同樣的文件夾結構,再在最裏層新建 .aidl 文件就能夠了:
注意看圖中的文件目錄。
Ok,如何新建AIDL文件說的差很少了,接下來就該寫AIDL文件的內容了。內容的話若是上一節有認真看的話基本上是沒什麼問題的。在這裏,咱們須要兩個AIDL文件,我是這樣寫的:
// Book.aidl //第一類AIDL文件 //這個文件的做用是引入了一個序列化對象 Book 供其餘的AIDL文件使用 //注意:Book.aidl與Book.java的包名應當是同樣的 package com.lypeer.ipcclient; //注意parcelable是小寫 parcelable Book;
// BookManager.aidl //第二類AIDL文件 //做用是定義方法接口 package com.lypeer.ipcclient; //導入所須要使用的非默認支持數據類型的包 import com.lypeer.ipcclient.Book; interface BookManager { //全部的返回值前都不須要加任何東西,無論是什麼數據類型 List<Book> getBooks(); //傳參時除了Java基本類型以及String,CharSequence以外的類型 //都須要在前面加上定向tag,具體加什麼量需而定 void addBook(in Book book); }
注意:這裏又有一個坑!你們可能注意到了,在 Book.aidl 文件中,我一直在強調:Book.aidl與Book.java的包名應當是同樣的。這彷佛理所固然的意味着這兩個文件應當是在同一個包裏面的——事實上,不少比較老的文章裏就是這樣說的,他們說最好都在 aidl 包裏同一個包下,方便移植——然而在 Android Studio 裏並非這樣。若是這樣作的話,系統根本就找不到 Book.java 文件,從而在其餘的AIDL文件裏面使用 Book 對象的時候會報 Symbol not found 的錯誤。爲何會這樣呢?由於 Gradle 。你們都知道,Android Studio 是默認使用 Gradle 來構建 Android 項目的,而 Gradle 在構建項目的時候會經過 sourceSets 來配置不一樣文件的訪問路徑,從而加快查找速度——問題就出在這裏。Gradle 默認是將 java 代碼的訪問路徑設置在 java 包下的,這樣一來,若是 java 文件是放在 aidl 包下的話那麼理所固然系統是找不到這個 java 文件的。那應該怎麼辦呢?
又要 java文件和 aidl 文件的包名是同樣的,又要能找到這個 java 文件——那麼仔細想一下的話,其實解決方法是很顯而易見的。首先咱們能夠把問題轉化成:如何在保證兩個文件包名同樣的狀況下,讓系統可以找到咱們的 java 文件?這樣一來思路就很明確了:要麼讓系統來 aidl 包裏面來找 java 文件,要麼把 java 文件放到系統能找到的地方去,也即放到 java 包裏面去。接下來我詳細的講一下這兩種方式具體應該怎麼作:
修改 build.gradle 文件:在 android{} 中間加上下面的內容:
sourceSets { main { java.srcDirs = ['src/main/java', 'src/main/aidl'] } }
也就是把 java 代碼的訪問路徑設置成了 java 包和 aidl 包,這樣一來系統就會到 aidl 包裏面去查找 java 文件,也就達到了咱們的目的。只是有一點,這樣設置後 Android Studio 中的項目目錄會有一些改變,我感受改得挺難看的。
咱們能夠用上面兩個方法之一來解決找不到 .java 文件的坑,具體用哪一個就看你們怎麼選了,反正都挺簡單的。
到這裏咱們就已經將AIDL文件新建而且書寫完畢了,clean 一下項目,若是沒有報錯,這一塊就算是大功告成了。
咱們須要保證,在客戶端和服務端中都有咱們須要用到的 .aidl 文件和其中涉及到的 .java 文件,所以無論在哪一端寫的這些東西,寫完以後咱們都要把這些文件複製到另外一端去。若是是用的上面兩個方法中的第一個解決的找不到 .java 文件的問題,那麼直接將 aidl 包複製到另外一端的 main 目錄下就能夠了;若是是使用第二個方法的話,就除了把把整個 aidl 文件夾拿過去,還要單獨將 .java 文件放到 java 文件夾裏去。
經過上面幾步,咱們已經完成了AIDL及其相關文件的所有內容,那麼咱們究竟應該如何利用這些東西來進行跨進程通訊呢?其實,在咱們寫完AIDL文件並 clean 或者 rebuild 項目以後,編譯器會根據AIDL文件爲咱們生成一個與AIDL文件同名的 .java 文件,這個 .java 文件纔是與咱們的跨進程通訊密切相關的東西。事實上,基本的操做流程就是:在服務端實現AIDL中定義的方法接口的具體邏輯,而後在客戶端調用這些方法接口,從而達到跨進程通訊的目的。
接下來我直接貼上我寫的服務端代碼:
/** * 服務端的AIDLService.java * <p/> * Created by lypeer on 2016/7/17. */ public class AIDLService extends Service { public final String TAG = this.getClass().getSimpleName(); //包含Book對象的list private List<Book> mBooks = new ArrayList<>(); //由AIDL文件生成的BookManager private final BookManager.Stub mBookManager = new BookManager.Stub() { @Override public List<Book> getBooks() throws RemoteException { synchronized (this) { Log.e(TAG, "invoking getBooks() method , now the list is : " + mBooks.toString()); if (mBooks != null) { return mBooks; } return new ArrayList<>(); } } @Override public void addBook(Book book) throws RemoteException { synchronized (this) { if (mBooks == null) { mBooks = new ArrayList<>(); } if (book == null) { Log.e(TAG, "Book is null in In"); book = new Book(); } //嘗試修改book的參數,主要是爲了觀察其到客戶端的反饋 book.setPrice(2333); if (!mBooks.contains(book)) { mBooks.add(book); } //打印mBooks列表,觀察客戶端傳過來的值 Log.e(TAG, "invoking addBooks() method , now the list is : " + mBooks.toString()); } } }; @Override public void onCreate() { super.onCreate(); Book book = new Book(); book.setName("Android開發藝術探索"); book.setPrice(28); mBooks.add(book); } @Nullable @Override public IBinder onBind(Intent intent) { Log.e(getClass().getSimpleName(), String.format("on bind,intent = %s", intent.toString())); return mBookManager; } }
總體的代碼結構很清晰,大體能夠分爲三塊:第一塊是初始化。在 onCreate() 方法裏面我進行了一些數據的初始化操做。第二塊是重寫 BookManager.Stub 中的方法。在這裏面提供AIDL裏面定義的方法接口的具體實現邏輯。第三塊是重寫 onBind() 方法。在裏面返回寫好的 BookManager.Stub 。
接下來在 Manefest 文件裏面註冊這個咱們寫好的 Service ,這個不寫的話咱們前面作的工做都是無用功:
<service android:name=".service.AIDLService" android:exported="true"> <intent-filter> <action android:name="com.lypeer.aidl"/> <category android:name="android.intent.category.DEFAULT"/> </intent-filter> </service>
到這裏咱們的服務端代碼就編寫完畢了,若是你對裏面的一些地方感受有些陌生或者根本不知所云的話,說明你對 Service 相關的知識已經有些遺忘了,建議再去看看這兩篇博文:Android中的Service:默默的奉獻者 (1),Android中的Service:Binder,Messenger,AIDL(2) 。
前面說過,在客戶端咱們要完成的工做主要是調用服務端的方法,可是在那以前,咱們首先要鏈接上服務端,完整的客戶端代碼是這樣的:
/** * 客戶端的AIDLActivity.java * 因爲測試機的無用debug信息太多,故log都是用的e * <p/> * Created by lypeer on 2016/7/17. */ public class AIDLActivity extends AppCompatActivity { //由AIDL文件生成的Java類 private BookManager mBookManager = null; //標誌當前與服務端鏈接情況的布爾值,false爲未鏈接,true爲鏈接中 private boolean mBound = false; //包含Book對象的list private List<Book> mBooks; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_aidl); } /** * 按鈕的點擊事件,點擊以後調用服務端的addBookIn方法 * * @param view */ public void addBook(View view) { //若是與服務端的鏈接處於未鏈接狀態,則嘗試鏈接 if (!mBound) { attemptToBindService(); Toast.makeText(this, "當前與服務端處於未鏈接狀態,正在嘗試重連,請稍後再試", Toast.LENGTH_SHORT).show(); return; } if (mBookManager == null) return; Book book = new Book(); book.setName("APP研發錄In"); book.setPrice(30); try { mBookManager.addBook(book); Log.e(getLocalClassName(), book.toString()); } catch (RemoteException e) { e.printStackTrace(); } } /** * 嘗試與服務端創建鏈接 */ private void attemptToBindService() { Intent intent = new Intent(); intent.setAction("com.lypeer.aidl"); intent.setPackage("com.lypeer.ipcserver"); bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE); } @Override protected void onStart() { super.onStart(); if (!mBound) { attemptToBindService(); } } @Override protected void onStop() { super.onStop(); if (mBound) { unbindService(mServiceConnection); mBound = false; } } private ServiceConnection mServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { Log.e(getLocalClassName(), "service connected"); mBookManager = BookManager.Stub.asInterface(service); mBound = true; if (mBookManager != null) { try { mBooks = mBookManager.getBooks(); Log.e(getLocalClassName(), mBooks.toString()); } catch (RemoteException e) { e.printStackTrace(); } } } @Override public void onServiceDisconnected(ComponentName name) { Log.e(getLocalClassName(), "service disconnected"); mBound = false; } }; }
一樣很清晰,首先創建鏈接,而後在 ServiceConnection 裏面獲取 BookManager 對象,接着經過它來調用服務端的方法。
經過上面的步驟,咱們已經完成了全部的前期工做,接下來就能夠經過AIDL來進行跨進程通訊了!將兩個app同時運行在同一臺手機上,而後調用客戶端的 addBook() 方法,咱們會看到服務端的 logcat 信息是這樣的:
//服務端的 log 信息,我把無用的信息頭去掉了,而後給它編了個號 1,on bind,intent = Intent { act=com.lypeer.aidl pkg=com.lypeer.ipcserver } 2,invoking getBooks() method , now the list is : [name : Android開發藝術探索 , price : 28] 3,invoking addBooks() method , now the list is : [name : Android開發藝術探索 , price : 28, name : APP研發錄In , price : 2333]
客戶端的信息是這樣的:
//客戶端的 log 信息 1,service connected 2,[name : Android開發藝術探索 , price : 28] 3,name : APP研發錄In , price : 2333
全部的 log 信息都很正常而且符合預期——這說明咱們到這裏爲止的步驟都是正確的,按照上面說的來作是可以正確的使用AIDL來進行跨進程通訊的。
這一篇文章主要介紹了咱們在概述裏提到的前三個問題,即:
原本我是準備在這篇文章裏把我那五個問題都講完的,結果寫到這裏發現篇幅已經有些長了,再寫的話可能就少有人有這個耐性讀下去了——那麼寫在後面的這些又有什麼意義呢?因而就乾脆從這裏截斷,將AIDL的工做原理和它的設計思想以及我對於它的這種設計的一些見解放在下一篇博文裏來說述——恰好,有那麼點基礎篇和提升篇的意思,哈哈。