到底什么是Context?
Context類對于做Android開發的同學肯定不陌生,但或許許多同學都沒有正確地使用Context實例。
Context實例非常常見,在許多的情境下(加載資源、啟動一個Activity、取得一個系統級的Service、取得應用獨有的文件存儲路徑還有創建View等)都需要用到一個Context實例,但如果不加區分地使用任意的Context實例,很容易會導致一些沒意料到的狀況發生。
Context的種類
并不是所有的Context實例都是一樣的構造流程。常見的Context子類如下所列:
-
Application——在你的應用進程中單例存在的一個實例。可以通過Activity或Service的getApplication()方法或者其他任意Context子類的getApplicationContext()方法來取得。不論是在哪里以及何時取得的Application實例,它都是進程唯一的。
-
Activity/Service——繼承自ContextWrapper類,它們實現了與Context類同樣的API,但代理了所有的方法到一個對外不可見的Context實例,也就是它們的base Context。每當系統框架創建一個新的Activity或者Service實例時,它同時也會創建一個ContextImpl實例去執行不同的組件所需要做的不同邏輯。每個Activity或Service,以及它們相應的base context,都是實例唯一的。
-
BroadcastReceiver——這并不是一個Context子類。但每個Receiver都會實現onReceive(Context context, Intent intent)這個回調方法,每次系統發送通知都是調用到這個回調方法,這里就給Receiver傳入了一個Context實例。這里傳入的Context實例又與其他的Context實例不一樣,這里傳入的Context實例是不能調用registerReceiver()方法和bindService()方法的。每次發送一個通知的時候,這里傳入的Context實例都是不一樣的。
-
ContentProvider——這同樣也不是一個Context子類。但它內部持有一個Context實例,這個實例可以通過getContext()方法取得。如果ContentProvider與調用者是運行在同一個進程中,那么它的getContext()方法返回的Context實例其實就是這個進程里的始終單例的Application Context。不過如果ContentProvider與調用者是運行在不同的進程中的,如應用A去調用應用B的ContentProvider,那么這時候ContentProvider的getContext()方法返回的則是應用B里的Application Context。
引用的保存
吶,我們先來說說非常常見的一種保存Context實例的引用從而導致內存泄漏的情形:一個實例或一個類,它保存了一個生命周期比自己短的Context實例,這就會導致內存泄漏。舉個例子,創建一個需要依賴一個Context實例的單例類來進行一些通用操作如加載資源、調用一個ContentProvider,并把當前Activity或者Service作為它依賴的Context實例設置進去。
錯誤單例的示范
public class CustomManager {
private static CustomManager sInstance;
public static CustomManager getInstance(Context context) {
if (sInstance == null) {
sInstance = new CustomManager(context);
}
return sInstance;
}
private Context mContext;
private CustomManager(Context context) {
mContext = context;
}
}</code></pre>
這段代碼最大的問題是我們并不知道傳入的Context參數是啥Context,所以對于我們這個單例來說直接保存這個Context的引用是很危險的(例如這里的Context是一個Activity或者Service的時候)。因為單例里面的對象是靜態的,這就會導致它引用的所有資源都不會被系統GC回收掉,假設這里的Context是一個Activity的話,我們這樣做就會導致這個Activity相關的View啊還有別的占內存的對象一直不能被系統回收掉,進而導致了內存泄漏。
為了避免這種情況,我們在下面的單例中改為始終是保存Application Context的引用。
正確單例的示范
public class CustomManager {
private static CustomManager sInstance;
public static CustomManager getInstance(Context context) {
if (sInstance == null) {
//不管什么Context,都改為取Application Context
sInstance = new CustomManager(context.getApplicationContext());
}
return sInstance;
}
private Context mContext;
private CustomManager(Context context) {
mContext = context;
}
}</code></pre>
這樣我們就不用關心傳入的Context到底是什么了,因為我們現在持有的引用是Application Context。就像前文提到的,Application Context是在整個應用程序中進程單例的,所以哪怕我們在代碼中對它持有靜態引用也不會導致什么內存泄漏。
那,為什么我們不能 總是 使用Application Context來完成各處需要Context的邏輯呢?這樣不就可以永不擔心Context相關的內存泄漏了嗎?原因其實很簡單,就像我在一開頭就提到的——一個Context實例并不一定能與另一個Context實例等同。
不同種類的Context的能力區別
直接參考下表即可:
Application
Activity
Service
ContentProvider
BroadcastReceiver
構造展示一個Dialog
NO
YES
NO
NO
NO
啟動一個Activity
NO 1
YES
NO 1
NO 1
NO 1
導入布局文件
NO 2
YES
NO 2
NO 2
NO 2
啟動一個Service
YES
YES
YES
YES
YES
綁定到一個Service
YES
YES
YES
YES
NO
發送一個廣播
YES
YES
YES
YES
YES
注冊一個BroadcastReceiver
YES
YES
YES
YES
NO 3
加載資源數值
YES
YES
YES
YES
YES
附注:
- 一個非Activity的Context可以用于啟動一個Activity,但這樣啟動的Activity需要新創建一個Activity堆疊棧。這個在某些特定情形下或許會適用,但這種設計一般來說都不太好。
- 這個其實也是可以的,但是這樣導入的布局會用當前系統的默認主題來設置,而不是用你在你的應用程序中設定的主題來設置的。
- 在Android 4.2及以上的系統里,如果receiver是null,那這也是可以的。這樣做是為了取得一個嚴格廣播的當前值。
用戶交互界面
從上表可以看出好些操作不適合使用Application Context來執行,而這些操作無一例外地全都是和用戶交互界面直接相關的。適合執行這些與用戶交互界面直接相關的操作的Context只有一種,那就是Activity;其他的Context其實和Application Context的功能都差不多。
不過其實這些個與UI相關的操作其實大多數時候都是在Activity中才會有執行的機會。假設使用一個非Activity的Context來調用展示一個Dialog,在調用Dialog實例的show()方法時就會報以下的錯誤直接崩潰:
Caused by: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application
又或者使用一個非Activity的Context來啟動另一個Activity,同樣也會報錯崩潰:
Caused by: android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?
但如果是使用一個非Activity的Context來導入布局,應用并不會報錯崩潰。詳細的流程可以參見我之前寫的 《布局的導入》 。此時,Android框架會默默地返回你需要的布局文件對應的View,其中的各個View的層次關系都是正確正常的,只是你在應用程序中設定的主題和樣式(在AndroidManifest.xml中設定的值)不會被應用到此時導入布局文件而產生的View中去,而是應用了系統默認的主題。這是因為在Manifest中定義的主題實際上是僅僅綁定到Activity這種Context上的,所以如果使用非Activity的Context實例來導入布局,那就只會應用系統默認的主題,從而導入了一個可能并不是你所期望的布局樣式。
但上述規則是不是有不完善的地方?
有些同學在開發的時候會發現,依照目前的程序設計,我們的程序就是要長時間的持有一個Context實例,而且這個實例還必須是Activity,因為在這長時間的持有過程中,會涉及到UI相關的操作邏輯。那么假設真的有這種情況,我強烈建議你們重新審視你們的程序的設計,因為這種情形完全就是在 對抗Android系統框架 。
經驗總結
在大多數情形下,代碼是跑在哪類Context內就使用當前可獲得的這類Context即可。只要這個Context類引用并不會超脫出它所引用的組件的生命周期,那你完全可以在你的邏輯代碼中持有這個引用。但是如果你需要長時持有一個Context引用,這個引用甚至會超脫你的Activity或Service的生命周期,哪怕僅僅是短暫地超脫出生命周期,也務必要把這個Context引用改為Application引用。
譯者說兩句
這段時間斷更了抱歉。
這篇文章雖然是2013年的老博文了,但在我看來還是非常有學習價值的。這是我第一次翻譯技術類文章,所以可能表述得不太好,我日后會繼續努力提升翻譯水平的。
依文中所說,在需要Context的時候,直接取能取到的“最近”的Context實例即可,一般情形下是不會導致內存泄漏的。舉個例子,在一個Activity A里有個Fragment a,然后Fragment a里面有Adapter View,那這時候就需要透傳Context實例來構造Adapter View里面的Item View了,那這時候,其實大膽地在a里面透傳A的引用到Adapter中其實是沒有問題的, 只要不要把持有的A的引用聲明為靜態就好 。
再比如,在后臺有個定時任務或者什么的,在特定時機要往SharedPreferences里面寫數據啊或者要讀取資源文件中的string字符串啥的,這時候就可以在定時任務的代碼中長期持有一個Application Context的引用來執行相關的操作,這樣也是不會引發內存泄漏的。
來自:http://www.jianshu.com/p/f7b6611df773