一種 Android 應用內全域性獲取 Context 例項的裝置
哥白尼 · 羅斯福 · 馬丁路德 · 李開復 · 嫁衣曾經說過
Where there is an Android App, there is an Application context.
沒毛病,扎心了。App 執行的時候,肯定是存在至少一個 Application 例項的。同時,Context 我們再熟悉不過了,寫程式碼的時候經常需要使用到 Context 例項,它一般是通過構造方法傳遞進來,通過方法的形式引數傳遞進來,或者是通過 attach 方法傳遞進我們需要用到的類。Context 實在是太重要了,以至於我經常恨不得著藏著掖著,隨身帶著,這樣需要用到的時候就能立刻掏出來用用。但是換個角度想想,既然 App 執行的時候,Application 例項總是存在的,那麼為何不設定一個全域性可以訪問的靜態方法用於獲取 Context 例項,這樣以來就不需要上面那些繁瑣的傳遞方式。
說到這裡,有的人可能說想這不是我們經常乾的好事嗎,有必要說的這麼玄乎?少俠莫急,請聽吾輩徐徐道來。
獲取 Context 例項的一般方式
這再簡單不過了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public static class Foo1 { public Foo1(Context context) { // 1. 在構造方法帶入 } } public static class Foo2 { public Foo2 attach(Context context) { // 2. 通過attach方法帶入 |
這種方式應該是最常見的獲取 Context 例項的方式了,優點就是嚴格按照程式碼規範來,不用擔心相容性問題;缺點就是 API 設計嚴重依賴於 Context 這個 API,如果早期介面設計不嚴謹,後期程式碼重構的時候可能很要命。此外還有一個比較有趣的問題,我們經常使用 Activity 或者 Application 類的例項作為 Context 的例項使用,而前者本身又實現了別的介面,比如以下程式碼。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public static class FooActivity extends Activity implements FooA, FooB, FooC { Foo mFoo; public void onCreate(Bundle bundle) { // 禁忌·四重存在! mFoo.foo(this, this, this, this); } ... } public static class Foo { public void foo(Context context, FooA a, FooB b, FooC c) { ... } } |
這段程式碼是我許久前看過的程式碼,本身不是什麼厲害的東西,不過這段程式碼段我至今印象深刻。設想,如果 Foo 的介面設計可以不用依賴 Context,那麼這裡至少可以少一個this
不是嗎。
獲取 Context 例項的二般方式
現在許多開發者喜歡設計一個全域性可以訪問的靜態方法,這樣以來在設計 API 的時候,就不需要依賴 Context 了,程式碼看起來像是這樣的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | /* * 全域性獲取Context例項的靜態方法。 */ public static class Foo { private static sContext; public static Context getContext() { return sContext; } public static void setContext(Context context) { sContext = context; } } |
這樣在整個專案中,都可以通過Foo#getContext()
獲取 Context 例項了。不過目前看起來好像還有點小缺陷,就是使用前需要呼叫Foo#setContext(Context)
方法進行註冊(這裡暫不討論靜態 Context 例項帶來的問題,這不是本篇幅的關注點)。好吧,以我的聰明才智,很快就想到了優化方案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | /* * 全域性獲取Context例項的靜態方法(改進版)。 */ public static class FooApplication extends Application { private static sContext; public FooApplication() { sContext = this; } public static Context getContext() { return sContext; } } |
不過這樣又有帶來了另一個問題,一般情況下,我們是把應用的入口程式類FooApplication
放在 App 模組下的,這樣一來,Library 模組裡面程式碼就訪問不到FooApplication#getContext()
了。當然把FooApplication
下移到基礎庫裡面也是一種辦法,不過以我的聰明才智又立刻想到了個好點子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | /* * 全域性獲取Context例項的靜態方法(改進版之再改進)。 */ public static class FooApplication extends BaseApplication { ... } /* * 基礎庫裡面 */ public static class BaseApplication extends Application { private static sContext; public BaseApplication() { sContext = this; } public static Context getContext() { return sContext; } } |
這樣以來,就不用把FooApplication
下移到基礎庫裡面,Library 模組裡面的程式碼也可以通過BaseApplication#getContext()
訪問到 Context 例項了。嗯,這看起來似乎是一種神奇的膜法,因吹斯聽。然而,程式碼寫完還沒來得及提交,包工頭打了個電話來和我說,由於專案接入了第三發 SDK,需要把FooApplication
繼承SdkApplication
。
…… 有沒有什麼辦法能讓FooApplication
同時繼承BaseApplication
和SdkApplication
啊?(場面一度很尷尬,這裡省略一萬字。)
以上談到的,都是以前我們在獲取 Context 例項的時候遇到的一些麻煩:
- 類 API 設計需要依賴 Context(這是一種好習慣,我可沒說這不好);
- 持有靜態的 Context 例項容易引發的記憶體洩露問題;
- 需要提註冊 Context 例項(或者釋放);
- 汙染程式的 Application 類;
那麼,有沒有一種方式,能夠讓我們在整個專案中可以全域性訪問到 Context 例項,不要提前註冊,不會汙染 Application 類,更加不會引發靜態 Context 例項帶來的記憶體洩露呢?
一種全域性獲取 Context 例項的方式
回到最開始的話,App 執行的時候,肯定存在至少一個 Application 例項。如果我們能夠在系統建立這個例項的時候,獲取這個例項的應用,是不是就可以全域性獲取 Context 例項了(因為這個例項是執行時一直存在的,所以也就不用擔心靜態 Context 例項帶來的問題)。那麼問題來了,Application 例項是什麼時候建立的呢?首先先來看看我們經常用來獲取 Base Context 例項的Application#attachBaseContext(Context)
方法,它是繼承自ContextWrapper#attachBaseContext(Context)
的。
1 2 3 4 5 6 7 8 9 | public class ContextWrapper extends Context { protected void attachBaseContext(Context base) { if (mBase != null) { throw new IllegalStateException("Base context already set"); } mBase = base; } } |
是誰呼叫了這個方法呢?可以很快定位到Application#attach(Context)
。
1 2 3 4 5 6 | public class Application extends ContextWrapper { final void attach(Context context) { attachBaseContext(context); mLoadedApk = ContextImpl.getImpl(context).mPackageInfo; } } |
又是誰呼叫了Application#attach(Context)
方法呢?一路下來可以直接定位到Instrumentation#newApplication(Class<?>, Context)
方法裡(這個方法名很好懂啊,一看就知道是幹啥的)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | /** * Base class for implementing application instrumentation code. When running * with instrumentation turned on, this class will be instantiated for you * before any of the application code, allowing you to monitor all of the * interaction the system has with the application. An Instrumentation * implementation is described to the system through an AndroidManifest.xml's * <instrumentation>. */ public class Instrumentation { static public Application newApplication(Class<?> clazz, Context context) throws InstantiationException, IllegalAccessException, ClassNotFoundException { Application app = (Application)clazz.newInstance(); app.attach(context); return app; } } |
看來是在這裡建立了 App 的入口 Application 類例項的,是不是想辦法獲取到這個例項的應用就可以了?不,還別高興太早。我們可以把 Application 例項當做 Context 例項使用,是因為它持有了一個 Context 例項(base),實際上 Application 例項都是通過代理呼叫這個 base 例項的介面完成相應的 Context 工作的。在上面的程式碼中,可以看到系統建立了 Application 例項 app 後,通過app.attach(context)
把 context 例項設定給了 app。直覺告訴我們,應該進一步關注這個 context 例項是怎麼建立的,可以定位到LoadedApk#makeApplication(boolean, Instrumentation)
程式碼段裡。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | /** * Local state maintained about a currently loaded .apk. * @hide */ public final class LoadedApk { public Application makeApplication(boolean forceDefaultAppClass, Instrumentation instrumentation) { if (mApplication != null) { return mApplication; } Application app = null; String appClass = mApplicationInfo.className; if (forceDefaultAppClass || (appClass == null)) { appClass = "android.app.Application"; } try { java.lang.ClassLoader cl = getClassLoader(); if (!mPackageName.equals("android")) { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "initializeJavaContextClassLoader"); initializeJavaContextClassLoader(); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); } // Context 例項建立的地方,可以看出Context例項是一個ContextImpl。 ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this); app = mActivityThread.mInstrumentation.newApplication( cl, appClass, appContext); appContext.setOuterContext(app); } catch (Exception e) { } ... return app; } } |
好了,到這裡我們定位到了 Application 例項和 Context 例項建立的位置,不過距離我們的目標只成功了一半。因為如果我們要想辦法獲取這些例項,就得先知道這些例項被儲存在什麼地方。上面的程式碼一路逆向追蹤過來,好像也沒看見例項被儲存給成員變數或者靜態變數,所以暫時還得繼續往上捋。很快就能捋到ActivityThread#performLaunchActivity(ActivityClientRecord, Intent)
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | /** * This manages the execution of the main thread in an * application process, scheduling and executing activities, * broadcasts, and other operations on it as the activity * manager requests. */ public final class ActivityThread { private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { ... ActivityInfo aInfo = r.activityInfo; ComponentName component = r.intent.getComponent(); Activity activity = null; try { java.lang.ClassLoader cl = r.packageInfo.getClassLoader(); activity = mInstrumentation.newActivity( cl, component.getClassName(), r.intent); StrictMode.incrementExpectedActivityCount(activity.getClass()); r.intent.setExtrasClassLoader(cl); r.intent.prepareToEnterProcess(); if (r.state != null) { r.state.setClassLoader(cl); } } catch (Exception e) { if (!mInstrumentation.onException(activity, e)) { throw new RuntimeException( "Unable to instantiate activity " + component + ": " + e.toString(), e); } } try { // 建立Application例項。 Application app = r.packageInfo.makeApplication(false, mInstrumentation); if (activity != null) { ... } r.paused = true; mActivities.put(r.token, r); } catch (Exception e) { if (!mInstrumentation.onException(activity, e)) { throw new RuntimeException( "Unable to start activity " + component + ": " + e.toString(), e); } } return activity; } } |
這裡是我們啟動 Activity 的時候,Activity 例項建立的具體位置,以上程式碼段還可以看到喜聞樂見的”Unable to start activity” 異常,你們猜猜這個異常是誰丟擲來的?這裡就不發散了,回到我們的問題來,以上程式碼段獲取了一個 Application 例項,但是並沒有保持住,看起來這裡的 Application 例項就像是一個臨時變數。沒辦法,再看看其他地方吧。接著找到ActivityThread#handleCreateService(CreateServiceData)
,不過這裡也一樣,並沒有把獲取的 Application 例項儲存起來,這樣我們就沒有辦法獲取到這個例項了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | public final class ActivityThread { private void attach(boolean system) { sCurrentActivityThread = this; mSystemThread = system; if (!system) { ... } else { // Don't set application object here -- if the system crashes, // we can't display an alert, we just want to die die die. android.ddm.DdmHandleAppName.setAppName("system_process", UserHandle.myUserId()); try { mInstrumentation = new Instrumentation(); ContextImpl context = ContextImpl.createAppContext( this, getSystemContext().mPackageInfo); mInitialApplication = context.mPackageInfo.makeApplication(true, null); mInitialApplication.onCreate(); } catch (Exception e) { throw new RuntimeException( "Unable to instantiate Application():" + e.toString(), e); } } ... } public static ActivityThread systemMain() { ... ActivityThread thread = new ActivityThread(); thread.attach(true); return thread; } public static void main(String[] args) { ... ActivityThread thread = new ActivityThread(); thread.attach(false); ... } } |
我們可以看到,這裡建立 Application 例項後,把例項儲存在 ActivityThread 的成員變數mInitialApplication
中。不過仔細一看,只有當system == true
的時候(也就是系統應用)才會走這個邏輯,所以這裡的程式碼也不是我們要找的。不過,這裡給我們一個提示,如果能想辦法獲取到 ActivityThread 例項,或許就能直接拿到我們要的 Application 例項。此外,這裡還把 ActivityThread 的例項賦值給一個靜態變數sCurrentActivityThread
,靜態變數正是我們獲取系統隱藏 API 例項的切入點,所以如果我們能確定 ActivityThread 的mInitialApplication
正是我們要找的 Application 例項的話,那就大功告成了。繼續查詢到ActivityThread#handleBindApplication(AppBindData)
,光從名字我們就能猜出這個方法是幹什麼的,直覺告訴我們離目標不遠了~
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | public final class ActivityThread { private void handleBindApplication(AppBindData data) { ... try { Application app = data.info.makeApplication(data.restrictedBackupMode, null); mInitialApplication = app; try { mInstrumentation.onCreate(data.instrumentationArgs); } catch (Exception e) { throw new RuntimeException( "Exception thrown in onCreate() of " + data.instrumentationName + ": " + e.toString(), e); } try { mInstrumentation.callApplicationOnCreate(app); } catch (Exception e) { if (!mInstrumentation.onException(app, e)) { throw new RuntimeException( "Unable to create application " + app.getClass().getName() + ": " + e.toString(), e); } } } } } |
我們看到這裡同樣把 Application 例項儲存在 ActivityThread 的成員變數mInitialApplication
中,緊接著我們看看誰是呼叫了handleBindApplication
方法,很快就能定位到ActivityThread.H#handleMessage(Message)
裡面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | public final class ActivityThread { public final void bindApplication( |