一種 Android 應(yīng)用內(nèi)全局獲取 Context 實例的裝置【轉(zhuǎn)】

App 運行的時候,肯定是存在至少一個 Application 實例的。同時,Context 我們再熟悉不過了,寫代碼的時候經(jīng)常需要使用到 Context 實例,它一般是通過構(gòu)造方法傳遞進來,通過方法的形式參數(shù)傳遞進來,或者是通過 attach 方法傳遞進我們需要用到的類。Context 實在是太重要了,以至于我經(jīng)常恨不得著藏著掖著,隨身帶著,這樣需要用到的時候就能立刻掏出來用用。但是換個角度想想,既然 App 運行的時候,Application 實例總是存在的,那么為何不設(shè)置一個全局可以訪問的靜態(tài)方法用于獲取 Context 實例,這樣以來就不需要上面那些繁瑣的傳遞方式。
說到這里,有的人可能說想這不是我們經(jīng)常干的好事嗎,有必要說的這么玄乎?少俠莫急,請聽吾輩徐徐道來。

獲取Context實例的一般方式

這再簡單不過了。

這種方式應(yīng)該是最常見的獲取 Context 實例的方式了,優(yōu)點就是嚴格按照代碼規(guī)范來,不用擔(dān)心兼容性問題;缺點就是 API 設(shè)計嚴重依賴于 Context 這個 API,如果早期接口設(shè)計不嚴謹,后期代碼重構(gòu)的時候可能很要命。此外還有一個比較有趣的問題,我們經(jīng)常使用 Activity 或者 Application 類的實例作為 Context 的實例使用,而前者本身又實現(xiàn)了別的接口,比如以下代碼。

這段代碼是我許久前看過的代碼,本身不是什么厲害的東西,不過這段代碼段我至今印象深刻。設(shè)想,如果 Foo 的接口設(shè)計可以不用依賴 Context,那么這里至少可以少一個this不是嗎。

獲取Context實例的二般方式

現(xiàn)在許多開發(fā)者喜歡設(shè)計一個全局可以訪問的靜態(tài)方法,這樣以來在設(shè)計 API 的時候,就不需要依賴 Context 了,代碼看起來像是這樣的。

這樣在整個項目中,都可以通過Foo#getContext()獲取 Context 實例了。不過目前看起來好像還有點小缺陷,就是使用前需要調(diào)用Foo#setContext(Context)方法進行注冊(這里暫不討論靜態(tài) Context 實例帶來的問題,這不是本篇幅的關(guān)注點)。好吧,以我的聰明才智,很快就想到了優(yōu)化方案。

不過這樣又有帶來了另一個問題,一般情況下,我們是把應(yīng)用的入口程序類FooApplication放在 App 模塊下的,這樣一來,Library 模塊里面代碼就訪問不到FooApplication#getContext()了。當然把FooApplication下移到基礎(chǔ)庫里面也是一種辦法,不過以我的聰明才智又立刻想到了個好點子。

這樣以來,就不用把FooApplication下移到基礎(chǔ)庫里面,Library 模塊里面的代碼也可以通過BaseApplication#getContext()訪問到 Context 實例了。嗯,這看起來似乎是一種神奇的膜法,因吹斯聽。然而,代碼寫完還沒來得及提交,包工頭打了個電話來和我說,由于項目接入了第三發(fā) SDK,需要把FooApplication繼承SdkApplication。

有沒有什么辦法能讓FooApplication同時繼承BaseApplication和SdkApplication啊?(場面一度很尷尬,這里省略一萬字。)

以上談到的,都是以前我們在獲取 Context 實例的時候遇到的一些麻煩:

  • 類 API 設(shè)計需要依賴 Context(這是一種好習(xí)慣,我可沒說這不好);
  • 持有靜態(tài)的 Context 實例容易引發(fā)的內(nèi)存泄露問題;
  • 需要提注冊 Context 實例(或者釋放);
  • 污染程序的 Application 類;
    那么,有沒有一種方式,能夠讓我們在整個項目中可以全局訪問到 Context 實例,不要提前注冊,不會污染 Application 類,更加不會引發(fā)靜態(tài) Context 實例帶來的內(nèi)存泄露呢?

