天天看點

Android Context完全解析,Context你用對了嗎

Context相信所有的Android開發人員基本上每天都在接觸,因為它太常見了。但是這并不代表Context沒有什麼東西好講的,實際上Context有太多小的細節并不被大家所關注,那麼今天我們就來學習一下那些你所不知道的細節。

Context類型

我們知道,Android應用都是使用Java語言來編寫的,那麼大家可以思考一下,一個Android程式和一個Java程式,他們最大的差別在哪裡?劃分界限又是什麼呢?其實簡單點分析,Android程式不像Java程式一樣,随便建立一個類,寫個main()方法就能跑了,而是要有一個完整的Android工程環境,在這個環境下,我們有像Activity、Service、BroadcastReceiver等系統元件,而這些元件并不是像一個普通的Java對象new一下就能建立執行個體的了,而是要有它們各自的上下文環境,也就是我們這裡讨論的Context。可以這樣講,Context是維持Android程式中各元件能夠正常工作的一個核心功能類。

下面我們來看一下Context的繼承結構:

Android Context完全解析,Context你用對了嗎

Context的繼承結構還是稍微有點複雜的,可以看到,直系子類有兩個,一個是ContextWrapper,一個是ContextImpl。那麼從名字上就可以看出,ContextWrapper是上下文功能的封裝類,而ContextImpl則是上下文功能的實作類。而ContextWrapper又有三個直接的子類,ContextThemeWrapper、Service和Application。其中,ContextThemeWrapper是一個帶主題的封裝類,而它有一個直接子類就是Activity。

那麼在這裡我們至少看到了幾個所比較熟悉的面孔,Activity、Service、還有Application。由此,其實我們就已經可以得出結論了,Context一共有三種類型,分别是Application、Activity和Service。這三個類雖然分别各種承擔着不同的作用,但它們都屬于Context的一種,而它們具體Context的功能則是由ContextImpl類去實作的。

那麼Context到底可以實作哪些功能呢?這個就實在是太多了,彈出Toast、啟動Activity、啟動Service、發送廣播、操作資料庫等等等等都需要用到Context。由于Context的具體能力是由ContextImpl類去實作的,是以在絕大多數場景下,Activity、Service和Application這三種類型的Context都是可以通用的。不過有幾種場景比較特殊,比如啟動Activity,還有彈出Dialog。出于安全原因的考慮,Android是不允許Activity或Dialog憑空出現的,一個Activity的啟動必須要建立在另一個Activity的基礎之上,也就是以此形成的傳回棧。而Dialog則必須在一個Activity上面彈出(除非是System Alert類型的Dialog),是以在這種場景下,我們隻能使用Activity類型的Context,否則将會出錯。

Context數量

那麼一個應用程式中到底有多少個Context呢?其實根據上面的Context類型我們就已經可以得出答案了。Context一共有Application、Activity和Service三種類型,是以一個應用程式中Context數量的計算公式就可以這樣寫:

Context數量 = Activity數量 + Service數量 + 1
           

上面的1代表着Application的數量,因為一個應用程式中可以有多個Activity和多個Service,但是隻能有一個Application。

Application Context的設計

基本上每一個應用程式都會有一個自己的Application,并讓它繼承自系統的Application類,然後在自己的Application類中去封裝一些通用的操作。其實這并不是Google所推薦的一種做法,因為這樣我們隻是把Application當成了一個通用工具類來使用的,而實際上使用一個簡單的單例類也可以實作同樣的功能。但是根據我的觀察,有太多的項目都是這樣使用Application的。當然這種做法也并沒有什麼副作用,隻是說明還是有不少人對于Application了解的還有些欠缺。那麼這裡我們先來對Application的設計進行分析,講一些大家所不知道的細節,然後再看一下平時使用Application的問題。

首先建立一個MyApplication并讓它繼承自Application,然後在AndroidManifest.xml檔案中對MyApplication進行指定,如下所示:

  1. <application
  2. android:name= ".MyApplication"
  3. android:allowBackup= "true"
  4. android:icon= "@drawable/ic_launcher"
  5. android:label= "@string/app_name"
  6. android:theme= "@style/AppTheme" >
  7. ......
  8. </application>

指定完成後,當我們的程式啟動時Android系統就會建立一個MyApplication的執行個體,如果這裡不指定的話就會預設建立一個Application的執行個體。

