Service與Android系統設計(2)-- Parcel

特別聲明:本系列文章LiAnLab.org著作權所有,轉載請註明出處。作者系LiAnLab.org資深Android技術顧問吳赫老師。本系列文章交流與討論:@宋寶華Barry

共18次連載,講述Android Service背後的實現原理,透析Binder相關的RPC。

Parcel與Parcelable

當我們在調用遠程方法時,需要在進程間傳遞參數以及返回結果。這種類似的處理方式,需要把數據與進程相關性去除,變成一種中間形式,然後按統一的接口進行讀寫操作。這樣的機制,一般在高級編程語言裏都被稱爲序列化。
在Android世界裏處理數據的序列化操作的,使用了一種Parcel類,而能夠處理數據序列能力,則是實現Parcelable接口來實現。於是,當我們需要在進程間傳輸一個對象,則實現這一對象的類必須實現Parcelable接口裏定義的相應屬性或方法,而在使用這一對象時,則可以使用一個Parcel引用來處理傳輸時的基本操作。
前面說明的AIDL編程,都只是針對String,Int等Java的基本數據類型,如果我們需要處理一些複雜的數據類型,或是需要定義一些自定義的數據類型,這時,我們需要aidl裏的另一種效用,導入Parcelable接口。

在aidl環境裏使用Parcelable接口,相對來說會更簡單。我們前面也強調過,在aidl工作的實現上,爲了達到簡潔高效的設計目標,aidl只支持基本的數據類型,一個接口類在aidl環境裏僅會定義方法,而不會涉及屬性定義,所以這種限制作用到Parcelable,Parcelable在aidl會只是簡單的一行,其他aidl使用到這些Parcelable接口的部分,可以直接引用定義這一Parcelable的aidl文件即可。

基於我們前面的例子來拓展一個Parcelable的例子,假如我們希望在進程能夠傳遞一些進程間的描述信息,我們會把這樣的數據組織到一個類時進行傳輸,我們可以把這個類叫TaskInfo,於是在單進程環境裏,我們大致會有這樣一個類:

[java] view plaincopy
  1. class TaskInfo {  
  2.     public long mPss;  
  3.     public long mTotalMemory;  
  4.     public long mElapsedTime;  
  5.     public int mPid;  
  6.     public int mUid;  
  7.     public String mPackageName;  
  8.    
  9.    TaskInfo() {  
  10.        mPss = 0;  
  11.        mTotalMemory = 0;  
  12.        mElapsedTime = 0;  
  13.        mPid = -1;  
  14.        mUid = -1;  
  15.        mPackageName = null;  
  16.     }  
  17. }  


我們通過這一個類來創建一個對象時,這個對象的有效使用範圍只是在一個進程裏,因爲進程空間是各自獨立的,在進程空間裏,實際上這一對象是保存在堆空間(Heap)裏的。如果我們希望通過IPC機制來直接傳遞這麼一個對象也是不現實的,因爲在Java環境裏對象是通過引用來訪問,一個對象裏對其他對象,比如訪問TaskInfo類的packageName,則是會通過一個類似於指針的引用來訪問。

所以,通過IPC來複制結構比較複雜的對象時,必須要通過某種機制可以將對象拆解成一種中間格式,通過在IPC裏傳輸這種中間格式然後得到對象在進程之間互相傳遞的效果。無論是使用什麼樣的IPC機制,這種傳輸過程都是會是一邊把數據寫進去,另一邊讀出來,根據我們前面的分析,我們使用一個繼承自Binder的對象即可完成這種功能。但這種讀寫兩端的代碼會不停重複,也容易引起潛在的錯誤,Android系統會使用面向對象的技巧來簡化這種操作。與進程間方法的相互調用不同,在進程間傳遞對象不在乎交互,而更關注使用時的靈活性。在對象傳遞時,共性是傳輸機制,差異性是傳輸的對象構成,於是我們在Binder類之上,再派生出一個Parcel類,專用於處理屬於共性部分的數據傳遞,而針對差異性部分對象的構成,則通過一個Parcelable的接口類,可以通過這一接口將對象組成上的差異性通知到Parcel。

