譯文《Context,到底什么是Context钦无?》

本文譯自《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
附注:

  1. 一個(gè)非Activity的Context可以用于啟動(dòng)一個(gè)Activity窄绒,但這樣啟動(dòng)的Activity需要新創(chuàng)建一個(gè)Activity堆疊棧。這個(gè)在某些特定情形下或許會(huì)適用崔兴,但這種設(shè)計(jì)一般來(lái)說(shuō)都不太好彰导。
  2. 這個(gè)其實(shí)也是可以的,但是這樣導(dǎo)入的布局會(huì)用當(dāng)前系統(tǒng)的默認(rèn)主題來(lái)設(shè)置敲茄,而不是用你在你的應(yīng)用程序中設(shè)定的主題來(lái)設(shè)置的位谋。
  3. 在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)存泄漏的翁锡。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市夕土,隨后出現(xiàn)的幾起案子馆衔,更是在濱河造成了極大的恐慌,老刑警劉巖隘弊,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件哈踱,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡梨熙,警方通過(guò)查閱死者的電腦和手機(jī)开镣,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)咽扇,“玉大人邪财,你說(shuō)我怎么就攤上這事≈视” “怎么了树埠?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)嘶伟。 經(jīng)常有香客問(wèn)我怎憋,道長(zhǎng),這世上最難降的妖魔是什么九昧? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任绊袋,我火速辦了婚禮,結(jié)果婚禮上铸鹰,老公的妹妹穿的比我還像新娘癌别。我一直安慰自己,他們只是感情好蹋笼,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布展姐。 她就那樣靜靜地躺著,像睡著了一般剖毯。 火紅的嫁衣襯著肌膚如雪圾笨。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天逊谋,我揣著相機(jī)與錄音擂达,去河邊找鬼。 笑死涣狗,一個(gè)胖子當(dāng)著我的面吹牛谍婉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播镀钓,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼穗熬,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了丁溅?” 一聲冷哼從身側(cè)響起唤蔗,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎窟赏,沒(méi)想到半個(gè)月后妓柜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡涯穷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年棍掐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拷况。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡作煌,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出赚瘦,到底是詐尸還是另有隱情粟誓,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布起意,位于F島的核電站鹰服,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏揽咕。R本人自食惡果不足惜悲酷,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望心褐。 院中可真熱鬧舔涎,春花似錦、人聲如沸逗爹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)掘而。三九已至挟冠,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間袍睡,已是汗流浹背知染。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留斑胜,地道東北人控淡。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓嫌吠,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親掺炭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子辫诅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容