前面提到過,現在很多的Application都是被當作通用工具類來使用的,那麼既然作為一個通用工具類,我們要怎樣才能擷取到它的執行個體呢?如下所示:

  1. public class MainActivity extends Activity {
  2. @Override
  3. protected void onCreate(Bundle savedInstanceState) {
  4. super.onCreate(savedInstanceState);
  5. setContentView(R.layout.activity_main);
  6. MyApplication myApp = (MyApplication) getApplication();
  7. Log.d( "TAG", "getApplication is " + myApp);
  8. }
  9. }

可以看到,代碼很簡單,隻需要調用getApplication()方法就能拿到我們自定義的Application的執行個體了,列印結果如下所示:

Android Context完全解析,Context你用對了嗎

那麼除了getApplication()方法,其實還有一個getApplicationContext()方法,這兩個方法看上去好像有點關聯,那麼它們的差別是什麼呢?我們将代碼修改一下:

  1. public class MainActivity extends Activity {
  2. @Override
  3. protected void onCreate(Bundle savedInstanceState) {
  4. super.onCreate(savedInstanceState);
  5. setContentView(R.layout.activity_main);
  6. MyApplication myApp = (MyApplication) getApplication();
  7. Log.d( "TAG", "getApplication is " + myApp);
  8. Context appContext = getApplicationContext();
  9. Log.d( "TAG", "getApplicationContext is " + appContext);
  10. }
  11. }

同樣,我們把getApplicationContext()的結果列印了出來,現在重新運作代碼,結果如下圖所示:

Android Context完全解析,Context你用對了嗎

咦?好像列印出的結果是一樣的呀,連後面的記憶體位址都是相同的,看來它們是同一個對象。其實這個結果也很好了解,因為前面已經說過了,Application本身就是一個Context,是以這裡擷取getApplicationContext()得到的結果就是MyApplication本身的執行個體。

那麼有的朋友可能就會問了,既然這兩個方法得到的結果都是相同的,那麼Android為什麼要提供兩個功能重複的方法呢?實際上這兩個方法在作用域上有比較大的差別。getApplication()方法的語義性非常強,一看就知道是用來擷取Application執行個體的,但是這個方法隻有在Activity和Service中才能調用的到。那麼也許在絕大多數情況下我們都是在Activity或者Service中使用Application的,但是如果在一些其它的場景,比如BroadcastReceiver中也想獲得Application的執行個體,這時就可以借助getApplicationContext()方法了,如下所示:

  1. public class MyReceiver extends BroadcastReceiver {
  2. @Override
  3. public void onReceive(Context context, Intent intent) {
  4. MyApplication myApp = (MyApplication) context.getApplicationContext();
  5. Log.d( "TAG", "myApp is " + myApp);
  6. }
  7. }

也就是說,getApplicationContext()方法的作用域會更廣一些,任何一個Context的執行個體,隻要調用getApplicationContext()方法都可以拿到我們的Application對象。

那麼更加細心的朋友會發現,除了這兩個方法之外,其實還有一個getBaseContext()方法,這個baseContext又是什麼東西呢?我們還是通過列印的方式來驗證一下:

Android Context完全解析,Context你用對了嗎

