編輯:關於Android編程
由於IPC機制牽扯的東西比較多,所以這裡將分為一個系列進行總結
主要介紹內如如下:
IPC簡介 Android中的多進程模式開啟多進程模式 多進程模式的運行機制 IPC基礎概念介紹
Serializable接口 Parcelable接口 Binder Android中的IPC方式
使用Bundle 使用文件共享 使用Messenger 使用AIDL 使用ContentProvider 使用Socket Binder連接池 選用合適的IPC方式
IPC是Inter-Process Communication的縮寫,其含義是進程間通信或者跨進程通信,是指兩個進程之間進行數據交換的過程。說起進程間通信,我們首先要理解什麼是進程,什麼是線程,進程和線程是截然不同的概念。按照操作系統中的描述,線程是CPU調度最小的單元,同時線程是一種有限的系統資源。而進程一般指一個執行單元,在PC和移動設備上指一個應用程序或一個應用。一個進程可以包含多個線程,因此進程和線程是包含與被包含的關系。最簡單的情況下,一個進程中可以只有一個線程,即主線程,在Android裡面主線程也叫作UI線程,在UI線程裡面才能操作界面的元素。很多時候,一個進程中需要執行大量的耗時的任務,如果這些任務放在主線程中去執行可能會造成界面無法響應,嚴重影響用戶體驗,這種情況在PC系統和移動系統中都存在,在Android中有一個特殊的名字叫做ANR(Application Not Responding),即應用無響應。解決這個問題就需要用到線程,把一些耗時的操作放到線程中去執行即可。而IPC的使用場景也很多,比如:一個應用因為某些原因自身需要采用多進程的模式來實現,至於原因,可能有很多,比如有些模塊由於特殊原因需要運行在單獨的進程中,又或者為了加大一個應用可使用的內存所以需要通過多進程來獲取多份內存空間等等…。
在正式介紹進程間通信之前,我們必須先要理解Android中的多進程模式。通過給四大組件指定Android:process屬性,我們可以輕易地開啟多進程模式,這看起來很簡單,但是實際使用過程中卻暗藏殺機,多進程遠遠沒有我們想的那麼簡單,有時候我們通過多進程得到的好處甚至都不足以彌補使用多進程所帶來的代碼層面的負面影響。下面會詳細分析這些問題。
正常情況下,在Android中多進程是指一個應用中存在多個進程的情況,因此這裡不討論兩個應用之間的多進程情況。首先,在Android中使用多進程只有一種方法,那就是給四大組件(Activity、Service、Receiver、ContentProvider)在AndroidMenifest文件中指定android:process屬性,除此之外沒有其他辦法,也就是說我們無法給一個線程或者一個實體類指定其運行時所在的進程。其實還有另外一種非常規的多進程方法,那就是通過JNI在native層去fork一個新的進程,但是這種方法屬於特殊情況,也不是常用的創建多進程的方式,因此我們不考慮這種方式。下面通過一個示例,描述如何Android中創建多進程:
上面的示例分別為SecondActivity和ThirdActivity指定了process屬性,並且他們的屬性值不同這意味著當前應用又增加了兩個新進程。假設當前應用的包名為“com.layoutinflate.mk.www.myapplication”,當SecondActivity啟動時,系統會為它創建一個單獨的進程,進程名為“com.layoutinflate.mk.www.myapplication:remote”;當ThirdActivity啟動時,系統也會為它創建一個單獨的進程,進程名為“com.layoutinflate.mk.www.myapplication.remote”(這裡需要注意的是這個進程名並不一樣,SecondActivity和ThirdActivity並不在一個進程中,後面會詳細講解)。同時入口Activity是MainActivity,沒有為它指定process屬性,那麼它運行在默認進程中,默認進程的進程名是包名。下面我們來運行下看下效果,如圖所示:
可以看到進程列表中存在者3個進程,進程ID分別為:26864、27148、27174這說明我們的應用已經成功地使用了多進程技術。除了使用IDE的DDMS視圖查看進程信息,還可以使用shell命令來查看,命令為:adb shell ps 或者 adb shell ps " grep com.layoutinflate.mk.www.myapplication。其中,com.layoutinflate.mk.www.myapplication是包名。
$ adb shell ps | grep com.layoutinflate.mk.www.myapplication u0_a92 4129 32487 415604 26416 ffffffff 00000000 S com.layoutinflate.mk.www.myapplication u0_a92 4155 32487 415636 26460 ffffffff 00000000 S com.layoutinflate.mk.www.myapplication:remote u0_a92 4181 32487 420560 28064 ffffffff 00000000 S com.layoutinflate.mk.www.myapplication.remote
說到這裡不知道有沒有注意到,SecondActivity和ThirdActivity的android:process屬性分別為:”:remote” 和 “com.layoutinflate.mk.www.myapplication.remote”,那麼這兩種方式有什麼區別麼?其實是有區別的,區別主要體現在兩方面:首先,“:”的含義是指在當前的進程名前面附件上當前的包名,這是一種簡寫的方法,對於SecondActivity來說,它的完整進程名為”com.layoutinflate.mk.www.myapplication:remote”,這一點可以通過上面給出的效果圖以及我們通過adb shell 都可以查看到,二對於ThirdActivity中的生命方式,它是一種完整的命名方式,不會附加包名信息;其次,進程名以“:”開頭的進程屬於當前應用的私有進程,其他應用的組件不可以和它跑在同一個進程中,而進程名不以
“:”開頭的進程屬於全局進程,其他應用可以通過ShareUID方式可以和它跑在同一個進程。
Android系統會每個應用分配一個唯一的UID,具有相同UID的應用才能共享數據(這個UID是可以手動設置的)。這裡需要說明的是,兩個應用通過ShareUID跑在同一個進程中是有要求的,需要這兩個應用有相同的ShareUID並且簽名相同才可以。在這種情況下,他們可以互相訪問對方的私有數據,比如data目錄,組件信息等,不管它們是否跑在同一個進程中。當然如果它們跑在同一個進程中,那麼除了能共享data目錄、組件信息,還可以共享內存數據,或者說它們看起來就像是一個應用的兩個部分。
大部分人都認為開啟多進程是很簡單的事情,只需要給四大組件指定android:process屬性即可,但是使用多進程真的就那麼簡單麼?可以說“當應用開啟了多進程以後,各種奇怪的現象都出現了”,為什麼這麼說呢?下面我們就通過一個例子來驗證使用多進程時會出現的一些問題:還是之前說的那個例子,其中SecondActivity通過指定android:process屬性從而是其運行在一個獨立的進程中,這裡做了一些改動,我們新建了一個類,叫做UserManager,這個類中有一個public的靜態成員變量,如下所示:
public class UserManager { public static int sUserId = 1; }
我們要做的操作非常簡單,我們只需要在MainActivity的onCreate方法中吧這個sUserId重新賦值為2,打印這個靜態變量後的值後在啟動SecondActivity,在SecondActivity中我們在打印一下sUserID的值,按照我們的正常邏輯,靜態變量是可以在所有地方共享的,並且一處修改處處同步,那麼按照我們的猜想那麼SecondActivity中打印的sUserID的值也應該為2,我們看下結果再來討論;
08-27 10:00:34.299 5804-5804/? E/MainActivity: sUserId = 2 08-27 10:00:37.141 5833-5833/? E/SecondActivity: sUserId = 1
咦!怎麼打印的結果和我們猜想的不一樣呢?從日志上可以看到SecondActivity中打印的sUserId的值還是為“1”,可是我們的確已經在MainActivity中把sUserId重新賦值為2了。看到這裡,大家應該明白了這就是多進程所帶來的問題,多進程絕非只是僅僅指定一個android:process屬性那麼簡單。
上述問題出現的原因是SecondActivity運行在一個單獨的進程中,我們知道Android為每一個應用分配了一個獨立的虛擬機,或者說為每一個進程都分配了一個獨立的虛擬機,不同的虛擬機在內存分配上有不同的地址空間,這就導致在不同的虛擬機中訪問同一個對象會產生多份副本。那我們這個例子來說,在進程“com.layoutinflate.mk.www.myapplication”和進程“com.layoutinflate.mk.www.myapplication:remote”中我們都存在一個UserManager類,並且這兩個類是互不干擾的,在一個進程中修改sUserId的值只會影響當前進程,對其他進程不會造成任何影響,這樣我們就可以理解為什麼在MainActivity中修改了sUserid的值,但是在SecondActivity中sUserId的值卻沒有發生改變這個現象。
所有運行在不同進程中的四大組件,只要他們之間需要通過內存來共享數據,都會共享失敗。(只有在同一個進程中才可以共享內存),這也是多進程多帶來的影響。正常情況下,四大組件中間不可能通過一些中間層來共享數據,那麼簡單地指定進程名來開啟多進程多會無法正確運行。當然特殊情況下,某些組件之間不需要共享數據,這個時候可以直接指定android:process屬性來開啟多進程,但是這種場景是不常見的,幾乎所有情況都需要共享數據。
第1個問題上面已經進行了分析。第2個問題本質上和第一個問題是類似的,既然都不是一塊內存了,那麼不管是鎖對象還是鎖全局類都無法保證線程同步,因為不同進程鎖的不是同一個對象。第三個問題是因為SharePreferences不支持兩個進程去執行寫操作,否則會導致一定幾率的數據丟失,這是因為SharePreference底層是通過讀/寫XML文件來實現的,並發寫顯然是可能出問題的,甚至並發讀/寫都有可能出問題。第四個問題也是顯而易見的,當一個組件跑在一個新的進程中的時候,由於系統要創建新的進程同時分配獨立的虛擬機,所以這個過程其實就是啟動一個應用的過程。因此,相當於系統有吧這個應用重新啟動了一遍,既然都重新啟動了,那麼自然會創建新的Application。這個問題其實可以這麼理解,運行在同一個進程中的組件是屬於同一個虛擬機和同一個Application,同理,運行在不同進程中的組件是屬於兩個不同虛擬機和Application。為了更加清晰地展示這個點,下面我們來做個測試,首先在Application的onCreate方法中打印出當前進程的名字,然後連續啟動三個同一個應用內但不屬於同一個進程的Activity,按照期望,Application的onCreate方法應該會執行三次並打印三次進程名不同的Log,代碼如下所示:
public class MyApplication extends Application { @Override public void onCreate() { super.onCreate(); String curProcessName = getCurProcessName(getApplicationContext()); Log.e("MyApplication", "application process name = " + curProcessName); } private String getCurProcessName(Context context) { int pid = android.os.Process.myPid(); ActivityManager mActivityManager = (ActivityManager) context .getSystemService(Context.ACTIVITY_SERVICE); for (ActivityManager.RunningAppProcessInfo appProcess : mActivityManager .getRunningAppProcesses()) { if (appProcess.pid == pid) { return appProcess.processName; } } return null; } }
運行下開一下log,如圖所示:
08-27 10:56:19.029 8231-8231/? E/MyApplication: application process name = com.layoutinflate.mk.www.myapplication 08-27 10:56:21.745 8282-8282/? E/MyApplication: application process name = com.layoutinflate.mk.www.myapplication:remote 08-27 10:56:23.022 8308-8308/? E/MyApplication: application process name = com.layoutinflate.mk.www.myapplication.remote
通過log可以看出,Application共執行了三次onCreate,並且每次的進程名稱和進程ID都不一樣,它們的進程名是和我們為Activity指定的android:process屬性是一致的。這也就證實了在多進程模式中,不同進程的組件的確會擁有獨立的虛擬機、Application以及內存空間,這會給實際的開發帶來很多困擾。
IPC的基礎概念主要包含三方面的內容:Serializable接口、Parcelable接口以及Binder,只有熟悉這三方面的內容後,我們才能更好地理解跨進程通信的各種方式。Serializable和Parcelable接口可以完成對象的序列化過程,當我們需要通過Intent和Binder傳輸數據時就需要使用Parcelable或者Serializable。還有的時候我們需要把對象持久化到存儲設備上或者通過網絡傳輸給其他客戶端,這個時候也需要使用Serializable來完成對象的持久化。
Serializable是Java所提供的一個序列化接口,它是一個空接口,為對象提供標准的序列化和反序列化操作。使用Serializable來實現序列化相當簡單,只需要在類的聲明中指定一個類似下面的標識即可自動實現默認的序列化過程:
private static final long serialVersionUID = 1L;
上面提到,想讓一個對象實現序列化,只需要這個類實現Serializable接口並聲明一個serialVersionUID即可,實際上,甚至這個serialVersionUID也不是必需的,我們不聲明這個serialVersionUID同樣也可以實現序列化,但是這將會對反序列化過程產生影響,具體什麼影響後面在介紹。下面我們來看個例子:
public class User implements Serializable { private static final long serialVersionUID = 1L; private int userId; private String name; public User(int userId, String name) { this.setUserId(userId); this.setName(name); } public static long getSerialVersionUID() { return serialVersionUID; } public int getUserId() { return userId; } public void setUserId(int userId) { this.userId = userId; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
通過Serializable方式來實現對象的序列化,實現起來非常簡單,幾乎所有的工作都被系統自動完成了。如果進行對象的序列化和反序列化也非常簡單,只需要采用ObjectOutputStream和ObjectInputStream即可輕松實現。下面給出代碼:
//序列化過程 try { User user = new User(0, "mk"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(new File(getCacheDir(),"cache.text"))); objectOutputStream.writeObject(user); objectOutputStream.close(); } catch (IOException e) { } //反序列化過程 try { ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(new File(getCacheDir(),"cache.text"))); User user = (User) objectInputStream.readObject(); Log.e("mainUser", "user" + user + "name" + user.getName() + "userid" + user.getUserId()); objectInputStream.close(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); }
上述代碼演示了采用Serializable方式序列化對象的典型過程,很簡單,只需要把實現了Serializable接口的User對象寫到文件中就可以快速恢復了,恢復後的對象user和user的內容完全一樣,但是兩者並不是同一個對象。
剛開始提到,即使不指定serialVersionUID也可以實現序列化,那到底要不要指定呢?如果指定的話,serialVersionUID後面的那一長串數字又是什麼含義呢?我們要明白,系統既然提供了這個serialVersionUID,那麼它必須是有用的。這個serialVersionUID是用來輔助序列化和反序列化過程的,原則上序列化後的數據中的serialVersionUID只有和當前類的serialVersionUID相同才能夠正常地被反序列化。serialVersionUID的詳細工作機制是這樣的:序列化的時候系統會把當前類的serialVersionUID寫入序列化的文件中(也可能是其他中介),當反序列化的時候系統會去檢測文件中的serialVersionUID,看它是否和當前類的serialVersionUID一致,如果一致就說明序列化的類的版本和當前的版本是相同的,這個時候可以成功反序列化;否則就說明當前類和序列化的類相比發生了某些變換,比如成員變量的數量、類型可能發生了改變,這個時候是無法正常反序列化的。
一般來說,我們應該手動指定serialVersionUID的值,比如1L,也可以讓Eclipse根據當前類的結構自動去生成它的hash值,這樣序列化和反序列化時兩者的serialVersionUID是相同的,因此可以正常反序列化。如果不手動指定serialVersionUID的值,反序列化時當前類有所改變,比如增加或者刪除了某些成員變量,那麼系統就會重新計算當前類的hash值並把它賦值給serialVersionUID,這個時候當前類的serialVersionUID就和序列化的數據中的serialVersionUID不一致,於是反序列化失敗,程序就會出現crash。所以,我們明顯感覺到serialVersionUID的作用,當我們手動指定了它以後,就可以在很大程度上避免反序列化過程的失敗。比如當前版本升級後,我們可能刪除了某個成員變量也可能增加了一些新的成員變量,這個時候我們的反向序列化過程仍然能夠成功,程序仍然能夠最大限度地恢復數據,相反,如果不指定serialVersionUID的話,程序則會掛掉。當然我們還要考慮另外一種情況,如果類結構發生了非常規性的改變,比如修改了類名,修改了成員變量的類型,這個時候盡管serialVersionUID驗證通過了,但是反序列化過程還是會失敗,因為類結構有了毀滅性的改變,根本無法從老版本的數據中還原出一個新的類結構的對象。(給serialVersionUID指定為1L或者采用Eclipse根據當前類結構去生成hash值,這兩者沒有本質區別,效果完全一樣。)
Parcelable也是一個接口,只要實現這個接口,一個類的對象就可以實現序列化並可以通過Intent和Binder傳遞。
直觀來說,Binder是Android中的一個類,它實現了IBinder接口。從IPC角度來說,Binder是Android中的一種跨進程通信方式,Binder還可以理解為一種虛擬的物理設備,它的設備驅動是/dev/binder,該通信方式在Linux中沒有;從AndroidFramework角度來說,Binder是ServiceManger連接各種Manager(ActivityManager、WindowManager,等等)和相應ManagerService的橋梁;從Android應用層來說,Binder是客戶端和服務端進行通信的媒介,當bindService的時候,服務端會返回一個包含了服務端業務調用的Binder對象,通過這個Binder對象,客戶端就可以獲取服務端提供的服務或者數據,這裡的服務包括普通服務和基於AIDL的服務。
Android開發中,Binder主要用在Service中,包括AIDL和Messager,其中普通Service中的Binder不涉及進程間通信,所以較為簡單,無法設計Binder的核心(startService),而Messager的底層其實是AIDL,所以這裡選用AIDL來分析Binder的工作機制,這裡我們只分析系統為我們自動生成的Binder類,我們都知道我們在使用AIDL的時候,都需要創建一個接口(這個接口就是我們的Binder),這裡我們就暫時創建一個IBookManager.aidl文件,代碼如下所示:
我們自己創建的AIDL文件:
// IBookManager.aidl package com.layoutinflate.mk.www.myapplication; // Declare any non-default types here with import statements interface IBookManager { void addBook(); }
系統為IBookManager生成的Binder類:
/* * This file is auto-generated. DO NOT MODIFY. * Original file: G:\\android_github_project\\GitLab\\MyApplication\\app\\src\\main\\aidl\\com\\layoutinflate\\mk\\www\\myapplication\\IBookManager.aidl */ package com.layoutinflate.mk.www.myapplication; // Declare any non-default types here with import statements public interface IBookManager extends android.os.IInterface { /** * Local-side IPC implementation stub class. */ public static abstract class Stub extends android.os.Binder implements com.layoutinflate.mk.www.myapplication.IBookManager { private static final java.lang.String DESCRIPTOR = "com.layoutinflate.mk.www.myapplication.IBookManager"; /** * Construct the stub at attach it to the interface. */ public Stub() { this.attachInterface(this, DESCRIPTOR); } /** * Cast an IBinder object into an com.layoutinflate.mk.www.myapplication.IBookManager interface, * generating a proxy if needed. */ public static com.layoutinflate.mk.www.myapplication.IBookManager asInterface(android.os.IBinder obj) { if ((obj == null)) { return null; } android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR); if (((iin != null) && (iin instanceof com.layoutinflate.mk.www.myapplication.IBookManager))) { return ((com.layoutinflate.mk.www.myapplication.IBookManager) iin); } return new com.layoutinflate.mk.www.myapplication.IBookManager.Stub.Proxy(obj); } @Override public android.os.IBinder asBinder() { return this; } @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException { switch (code) { case INTERFACE_TRANSACTION: { reply.writeString(DESCRIPTOR); return true; } case TRANSACTION_addBook: { data.enforceInterface(DESCRIPTOR); this.addBook(); reply.writeNoException(); return true; } } return super.onTransact(code, data, reply, flags); } private static class Proxy implements com.layoutinflate.mk.www.myapplication.IBookManager { private android.os.IBinder mRemote; Proxy(android.os.IBinder remote) { mRemote = remote; } @Override public android.os.IBinder asBinder() { return mRemote; } public java.lang.String getInterfaceDescriptor() { return DESCRIPTOR; } @Override public void addBook() throws android.os.RemoteException { android.os.Parcel _data = android.os.Parcel.obtain(); android.os.Parcel _reply = android.os.Parcel.obtain(); try { _data.writeInterfaceToken(DESCRIPTOR); mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0); _reply.readException(); } finally { _reply.recycle(); _data.recycle(); } } } static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0); } public void addBook() throws android.os.RemoteException; }
上述代碼是系統生成的,在gen目錄下(使用Android studio是在build/generated/resource/aidl/debug下)可以看到根據IBookManager.aidl系統為我們生成了IBookManager.java 類,它繼承了IInterface這個接口,同時它自己也還是個接口,所以可以在Binder中傳輸的接口都需要繼承IInterface接口。這個類剛開始看起來邏輯很混亂,但是實際上還是很清晰的,通過它我們可以很清楚地了解到Binder的工作機制。這個類的結構其實很簡單,首先,它聲明了一個方法addBook,顯然這就是我們在IBookManager.aidl中聲明的方法,同時它還聲明了一個整型的id用於標識這個方法(TRANSACTION_addBook),這個id用於標識在transact過程中客戶端所請求的到底是那個方法。接著,它聲明了一個內部類Stub,這個Stub就是一個Binder類,同時也是IBookManager的具體實現,當客戶端和服務端都位於同一個進程時,方法調用不會走跨進程的transact過程,而當兩者位於不同進程時,方法調用需要走transact過程,這個邏輯有Stub的內部代理類Proxy來完成。這麼看來,IBookManager這個接口的確很簡單,但是我們也應該認識到,這個接口的核心實現就是它的內部類Stub和Stub的內部代理類Proxy,下面詳細介紹針對這兩個類的每個方法的含義:
1、DESCRIPTOR
Binder的唯一標識,一般用當前Binder的類名表示,比如本例中的“com.layoutinflate.mk.www.myapplication.IBookManager”
2、asInterface(android.os.IBinder obj)
用於將服務端的Binder對象轉換為客戶端所需要的AIDL接口類型的對象,這種轉換過程是區分進程的,如果客戶端和服務端位於同一進程,那麼此方法返回的就是服務端的Stub對象本身,否則返回的是系統封裝後的Stub.proxy對象。
3、asBinder
此方法用於返回當前Binder對象。
4、onTransact
這個方法運行在服務端中的Binder線程池中,當客戶端發起跨進程請求時,遠程請求會通過系統底層封裝後交友此方法來處理。該方法的原型為 public Boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags)。服務端通過code可以確定可以確定客戶端所請求的目標方法是什麼,接著從data中取出目標方法所需的參數(如果目標方法有參數的話),然後執行目標方法。當目標方法執行完畢後,就向reply中寫入返回值(如果目標方法有返回值的話),onTransact方法的執行過程就是這樣的。需要注意的是,如果此方法返回false,那麼客戶端會請求失敗,因此我們可以利用這個特性來做權限驗證,畢竟我們也不希望隨便一個進程都能遠程調用我們的服務。
5、Proxy#addBook
這個方法運行在客戶端,當客戶端遠程調用此方法時,它的內部實現是這樣的:首先創建該方法需要的輸入型Parcel對象_data、輸出型對象_reply;然後把該方法的參數信息寫入_data中(如果有參數的話);接著調用transact方法來發起RPC(遠程過程調用)請求,同時當前線程掛起;然後服務端的onTransact方法會被調用,直到RPC過程返回後當前線程繼續執行,並從_reply中取出RPC過程的返回結果;最後返回_reply中的數據。
通過上面的分析,我們應該已經了解了Binder的工作機制,但是有兩點還是需要額外說明一下:首先,當客戶端發起遠程請求時,由於當前線程會被掛起直至服務端進程返回數據,所以如果一個遠程方法是很耗時的,那麼不能在UI線程中發起此遠程請求;其次由於服務端的Binder方法運行在Binder的線程池中,所以Binder方法不管是否耗時都應該采用同步的方式去實現,因為它已經運行在一個線程中了,為了更好地說明Binder,給出Binder的工作機制圖:
可以根據上述例子中的Proxy#addBook 結合onTransact來分析上圖的具體執行流程。
以前也實現過,ListView的上拉刷新,不過用的是開源代碼,由於本人比較懶吧,源碼也沒怎麼研究,所以現在寫出來還不是那麼流利。還好本人發現了自己的這些特點,所以寫出來個
Call openFileOutput() with the name of the file and the operating mode. This return
功能描述菜單分左右兩側,整體可以滑動,效果如下功能分析widthMeasureSpec:期望值組成: 32位的010101010101011010101組成 頭2位:代表
方案一:PreLollipopTransition首先在 build.gradle 配置文件添加這個庫依賴dependencies { compile