有了Parcel類與Parcelable接口類之後,我們要傳遞像TaskInfo對象則變得更容易。我們可以讓我們的TaskInfo類來實現Parcelable功能,使這一類生成的對象,都將在底層通過Parcel對象來完成底層的傳輸。我們前面定義的TaskInfo類,則會被變成如下的樣子:

[java] view plaincopy
  1. package org.lianlab.services;  
  2. import android.os.Parcel;  
  3. import android.os.Parcelable;  
  4.    
  5. public class TaskInfo implements Parcelable {  
  6.     public long mPss;  
  7.     public long mTotalMemory;  
  8.     public long mElapsedTime;  
  9.     public int mPid;  
  10.     public int mUid;  
  11.     public String mPackageName;  
  12.    
  13.    TaskInfo() {  
  14.        mPss = 0;  
  15.        mTotalMemory = 0;  
  16.        mElapsedTime = 0;  
  17.        mPid = -1;  
  18.        mUid = -1;  
  19.        mPackageName = null;  
  20.     }  
  21.    
  22.     public int describeContents() {                1  
  23.        return 0;  
  24.     }  
  25.    
  26.     public void writeToParcel(Parcel out, int flags) {    2  
  27.        out.writeLong(mPss);  
  28.        out.writeLong(mTotalMemory);  
  29.        out.writeLong(mElapsedTime);  
  30.        out.writeInt(mPid);  
  31.        out.writeInt(mUid);  
  32.        out.writeString(mPackageName);  
  33.     }  
  34.    
  35.     public static final Parcelable.Creator<TaskInfo>CREATOR = new Parcelable.Creator<TaskInfo>() {                 3  
  36.        public TaskInfo createFromParcel(Parcel in) {            4  
  37.            return new TaskInfo(in);  
  38.        }  
  39.    
  40.        public TaskInfo[] newArray(int size) {             5  
  41.            return new TaskInfo[size];  
  42.        }  
  43.     };  
  44.    
  45.     privateTaskInfo(Parcel in) {                 6  
  46.        mPss = in.readLong();  
  47.        mTotalMemory = in.readLong();  
  48.        mElapsedTime = in.readLong();  
  49.        mPid = in.readInt();  
  50.        mUid = in.readInt();  
  51.        mPackageName = in.readString();  
  52.     }  
  53. }  

  1. describeContents(),Parcelabl所需要的接口方法之一,必須實現。這一方法作用很簡單,就是通過返回的整形來描述這一Parcel是起什麼作用的,通過這一整形每個bit來描述其類型,一般會返回0。
  2. writeToParcel(),Parcelabl所需要的接口方法之二,必須實現。writeToParcel()方法的作用是發送,就是將類所需要傳輸的屬性寫到Parcel裏,被用來提供發送功能的Parcel,會作爲第一個參數傳入,於是在這個方法裏都是使用writeInt()、writeLong()寫入到Parcel裏。這一方法的第二參數是一個flag值,可以用來指定這樣的發送是單向還是雙向的,可以與aidl的in、out、inout三種限定符匹配。
  3. CREATOR對象,Parcelable接口所需要的第三項,必須提供實現,但這是一個是接口對象。正如我們看到的,這一CREATOR對象,是使用模板類Parcelable.Creator,套用到TaskInfo來得到的,Parcelable.Creator<TaskInfo>。這個CREATOR對象在很大程度上是一個工廠(Factory)類,用於遠程對象在接收端的創建。從某種意義上來說,writeToParcel()與CREATOR是一一對應的,發送端進程通過writeToParcel(),使用一個Parcel對象將中間結果保存起來,而接收端進程則會使用CREATOR對象把作爲Parcel對象的中間對象再恢復出來,通過類的初始化方法以這個Parcel對象爲基礎來創建新對象。後續的4-6,則是完成這個CREATOR對象的實現。
  4. createFromParcel(),這是ParcelableCreator<T>模板類所必須實現的接口方法,提供從Parcel轉義出新的對象的能力。接收端來接收傳輸過來的Parcel對象時,便會以這一個接口方法來取得對象。我們這裏直接調用基於Parcel 的類的初始化方法,然後將創建的對象返回。
  5. newArray(),這是ParcelableCreator<T>模板類所必須實現的另一個接口方法,但這一方法用於創建多個這種實現了Parcelable接口的類。通過這一方法,CREATOR對象不光能創建單個對象,也能返回多個創建好的空對象,但多個對象不能以某個Parcel對象爲基礎創建,於是會使用默認的類創始化方法。
  6. 實現具體的以Parcel爲參照的初始化方法,這並非必須,我們也可以在createFromParcel()裏直接根據Parcel的值賦值到對象來實現,但這樣實現則更清晰。這一方法,基本上與writeToParcel()是成對的,以什麼順序將對象屬性寫入Parcel,則在createFromParcel()會就會以同樣的順序對象屬性從Parcel裏讀出來,使用Parcel的readInt()、readLong()等方法來完成。