哦?這次得到的是不同的對象了,getBaseContext()方法得到的是一個ContextImpl對象。這個ContextImpl是不是感覺有點似曾相識?回去看一下Context的繼承結構圖吧,ContextImpl正是上下文功能的實作類。也就是說像Application、Activity這樣的類其實并不會去具體實作Context的功能,而僅僅是做了一層接口封裝而已,Context的具體功能都是由ContextImpl類去完成的。那麼這樣的設計到底是怎麼實作的呢?我們還是來看一下源碼吧。因為Application、Activity、Service都是直接或間接繼承自ContextWrapper的,我們就直接看ContextWrapper的源碼,如下所示:

  1. /**
  2. * Proxying implementation of Context that simply delegates all of its calls to
  3. * another Context. Can be subclassed to modify behavior without changing
  4. * the original Context.
  5. */
  6. public class ContextWrapper extends Context {
  7. Context mBase;
  8. /**
  9. * Set the base context for this ContextWrapper. All calls will then be
  10. * delegated to the base context. Throws
  11. * IllegalStateException if a base context has already been set.
  12. *
  13. * @param base The new base context for this wrapper.
  14. */
  15. protected void attachBaseContext(Context base) {
  16. if (mBase != null) {
  17. throw new IllegalStateException( "Base context already set");
  18. }
  19. mBase = base;
  20. }
  21. /**
  22. * @return the base context as set by the constructor or setBaseContext
  23. */
  24. public Context getBaseContext() {
  25. return mBase;
  26. }
  27. @Override
  28. public AssetManager getAssets() {
  29. return mBase.getAssets();
  30. }
  31. @Override
  32. public Resources getResources() {
  33. return mBase.getResources();
  34. }
  35. @Override
  36. public ContentResolver getContentResolver() {
  37. return mBase.getContentResolver();
  38. }
  39. @Override
  40. public Looper getMainLooper() {
  41. return mBase.getMainLooper();
  42. }
  43. @Override
  44. public Context getApplicationContext() {
  45. return mBase.getApplicationContext();
  46. }
  47. @Override
  48. public String getPackageName() {
  49. return mBase.getPackageName();
  50. }
  51. @Override
  52. public void startActivity(Intent intent) {
  53. mBase.startActivity(intent);
  54. }
  55. @Override
  56. public void sendBroadcast(Intent intent) {
  57. mBase.sendBroadcast(intent);
  58. }
  59. @Override
  60. public Intent registerReceiver(
  61. BroadcastReceiver receiver, IntentFilter filter) {
  62. return mBase.registerReceiver(receiver, filter);
  63. }
  64. @Override
  65. public void unregisterReceiver(BroadcastReceiver receiver) {
  66. mBase.unregisterReceiver(receiver);
  67. }
  68. @Override
  69. public ComponentName startService(Intent service) {
  70. return mBase.startService(service);
  71. }
  72. @Override
  73. public boolean stopService(Intent name) {
  74. return mBase.stopService(name);
  75. }
  76. @Override
  77. public boolean bindService(Intent service, ServiceConnection conn,
  78. int flags) {
  79. return mBase.bindService(service, conn, flags);
  80. }
  81. @Override
  82. public void unbindService(ServiceConnection conn) {
  83. mBase.unbindService(conn);
  84. }
  85. @Override
  86. public Object getSystemService(String name) {
  87. return mBase.getSystemService(name);
  88. }
  89. ......
  90. }

由于ContextWrapper中的方法還是非常多的,我就進行了一些篩選,隻貼出來了部分方法。那麼上面的這些方法相信大家都是非常熟悉的,getResources()、getPackageName()、getSystemService()等等都是我們經常要用到的方法。那麼所有這些方法的實作又是什麼樣的呢?其實所有ContextWrapper中方法的實作都非常統一,就是調用了mBase對象中對應目前方法名的方法。

那麼這個mBase對象又是什麼呢?我們來看第16行的attachBaseContext()方法,這個方法中傳入了一個base參數,并把這個參數指派給了mBase對象。而attachBaseContext()方法其實是由系統來調用的,它會把ContextImpl對象作為參數傳遞到attachBaseContext()方法當中,進而指派給mBase對象,之後ContextWrapper中的所有方法其實都是通過這種委托的機制交由ContextImpl去具體實作的,是以說ContextImpl是上下文功能的實作類是非常準确的。

那麼另外再看一下我們剛剛列印的getBaseContext()方法,在第26行。這個方法隻有一行代碼,就是傳回了mBase對象而已,而mBase對象其實就是ContextImpl對象,是以剛才的列印結果也得到了印證。

使用Application的問題

雖說Application的用法确實非常簡單,但是我們平時的開發工作當中也着實存在着不少Application誤用的場景,那麼今天就來看一看有哪些比較容易犯錯的地方是我們應該注意的。

Application是Context的其中一種類型,那麼是否就意味着,隻要是Application的執行個體,就能随時使用Context的各種方法呢?我們來做個實驗試一下就知道了:

  1. public class MyApplication extends Application {
  2. public MyApplication() {
  3. String packageName = getPackageName();
  4. Log.d( "TAG", "package name is " + packageName);
  5. }
  6. }

這是一個非常簡單的自定義Application,我們在MyApplication的構造方法當中擷取了目前應用程式的包名,并列印出來。擷取包名使用了getPackageName()方法,這個方法就是由Context提供的。那麼上面的代碼能正常運作嗎?跑一下就知道了,你将會看到如下所示的結果:

Android Context完全解析,Context你用對了嗎

應用程式一啟動就立刻崩潰了,報的是一個空指針異常。看起來好像挺簡單的一段代碼,怎麼就會成空指針了呢?但是如果你嘗試把代碼改成下面的寫法,就會發現一切正常了:

  1. public class MyApplication extends Application {
  2. @Override
  3. public void onCreate() {
  4. super.onCreate();
  5. String packageName = getPackageName();
  6. Log.d( "TAG", "package name is " + packageName);
  7. }
  8. }