一種全局獲取 Context 實例的方式

回到最開始的話,App 運行的時候,肯定存在至少一個 Application 實例。如果我們能夠在系統(tǒng)創(chuàng)建這個實例的時候,獲取這個實例的應(yīng)用,是不是就可以全局獲取 Context 實例了(因為這個實例是運行時一直存在的,所以也就不用擔(dān)心靜態(tài) Context 實例帶來的問題)。那么問題來了,Application 實例是什么時候創(chuàng)建的呢?首先先來看看我們經(jīng)常用來獲取 Base Context 實例的Application#attachBaseContext(Context)方法,它是繼承自ContextWrapper#attachBaseContext(Context)的。

是誰調(diào)用了這個方法呢?可以很快定位到Application#attach(Context)。

又是誰調(diào)用了Application#attach(Context)方法呢?一路下來可以直接定位到Instrumentation#newApplication(Class<?>, Context)方法里(這個方法名很好懂啊,一看就知道是干啥的)。

看來是在這里創(chuàng)建了 App 的入口 Application 類實例的,是不是想辦法獲取到這個實例的應(yīng)用就可以了?不,還別高興太早。我們可以把 Application 實例當做 Context 實例使用,是因為它持有了一個 Context 實例(base),實際上 Application 實例都是通過代理調(diào)用這個 base 實例的接口完成相應(yīng)的 Context 工作的。在上面的代碼中,可以看到系統(tǒng)創(chuàng)建了 Application 實例 app 后,通過app.attach(context)把 context 實例設(shè)置給了 app。直覺告訴我們,應(yīng)該進一步關(guān)注這個 context 實例是怎么創(chuàng)建的,可以定位到LoadedApk#makeApplication(boolean, Instrumentation)代碼段里。

好了,到這里我們定位到了 Application 實例和 Context 實例創(chuàng)建的位置,不過距離我們的目標只成功了一半。因為如果我們要想辦法獲取這些實例,就得先知道這些實例被保存在什么地方。上面的代碼一路逆向追蹤過來,好像也沒看見實例被保存給成員變量或者靜態(tài)變量,所以暫時還得繼續(xù)往上捋。很快就能捋到ActivityThread#performLaunchActivity(ActivityClientRecord, Intent)。

這里是我們啟動 Activity 的時候,Activity 實例創(chuàng)建的具體位置,以上代碼段還可以看到喜聞樂見的”Unable to start activity” 異常,你們猜猜這個異常是誰拋出來的?這里就不發(fā)散了,回到我們的問題來,以上代碼段獲取了一個 Application 實例,但是并沒有保持住,看起來這里的 Application 實例就像是一個臨時變量。沒辦法,再看看其他地方吧。接著找到ActivityThread#handleCreateService(CreateServiceData),不過這里也一樣,并沒有把獲取的 Application 實例保存起來,這樣我們就沒有辦法獲取到這個實例了。

我們可以看到,這里創(chuàng)建 Application 實例后,把實例保存在 ActivityThread 的成員變量mInitialApplication中。不過仔細一看,只有當system == true的時候(也就是系統(tǒng)應(yīng)用)才會走這個邏輯,所以這里的代碼也不是我們要找的。不過,這里給我們一個提示,如果能想辦法獲取到 ActivityThread 實例,或許就能直接拿到我們要的 Application 實例。此外,這里還把 ActivityThread 的實例賦值給一個靜態(tài)變量sCurrentActivityThread,靜態(tài)變量正是我們獲取系統(tǒng)隱藏 API 實例的切入點,所以如果我們能確定 ActivityThread 的mInitialApplication正是我們要找的 Application 實例的話,那就大功告成了。繼續(xù)查找到ActivityThread#handleBindApplication(AppBindData),光從名字我們就能猜出這個方法是干什么的,直覺告訴我們離目標不遠了~