通過上述的這一個類的定義,我們便得到了一個可以被跨進程傳輸的類,通過這個類所創建的對象,可以無縫地在進程進行傳輸。如果需要使用這一我們已經定義好的 Parcelable類,我們只需要新建一個aidl文件,描述這一Parcelable的存在信息,從而通知到aidl編譯工具。如果是基於我們前面例子裏定義的Parcelable類TaskInfo,我們會需要使用一個與之同名的,叫TaskInfo.aidl的文件:

package org.lianlab.services;

parcelableTaskInfo;

這一文件很簡單,實際就是一行用於定義包名,一行定義存在一個實現了Parcelable接口的TaskInfo類。然後,我們在需要使用它的部分再將這一aidl文件導入,我們可以在前面的ITaskService.aidl進行拓展,定義一個新的getTaskStatus()接口方法來返回一個TaskInfo對象。在這些定義接口方法的aidl文件裏使用Parcelable類很簡單,只需要引用定義這一Parcelable類的aidl文件即可:

[java] view plaincopy
  1. package org.lianlab.services;  
  2. importorg.lianlab.services.ITaskServiceCallback;  
  3. import org.lianlab.services.TaskInfo;  
  4.    
  5. interface ITaskService {  
  6.     intgetPid (ITaskServiceCallback callback);  
  7.    TaskInfo getTaskStatus ();  
  8. }  

當然,這時我們的ITaskService接口變掉了,於是我們會需要改寫我們的Stub類的實現,我們需要到TaskService.java的實現裏,新增強getTaskStatus()方法的實現:

[java] view plaincopy
  1. private final ITaskService.StubmTaskServiceBinder =new ITaskService.Stub() {  
  2.    public int getPid(ITaskServiceCallback callback) {  
  3.        mCount++;  
  4.        try {  
  5.             callback.valueCounted(mCount);  
  6.        } catch (RemoteException e) {  
  7.             e.printStackTrace();  
  8.        }  
  9.        return Process.myPid();  
  10.    }  
  11.   
  12.    public TaskInfo getTaskStatus() {  
  13.   
  14.        TaskInfo mTaskInfo = new TaskInfo();  
  15.        mTaskInfo.mUid = Process.myUid();  
  16.        Debug.MemoryInfo memInfo = new Debug.MemoryInfo();  
  17.        Debug.getMemoryInfo(memInfo);  
  18.        mTaskInfo.mPss = memInfo.nativePss;  
  19.   
  20.        Runtime runtime = Runtime.getRuntime();  
  21.        mTaskInfo.mTotalMemory = runtime.totalMemory();  
  22.   
  23.        mTaskInfo.mPid = Debug.getBinderReceivedTransactions();  
  24.        mTaskInfo.mElapsedTime = Process.getElapsedCpuTime();  
  25.        mTaskInfo.mPackageName = getPackageName();  
  26.        return mTaskInfo;  
  27.    }  
  28.   
  29. };  