運作結果如下所示:

Android Context完全解析,Context你用對了嗎

在構造方法中調用Context的方法就會崩潰,在onCreate()方法中調用Context的方法就一切正常,那麼這兩個方法之間到底發生了什麼事情呢?我們重新回顧一下ContextWrapper類的源碼,ContextWrapper中有一個attachBaseContext()方法,這個方法會将傳入的一個Context參數指派給mBase對象,之後mBase對象就有值了。而我們又知道,所有Context的方法都是調用這個mBase對象的同名方法,那麼也就是說如果在mBase對象還沒指派的情況下就去調用Context中的任何一個方法時,就會出現空指針異常,上面的代碼就是這種情況。Application中方法的執行順序如下圖所示:

Android Context完全解析,Context你用對了嗎

Application中在onCreate()方法裡去初始化各種全局的變量資料是一種比較推薦的做法,但是如果你想把初始化的時間點提前到極緻,也可以去重寫attachBaseContext()方法,如下所示:

  1. public class MyApplication extends Application {
  2. @Override
  3. protected void attachBaseContext(Context base) {
  4. // 在這裡調用Context的方法會崩潰
  5. super.attachBaseContext(base);
  6. // 在這裡可以正常調用Context的方法
  7. }
  8. }

以上是我們平時在使用Application時需要注意的一個點,下面再來介紹另外一種非常普遍的Application誤用情況。

其實Android官方并不太推薦我們使用自定義的Application,基本上隻有需要做一些全局初始化的時候可能才需要用到自定義Application,官方文檔描述如下:

Android Context完全解析,Context你用對了嗎

但是就我的觀察而言,現在自定義Application的使用情況基本上可以達到100%了,也就是我們平時自己寫測試demo的時候可能不會使用,正式的項目幾乎全部都會使用自定義Application。可是使用歸使用,有不少項目對自定義Application的用法并不到位,正如官方文檔中所表述的一樣,多數項目隻是把自定義Application當成了一個通用工具類,而這個功能并不需要借助Application來實作,使用單例可能是一種更加标準的方式。

不過自定義Application也并沒有什麼副作用,它和單例模式二選一都可以實作同樣的功能,但是我見過有一些項目,會把自定義Application和單例模式混合到一起使用,這就讓人大跌眼鏡了。一個非常典型的例子如下所示:

  1. public class MyApplication extends Application {
  2. private static MyApplication app;
  3. public static MyApplication getInstance() {
  4. if (app == null) {
  5. app = new MyApplication();
  6. }
  7. return app;
  8. }
  9. }

就像單例模式一樣,這裡提供了一個getInstance()方法,用于擷取MyApplication的執行個體,有了這個執行個體之後,就可以調用MyApplication中的各種工具方法了。

但是這種寫法對嗎?這種寫法是大錯特錯!因為我們知道Application是屬于系統元件,系統元件的執行個體是要由系統來去建立的,如果這裡我們自己去new一個MyApplication的執行個體,它就隻是一個普通的Java對象而已,而不具備任何Context的能力。有很多人向我回報使用 LitePal 時發生了空指針錯誤其實都是由于這個原因,因為你提供給LitePal的隻是一個普通的Java對象,它無法通過這個對象來進行Context操作。

那麼如果真的想要提供一個擷取MyApplication執行個體的方法,比較标準的寫法又是什麼樣的呢?其實這裡我們隻需謹記一點,Application全局隻有一個,它本身就已經是單例了,無需再用單例模式去為它做多重執行個體保護了,代碼如下所示:

  1. public class MyApplication extends Application {
  2. private static MyApplication app;
  3. public static MyApplication getInstance() {
  4. return app;
  5. }
  6. @Override
  7. public void onCreate() {
  8. super.onCreate();
  9. app = this;
  10. }
  11. }

getInstance()方法可以照常提供,但是裡面不要做任何邏輯判斷,直接傳回app對象就可以了,而app對象又是什麼呢?在onCreate()方法中我們将app對象指派成this,this就是目前Application的執行個體,那麼app也就是目前Application的執行個體了。

好了,關于Context的介紹就到這裡吧,内容還是比較簡單易懂的,希望大家通過這篇文章可以了解Context更多的細節,并且不要去犯使用Context時的一些低級錯誤