我們看到這里同樣把 Application 實例保存在 ActivityThread 的成員變量mInitialApplication中,緊接著我們看看誰是調(diào)用了handleBindApplication方法,很快就能定位到ActivityThread.H#handleMessage(Message)里面。

Bingo!至此一切都清晰了,ActivityThread#mInitialApplication確實就是我們需要找的 Application 實例。整個流程捋順下來,系統(tǒng)創(chuàng)建 Base Context 實例、Application 實例,以及把 Base Context 實例 attach 到 Application 內(nèi)部的流程大致可以歸納為以下調(diào)用順序。

ActivityThread#bindApplication (異步) –> ActivityThread#handleBindApplication –> LoadedApk#makeApplication –> Instrumentation#newApplication –> Application#attach –> ContextWrapper#attachBaseContext

源碼擼完了,再回到我們一開始的需求來。現(xiàn)在我們要獲取 ActivityThread 的靜態(tài)成員變量 sCurrentActivityThread。閱讀源碼后我們發(fā)現(xiàn)可以通過ActivityThread#currentActivityThread()這個靜態(tài)方法來獲取這個靜態(tài)對象,然后通過ActivityThread#getApplication()方法就可能直接獲取我們需要的 Application 實例了。啊,這用反射搞起來簡直再簡單不過了!說搞就搞。

這樣以來, 無論在項目的什么地方,無論是在 App 模塊還是 Library 模塊,都可以通過Applications#context()獲取 Context 實例,而且不需要做任何初始化工作,也不用擔(dān)心靜態(tài) Context 實例帶來的問題,測試代碼跑起來沒問題,接入項目后也沒有發(fā)現(xiàn)什么異常,我們簡直要上天了。不對,哪里不對。不科學(xué),一般來說不可能這么順利的,這一定是錯覺。果然項目上線沒多久后立刻原地爆炸了,在一些機型上,通過Applications#context()獲取到的 Context 恒為 null。

通過測試發(fā)現(xiàn),在 4.1.1 系統(tǒng)的機型上,會穩(wěn)定出現(xiàn)獲取結(jié)果為 null 的現(xiàn)象,看來是系統(tǒng)源碼的實現(xiàn)上有一些出入導(dǎo)致,總之先看看源碼吧。

原來是這么一個幺蛾子,在 4.1.1 系統(tǒng)上,ActivityThread 是使用一個 ThreadLocal 實例來存放靜態(tài) ActivityThread 實例的。至于 ThreadLocal 是干什么用的這里暫不展開,簡單說來,就是系統(tǒng)只有在 UI 線程使用 sThreadLocal 來保存靜態(tài) ActivityThread 實例,所以我們只能在 UI 線程通過 sThreadLocal 獲取到這個保存的實例,在 Worker 線程 sThreadLocal 會直接返回空。

這樣以來解決方案也很明朗,只需要在事先現(xiàn)在 UI 線程觸發(fā)一次Applications#context()調(diào)用保存 Application 實例即可。不過項目的代碼一直在變化,我們很難保證不會有誰不小心觸發(fā)了一次優(yōu)先的 Worker 線程的調(diào)用,那就 GG 了,所以最好在Applications#context()方法里處理,我們只需要確保能在 Worker 線程獲得 ActivityThread 實例就 Okay 了。不過一時半會我想不出切確的辦法,也找不到適合的切入點,只做了下簡單的處理:如果是優(yōu)先在 Worker 線程調(diào)用,就先使用 UI 線程的 Handler 提交一個任務(wù)去獲取 Context 實例,Worker 線程等待 UI 線程獲取完 Context 實例,再接著返回這個實例。

最終完成的代碼可以參考 Applications

vip視頻

著作信息:本文章出自 Kaede 的博客,原文地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評論 6 535
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,744評論 3 421
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,935評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,325評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,534評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,084評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,892評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,322評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,800評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,084評論 2 375

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