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

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

獲取Context實例的一般方式

這再簡單不過了。

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

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

獲取Context實例的二般方式

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

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

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

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

有沒有什么辦法能讓FooApplication同時繼承BaseApplication和SdkApplication跋缤荨崇裁?(場面一度很尷尬拔稳,這里省略一萬字锹雏。)

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

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

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

回到最開始的話,App 運行的時候拱镐,肯定存在至少一個 Application 實例艘款。如果我們能夠在系統(tǒng)創(chuàng)建這個實例的時候哗咆,獲取這個實例的應用益眉,是不是就可以全局獲取 Context 實例了(因為這個實例是運行時一直存在的姥份,所以也就不用擔心靜態(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 類實例的香拉,是不是想辦法獲取到這個實例的應用就可以了中狂?不,還別高興太早盛险。我們可以把 Application 實例當做 Context 實例使用勋又,是因為它持有了一個 Context 實例(base),實際上 Application 實例都是通過代理調(diào)用這個 base 實例的接口完成相應的 Context 工作的鹤啡。在上面的代碼中挺邀,可以看到系統(tǒng)創(chuàng)建了 Application 實例 app 后跳座,通過app.attach(context)把 context 實例設置給了 app泣矛。直覺告訴我們,應該進一步關注這個 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)應用)才會走這個邏輯瓶盛,所以這里的代碼也不是我們要找的惩猫。不過,這里給我們一個提示拌阴,如果能想辦法獲取到 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 實例,而且不需要做任何初始化工作袭景,也不用擔心靜態(tài) Context 實例帶來的問題耸棒,測試代碼跑起來沒問題,接入項目后也沒有發(fā)現(xiàn)什么異常单山,我們簡直要上天了幅疼。不對,哪里不對悴晰。不科學,一般來說不可能這么順利的漂辐,這一定是錯覺棕硫。果然項目上線沒多久后立刻原地爆炸了,在一些機型上纬纪,通過Applications#context()獲取到的 Context 恒為 null滑肉。

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

原來是這么一個幺蛾子渐裂,在 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 提交一個任務去獲取 Context 實例慷彤,Worker 線程等待 UI 線程獲取完 Context 實例,再接著返回這個實例底哗。

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

vip視頻

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

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末野建,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子同眯,更是在濱河造成了極大的恐慌唯鸭,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,692評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件明肮,死亡現(xiàn)場離奇詭異,居然都是意外死亡循未,警方通過查閱死者的電腦和手機秫舌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評論 3 392
  • 文/潘曉璐 我一進店門足陨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來墨缘,“玉大人,你說我怎么就攤上這事镊讼『萏海” “怎么了?”我有些...
    開封第一講書人閱讀 162,995評論 0 353
  • 文/不壞的土叔 我叫張陵嫡良,是天一觀的道長献酗。 經(jīng)常有香客問我罕偎,道長,這世上最難降的妖魔是什么颜及? 我笑而不...
    開封第一講書人閱讀 58,223評論 1 292
  • 正文 為了忘掉前任俏站,我火速辦了婚禮,結果婚禮上墨林,老公的妹妹穿的比我還像新娘。我一直安慰自己酌呆,他們只是感情好搔耕,可當我...
    茶點故事閱讀 67,245評論 6 388
  • 文/花漫 我一把揭開白布度迂。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪而姐。 梳的紋絲不亂的頭發(fā)上拴念,一...
    開封第一講書人閱讀 51,208評論 1 299
  • 那天,我揣著相機與錄音风瘦,去河邊找鬼公般。 笑死,一個胖子當著我的面吹牛瞬雹,可吹牛的內(nèi)容都是我干的刽虹。 我是一名探鬼主播涌哲,決...
    沈念sama閱讀 40,091評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼哪廓!你這毒婦竟也來了稍刀?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,929評論 0 274
  • 序言:老撾萬榮一對情侶失蹤澳迫,失蹤者是張志新(化名)和其女友劉穎剧劝,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拢锹,經(jīng)...
    沈念sama閱讀 45,346評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡卒稳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,570評論 2 333
  • 正文 我和宋清朗相戀三年他巨,在試婚紗的時候發(fā)現(xiàn)自己被綠了染突。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,739評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡也榄,死狀恐怖司志,靈堂內(nèi)的尸體忽然破棺而出俐芯,到底是詐尸還是另有隱情,我是刑警寧澤吧史,帶...
    沈念sama閱讀 35,437評論 5 344
  • 正文 年R本政府宣布贸营,位于F島的核電站,受9級特大地震影響揣云,放射性物質(zhì)發(fā)生泄漏冰啃。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,037評論 3 326
  • 文/蒙蒙 一点弯、第九天 我趴在偏房一處隱蔽的房頂上張望矿咕。 院中可真熱鬧,春花似錦捡絮、人聲如沸莲镣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽区岗。三九已至,卻和暖如春慈缔,著一層夾襖步出監(jiān)牢的瞬間种玛,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評論 1 269
  • 我被黑心中介騙來泰國打工娱节, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留祭示,地道東北人质涛。 一個月前我還...
    沈念sama閱讀 47,760評論 2 369
  • 正文 我出身青樓,卻偏偏與公主長得像怒炸,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子毡代,可洞房花燭夜當晚...
    茶點故事閱讀 44,647評論 2 354

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