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。