在我們新增的getTaskStatus()方法裏,我們會創建一個新的TaskInfo對象,然後根據當前的進程上下文環境給這一TaskInfo對象進行賦值,然後再返回這一對象。由於TaskInfo現在已經榮升成Parcelable對象了,於是這一返回,實際上會發生跨進程環境裏,在Remote Service的調用端返回這一對象。

從上面的代碼示例,我們大致也可以分析到通過Parcel來傳遞對象的原理。這跟我們中寄送由零部件組成的物品類似。生活中,我們寄運由零部件構成的物品,一般是把東西拆散成零組件,於是好包裝也方便運輸,把零部件儘可能靈活擺放塞進一個盒子裏,再寄送出去。接收到這個包裹的那方,會從盒子裏將零部件拆散開來,再按拆卸時同樣的構架再將零部件組裝到一起,於是我們就得到了原來的造型各式的物品。出於這樣的類比性,於是我們的負責搬運的類叫Parcel,就是包裹的意思,而用於搬運的拆裝過程,或者準確的是說是可拆卸然後再組裝的能力,就叫稱爲Parcelable。

對於Parcel的傳輸,0xLab的黃敬羣先生(JServ)作了一個形象的比喻,希望通過傳真機來發送一個紙盒子到遠端。傳真機不是時空傳送帶,並不會真正可以實現某個物品的跨空間傳遞,但我們一定的變通來完成:


       我們可以把一個紙盒子拆解攤開,這時得到一個完全平面化的帶六個格子的一個圖形。我們通過傳真機傳送時就有了可能,我們傳真這個帶六個格子的圖形到遠端,這時遠端的傳真機就會收到同是六個格子的傳真圖像,打印到一張紙上。然後我們再將這張紙裁減開來,重新粘貼到一起,於是也可以得到一個紙盒子,跟我們原始的紙盒有着一模一樣的外觀。

於是,在發送時,我們大體上可認爲是把對象進行拆解打包,然後塞進Parcel對象裏。Parcel此時相當於容器的作用,於是其內容一定會有一段buffer用於存放這種中間結果。於是這一過程就會是通過writeToParcel()方法,將對象的屬性分拆開,填寫到這個Parcel的buffer裏:


因爲這一過程,在計算處理上相當於把原有來非線性存放的對象,通過平整化轉移到一個線性的內存空間裏進行保存,於是這一操作被稱爲平整化”flatter”。從前面的代碼我們也可以看到,所謂的平整化操作,就是通過writeToParcel()寫入到一個Parcel對象裏。我們更細一點來觀察的話,這一過程就是通過Parcel對象的writeInt()、writeLogn()、writeString()等不同操作方法,將對象的屬性寫入到Parcel的內置buffer裏。

在讀取的那一端,我們會通過某種手段,將Parcel對象讀出來,通過CREATOR裏定義的初始化方法,得到Parcel裏保存的對象:


對應於發送時的flatten,我們接收時的操作就是unflatten,將對象從一個線性化保存在Parcel的內置buffer裏的數據,還原成具體的對象。這一步驟會是通過CREATOR裏通過Parcel來初始化對象的createFromParcel()來完成。

到此,我們至少就得到通過一箇中間區域來搬運與傳送對象的能力。對象不能直接拷貝與傳輸,但線性化內存是可以的,我們可以把內存從0到結果的位置通過某種IPC機制發送出去,在另一端進行接收,就可以得到這段內存的內容。最後,如果我們把Parcel對象的這段內容通過Binder通信來進行傳輸,此時我們就得到了進程間對象互相傳送的能力:


