本文譯自《Context, What Context?》
注:文中提到的“導(dǎo)入布局”逗栽,即是指利用LayoutInflater來(lái)inflate layout的操作。
Context類對(duì)于做Android開發(fā)的同學(xué)肯定不陌生失暂,但或許許多同學(xué)都沒(méi)有正確地使用Context實(shí)例祭陷。
Context實(shí)例非常常見(jiàn),在許多的情境下(加載資源趣席、啟動(dòng)一個(gè)Activity、取得一個(gè)系統(tǒng)級(jí)的Service醇蝴、取得應(yīng)用獨(dú)有的文件存儲(chǔ)路徑還有創(chuàng)建View等)都需要用到一個(gè)Context實(shí)例宣肚,但如果不加區(qū)分地使用任意的Context實(shí)例,很容易會(huì)導(dǎo)致一些沒(méi)意料到的狀況發(fā)生悠栓。
Context的種類
并不是所有的Context實(shí)例都是一樣的構(gòu)造流程霉涨。常見(jiàn)的Context子類如下所列:
Application——在你的應(yīng)用進(jìn)程中單例存在的一個(gè)實(shí)例〔咽剩可以通過(guò)Activity或Service的getApplication()方法或者其他任意Context子類的getApplicationContext()方法來(lái)取得笙瑟。不論是在哪里以及何時(shí)取得的Application實(shí)例,它都是進(jìn)程唯一的癞志。
Activity/Service——繼承自ContextWrapper類往枷,它們實(shí)現(xiàn)了與Context類同樣的API,但代理了所有的方法到一個(gè)對(duì)外不可見(jiàn)的Context實(shí)例,也就是它們的base Context汤纸。每當(dāng)系統(tǒng)框架創(chuàng)建一個(gè)新的Activity或者Service實(shí)例時(shí)拙泽,它同時(shí)也會(huì)創(chuàng)建一個(gè)ContextImpl實(shí)例去執(zhí)行不同的組件所需要做的不同邏輯先煎。每個(gè)Activity或Service,以及它們相應(yīng)的base context描睦,都是實(shí)例唯一的。
BroadcastReceiver——這并不是一個(gè)Context子類导而。但每個(gè)Receiver都會(huì)實(shí)現(xiàn)onReceive(Context context, Intent intent)這個(gè)回調(diào)方法忱叭,每次系統(tǒng)發(fā)送通知都是調(diào)用到這個(gè)回調(diào)方法,這里就給Receiver傳入了一個(gè)Context實(shí)例今艺。這里傳入的Context實(shí)例又與其他的Context實(shí)例不一樣韵丑,這里傳入的Context實(shí)例是不能調(diào)用registerReceiver()方法和bindService()方法的。每次發(fā)送一個(gè)通知的時(shí)候洼滚,這里傳入的Context實(shí)例都是不一樣的埂息。
ContentProvider——這同樣也不是一個(gè)Context子類。但它內(nèi)部持有一個(gè)Context實(shí)例遥巴,這個(gè)實(shí)例可以通過(guò)getContext()方法取得千康。如果ContentProvider與調(diào)用者是運(yùn)行在同一個(gè)進(jìn)程中,那么它的getContext()方法返回的Context實(shí)例其實(shí)就是這個(gè)進(jìn)程里的始終單例的Application Context铲掐。不過(guò)如果ContentProvider與調(diào)用者是運(yùn)行在不同的進(jìn)程中的拾弃,如應(yīng)用A去調(diào)用應(yīng)用B的ContentProvider,那么這時(shí)候ContentProvider的getContext()方法返回的則是應(yīng)用B里的Application Context摆霉。
引用的保存
吶豪椿,我們先來(lái)說(shuō)說(shuō)非常常見(jiàn)的一種保存Context實(shí)例的引用從而導(dǎo)致內(nèi)存泄漏的情形:一個(gè)實(shí)例或一個(gè)類,它保存了一個(gè)生命周期比自己短的Context實(shí)例携栋,這就會(huì)導(dǎo)致內(nèi)存泄漏搭盾。舉個(gè)例子,創(chuàng)建一個(gè)需要依賴一個(gè)Context實(shí)例的單例類來(lái)進(jìn)行一些通用操作如加載資源婉支、調(diào)用一個(gè)ContentProvider鸯隅,并把當(dāng)前Activity或者Service作為它依賴的Context實(shí)例設(shè)置進(jìn)去。
錯(cuò)誤單例的示范
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;
}
}
這段代碼最大的問(wèn)題是我們并不知道傳入的Context參數(shù)是啥Context向挖,所以對(duì)于我們這個(gè)單例來(lái)說(shuō)直接保存這個(gè)Context的引用是很危險(xiǎn)的(例如這里的Context是一個(gè)Activity或者Service的時(shí)候)蝌以。因?yàn)閱卫锩娴膶?duì)象是靜態(tài)的,這就會(huì)導(dǎo)致它引用的所有資源都不會(huì)被系統(tǒng)GC回收掉何之,假設(shè)這里的Context是一個(gè)Activity的話跟畅,我們這樣做就會(huì)導(dǎo)致這個(gè)Activity相關(guān)的View啊還有別的占內(nèi)存的對(duì)象一直不能被系統(tǒng)回收掉,進(jìn)而導(dǎo)致了內(nèi)存泄漏溶推。
為了避免這種情況徊件,我們?cè)谙旅娴膯卫懈臑槭冀K是保存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;
}
}
這樣我們就不用關(guān)心傳入的Context到底是什么了庇忌,因?yàn)槲覀儸F(xiàn)在持有的引用是Application Context舞箍。就像前文提到的,Application Context是在整個(gè)應(yīng)用程序中進(jìn)程單例的皆疹,所以哪怕我們?cè)诖a中對(duì)它持有靜態(tài)引用也不會(huì)導(dǎo)致什么內(nèi)存泄漏疏橄。
那,為什么我們不能總是使用Application Context來(lái)完成各處需要Context的邏輯呢略就?這樣不就可以永不擔(dān)心Context相關(guān)的內(nèi)存泄漏了嗎捎迫?原因其實(shí)很簡(jiǎn)單,就像我在一開頭就提到的——一個(gè)Context實(shí)例并不一定能與另一個(gè)Context實(shí)例等同表牢。
不同種類的Context的能力區(qū)別
直接參考下表即可:
|Application | Activity | Service | ContentProvider | BroadcastReceiver
---|---|---|---|---|---
構(gòu)造展示一個(gè)Dialog | NO | YES | NO | NO | NO
啟動(dòng)一個(gè)Activity | NO1 | YES | NO1 | NO1 | NO1
導(dǎo)入布局文件 | NO2 | YES | NO2 | NO2 | NO2
啟動(dòng)一個(gè)Service | YES | YES | YES | YES | YES
綁定到一個(gè)Service | YES | YES | YES | YES | NO
發(fā)送一個(gè)廣播 | YES | YES | YES | YES | YES
注冊(cè)一個(gè)BroadcastReceiver | YES | YES | YES | YES | NO3
加載資源數(shù)值 | YES | YES | YES | YES | YES
附注:
- 一個(gè)非Activity的Context可以用于啟動(dòng)一個(gè)Activity窄绒,但這樣啟動(dòng)的Activity需要新創(chuàng)建一個(gè)Activity堆疊棧。這個(gè)在某些特定情形下或許會(huì)適用崔兴,但這種設(shè)計(jì)一般來(lái)說(shuō)都不太好彰导。
- 這個(gè)其實(shí)也是可以的,但是這樣導(dǎo)入的布局會(huì)用當(dāng)前系統(tǒng)的默認(rèn)主題來(lái)設(shè)置敲茄,而不是用你在你的應(yīng)用程序中設(shè)定的主題來(lái)設(shè)置的位谋。
- 在Android 4.2及以上的系統(tǒng)里,如果receiver是null堰燎,那這也是可以的掏父。這樣做是為了取得一個(gè)嚴(yán)格廣播的當(dāng)前值。
用戶交互界面
從上表可以看出好些操作不適合使用Application Context來(lái)執(zhí)行秆剪,而這些操作無(wú)一例外地全都是和用戶交互界面直接相關(guān)的赊淑。適合執(zhí)行這些與用戶交互界面直接相關(guān)的操作的Context只有一種,那就是Activity仅讽;其他的Context其實(shí)和Application Context的功能都差不多陶缺。
不過(guò)其實(shí)這些個(gè)與UI相關(guān)的操作其實(shí)大多數(shù)時(shí)候都是在Activity中才會(huì)有執(zhí)行的機(jī)會(huì)。假設(shè)使用一個(gè)非Activity的Context來(lái)調(diào)用展示一個(gè)Dialog洁灵,在調(diào)用Dialog實(shí)例的show()方法時(shí)就會(huì)報(bào)以下的錯(cuò)誤直接崩潰:
Caused by: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application
又或者使用一個(gè)非Activity的Context來(lái)啟動(dòng)另一個(gè)Activity饱岸,同樣也會(huì)報(bào)錯(cuò)崩潰:
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?
但如果是使用一個(gè)非Activity的Context來(lái)導(dǎo)入布局,應(yīng)用并不會(huì)報(bào)錯(cuò)崩潰处渣。詳細(xì)的流程可以參見(jiàn)我之前寫的《布局的導(dǎo)入》。此時(shí)蛛砰,Android框架會(huì)默默地返回你需要的布局文件對(duì)應(yīng)的View罐栈,其中的各個(gè)View的層次關(guān)系都是正確正常的,只是你在應(yīng)用程序中設(shè)定的主題和樣式(在AndroidManifest.xml中設(shè)定的值)不會(huì)被應(yīng)用到此時(shí)導(dǎo)入布局文件而產(chǎn)生的View中去泥畅,而是應(yīng)用了系統(tǒng)默認(rèn)的主題荠诬。這是因?yàn)樵贛anifest中定義的主題實(shí)際上是僅僅綁定到Activity這種Context上的,所以如果使用非Activity的Context實(shí)例來(lái)導(dǎo)入布局,那就只會(huì)應(yīng)用系統(tǒng)默認(rèn)的主題柑贞,從而導(dǎo)入了一個(gè)可能并不是你所期望的布局樣式方椎。
但上述規(guī)則是不是有不完善的地方?
有些同學(xué)在開發(fā)的時(shí)候會(huì)發(fā)現(xiàn)钧嘶,依照目前的程序設(shè)計(jì)棠众,我們的程序就是要長(zhǎng)時(shí)間的持有一個(gè)Context實(shí)例,而且這個(gè)實(shí)例還必須是Activity有决,因?yàn)樵谶@長(zhǎng)時(shí)間的持有過(guò)程中闸拿,會(huì)涉及到UI相關(guān)的操作邏輯。那么假設(shè)真的有這種情況书幕,我強(qiáng)烈建議你們重新審視你們的程序的設(shè)計(jì)新荤,因?yàn)檫@種情形完全就是在對(duì)抗Android系統(tǒng)框架。
經(jīng)驗(yàn)總結(jié)
在大多數(shù)情形下台汇,代碼是跑在哪類Context內(nèi)就使用當(dāng)前可獲得的這類Context即可苛骨。只要這個(gè)Context類引用并不會(huì)超脫出它所引用的組件的生命周期,那你完全可以在你的邏輯代碼中持有這個(gè)引用苟呐。但是如果你需要長(zhǎng)時(shí)持有一個(gè)Context引用痒芝,這個(gè)引用甚至?xí)撃愕腁ctivity或Service的生命周期,哪怕僅僅是短暫地超脫出生命周期掠抬,也務(wù)必要把這個(gè)Context引用改為Application引用吼野。
譯者說(shuō)兩句
這段時(shí)間斷更了抱歉。
這篇文章雖然是2013年的老博文了两波,但在我看來(lái)還是非常有學(xué)習(xí)價(jià)值的瞳步。這是我第一次翻譯技術(shù)類文章,所以可能表述得不太好腰奋,我日后會(huì)繼續(xù)努力提升翻譯水平的单起。
依文中所說(shuō),在需要Context的時(shí)候劣坊,直接取能取到的“最近”的Context實(shí)例即可嘀倒,一般情形下是不會(huì)導(dǎo)致內(nèi)存泄漏的。舉個(gè)例子局冰,在一個(gè)Activity A里有個(gè)Fragment a测蘑,然后Fragment a里面有Adapter View,那這時(shí)候就需要透?jìng)鰿ontext實(shí)例來(lái)構(gòu)造Adapter View里面的Item View了康二,那這時(shí)候碳胳,其實(shí)大膽地在a里面透?jìng)鰽的引用到Adapter中其實(shí)是沒(méi)有問(wèn)題的,只要不要把持有的A的引用聲明為靜態(tài)就好沫勿。
再比如挨约,在后臺(tái)有個(gè)定時(shí)任務(wù)或者什么的味混,在特定時(shí)機(jī)要往SharedPreferences里面寫數(shù)據(jù)啊或者要讀取資源文件中的string字符串啥的,這時(shí)候就可以在定時(shí)任務(wù)的代碼中長(zhǎng)期持有一個(gè)Application Context的引用來(lái)執(zhí)行相關(guān)的操作诫惭,這樣也是不會(huì)引發(fā)內(nèi)存泄漏的翁锡。