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跋选。