我們在進行跨進程的對象傳遞過程裏,都通過Parcel這樣的包裝來進行傳輸。所以只要我們的Parcel足夠強大與靈活,我們可以進程間傳遞一切。我們的例子裏僅使用Int、Long、String等基本類型,實際上Parcel的能力遠不止如此。Parcel支持全部的基本數據型,同時因爲Parcelable.Creator<T>模板同時支持newArray()與createFromParcel()兩種接口方法,於是在傳輸時可以將兩者結合,通過newArray()進行對象創建,同時通過createFromParcel()再依次初始化,於是Parcel自動會支持數組類型的數據。而對[]操作符的支持,使在Parcel對象裏實現List、Map等類型非常容易。出於對傳輸時的節省存儲與拷貝時的開銷,於是在Array之上來提供了對SpareArray這樣鬆散數組的支持。Parcel還可以包含Parcel子類型,於是進一步提升了其對對象封裝處理的能力。最後,通過IBinder、Bundle、Serializable類型的支持幾乎完善了Parcel所能傳輸對象的能力。Parcel所能完整支持數據類型有:

  •   null
  •   String
  •   Byte
  •   Short
  •   Integer
  •   Long
  •   Float
  •   Double
  •   Boolean
  •   String[]
  •   boolean[]
  •   byte[]
  •   int[]
  •   long[]
  •   Object[]
  •   Bundle
  •   Map
  •   Parcelable
  •   Parcelable[]
  •   CharSequence
  •   List
  •   SparseArray
  •   IBinder
  •   Serializable

但Parcel並不提供完整的序列化支持,僅是一種跨進程傳輸時的高效手段,我們的對象在打包成Parcel時並非自動支持,都是靠writeToParcel()實現將對象分拆寫入的,我們必須保證讀與寫的順序一致,createFromParcel()與writeToParcel()裏的操作必須是一一對應的。另外,Parcel對象被存儲完成後,也是我們自定義的存儲,如果有任何讀寫上的變動,則這一保存過的中間結果就失效了。如果有序列化的需求,必須通過Parcel來傳遞Bundle類型來實現。但從實現上來看,這樣簡化設計,也是我們得到了更高效地對象傳輸能力,在Parcel內部操作裏,大部分還是靠JNI實現的更高效版本。所以,這種傳輸機制並不被稱爲序列化操作,而被稱爲Parcelable傳輸協議。

所以,在跨進程間的通信過程時,我們不但通過Binder來構造出了能夠以RPC方式進行通信的能力,也得到了通過Parcel來傳遞複雜對象的能力。這樣,我們的跨進程通信的能力就幾乎可以完成任何操作了。在我們應用程序程序裏,通過aidl設計,可以使構架更靈活,一些需要有後臺長期存在又會被多個部分所共享的代碼,都可以使用aidl實現,這時雖然會帶來一定的進程調度上的開銷,但使我們的構架將變得更加靈活。這樣應用情境有下載、登錄、第三方API、音樂播放等諸多使用上的實例。

Aidl對於Android系統的重要性更大,因爲整個Android環境,都是構建在一種多進程的“沙盒”模型之上。爲了支撐這種高靈活性的多進程設計,Android必然需要大量地使用aidl將系統功能拆分開。而aidl是僅對Java有效的一種編程環境,於是對於Android系統構架來說,aidl是Android系統實現裏由Java構建的基礎環境,我們每種需要暴露出來供應用程序使用的系統功能,必然都會是基於aidl實現的跨進程調用。當然,Android系統實現上並非全是由Java來構建的,對於底層一些有更高性能要求的代碼,有可能也是由C/C++編寫出來的,這就是我們在稍後要介紹的NativeService,但我們要知道,所謂的NativeService,也只不過是通過可執行代碼接入到aidl環境,通過Native代碼來模擬aidl的Service代碼實現的。上層代碼都使用跟aidl一模一樣調用方式,底層實現的細節,對上層來說,是透明的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章