哥白尼·羅斯福·馬丁路德·李開復·嫁衣曾經說過java
Where there is an Android App, there is an Application context.android
沒毛病,扎心了。App運行的時候,確定是存在至少一個Application實例的。同時,Context咱們再熟悉不過了,寫代碼的時候常常須要使用到Context實例,它通常是經過構造方法傳遞進來,經過方法的形式參數傳遞進來,或者是經過attach方法傳遞進咱們須要用到的類。Context實在是過重要了,以致於我常常巴不得着藏着掖着,隨身帶着,這樣須要用到的時候就能馬上掏出來用用。可是換個角度想一想,既然App運行的時候,Application實例老是存在的,那麼爲什麼不設置一個全局能夠訪問的靜態方法用於獲取Context實例,這樣以來就不須要上面那些繁瑣的傳遞方式。git
說到這裏,有的人可能說想這不是咱們常常乾的好事嗎,有必要說的這麼玄乎?少俠莫急,請聽吾輩徐徐道來。github
這再簡單不過了。app
public static class Foo1 { public Foo1(Context context) { // 1. 在構造方法帶入 } } public static class Foo2 { public Foo2 attach(Context context) { // 2. 經過attach方法帶入 return this; } } public static class Foo2 { public void foo(Context context) { // 3. 調用方法的時候,經過形參帶入 } }
這種方式應該是最多見的獲取Context實例的方式了,優勢就是嚴格按照代碼規範來,不用擔憂兼容性問題;缺點就是API設計嚴重依賴於Context這個API,若是早期接口設計不嚴謹,後期代碼重構的時候可能很要命。此外還有一個比較有趣的問題,咱們常用Activity或者Application類的實例做爲Context的實例使用,而前者自己又實現了別的接口,好比如下代碼。異步
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
不是嗎。ide
如今許多開發者喜歡設計一個全局能夠訪問的靜態方法,這樣以來在設計API的時候,就不須要依賴Context了,代碼看起來像是這樣的。oop
/* * 全局獲取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實例帶來的問題,這不是本篇幅的關注點)。好吧,以個人聰明才智,很快就想到了優化方案。測試
/* * 全局獲取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
下移到基礎庫裏面也是一種辦法,不過以個人聰明才智又馬上想到了個好點子。優化
/* * 全局獲取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實例帶來的內存泄露呢?
回到最開始的話,App運行的時候,確定存在至少一個Application實例。若是咱們可以在系統建立這個實例的時候,獲取這個實例的應用,是否是就能夠全局獲取Context實例了(由於這個實例是運行時一直存在的,因此也就不用擔憂靜態Context實例帶來的問題)。那麼問題來了,Application實例是何時建立的呢?首先先來看看咱們常常用來獲取Base Context實例的Application#attachBaseContext(Context)
方法,它是繼承自ContextWrapper#attachBaseContext(Context)
的。
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)
。
public class Application extends ContextWrapper { final void attach(Context context) { attachBaseContext(context); mLoadedApk = ContextImpl.getImpl(context).mPackageInfo; } }
又是誰調用了Application#attach(Context)
方法呢?一路下來能夠直接定位到Instrumentation#newApplication(Class<?>, Context)
方法裏(這個方法名很好懂啊,一看就知道是幹啥的)。
/** * 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)
代碼段裏。
/** * 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)
。
/** * 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實例保存起來,這樣咱們就沒有辦法獲取到這個實例了。
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)
,光從名字咱們就能猜出這個方法是幹什麼的,直覺告訴咱們離目標不遠了~
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)
裏面。
public final class ActivityThread { public final void bindApplication(String processName, ApplicationInfo appInfo, List<ProviderInfo> providers, ComponentName instrumentationName, ProfilerInfo profilerInfo, Bundle instrumentationArgs, IInstrumentationWatcher instrumentationWatcher, IUiAutomationConnection instrumentationUiConnection, int debugMode, boolean enableBinderTracking, boolean trackAllocation, boolean isRestrictedBackupMode, boolean persistent, Configuration config, CompatibilityInfo compatInfo, Map<String, IBinder> services, Bundle coreSettings) { ... sendMessage(H.BIND_APPLICATION, data); } private class H extends Handler { public void handleMessage(Message msg) { switch (msg.what) { ... case BIND_APPLICATION: Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindApplication"); AppBindData data = (AppBindData)msg.obj; handleBindApplication(data); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); break; case EXIT_APPLICATION: if (mInitialApplication != null) { mInitialApplication.onTerminate(); } Looper.myLooper().quit(); break; ... } } } }
Bingo!至此一切都清晰了,ActivityThread#mInitialApplication
確實就是咱們須要找的Application實例。整個流程捋順下來,系統建立Base Context實例、Application實例,以及把Base Context實例attach到Application內部的流程大體能夠概括爲如下調用順序。
ActivityThread#bindApplication (異步) --> ActivityThread#handleBindApplication --> LoadedApk#makeApplication --> Instrumentation#newApplication --> Application#attach --> ContextWrapper#attachBaseContext
源碼擼完了,再回到咱們一開始的需求來。如今咱們要獲取ActivityThread的靜態成員變量sCurrentActivityThread。閱讀源碼後咱們發現能夠經過ActivityThread#currentActivityThread()
這個靜態方法來獲取這個靜態對象,而後經過ActivityThread#getApplication()
方法就可能直接獲取咱們須要的Application實例了。啊,這用反射搞起來簡直再簡單不過了!說搞就搞。
public class Applications { @NonNull public static Application context() { return CURRENT; } @SuppressLint("StaticFieldLeak") private static final Application CURRENT; static { try { Object activityThread = getActivityThread(); Object app = activityThread.getClass().getMethod("getApplication").invoke(activityThread); CURRENT = (Application) app; } catch (Throwable e) { throw new IllegalStateException("Can not access Application context by magic code, boom!", e); } } private static Object getActivityThread() { Object activityThread = null; try { Method method = Class.forName("android.app.ActivityThread").getMethod("currentActivityThread"); method.setAccessible(true); activityThread = method.invoke(null); } catch (final Exception e) { Log.w(TAG, e); } return activityThread; } } // 測試代碼 @RunWith(AndroidJUnit4.class) public class ApplicationTest { public static final String TAG = "ApplicationTest"; @Test public void testGetGlobalContext() { Application context = Applications.context(); Assert.assertNotNull(context); Log.i(TAG, String.valueOf(context)); // MyApplication是項目的自定義Application類 Assert.assertTrue(context instanceof MyApplication); } }
這樣以來, 不管在項目的什麼地方,不管是在App模塊仍是Library模塊,均可以經過Applications#context()
獲取Context實例,並且不須要作任何初始化工做,也不用擔憂靜態Context實例帶來的問題,測試代碼跑起來沒問題,接入項目後也沒有發現什麼異常,咱們簡直要上天了。不對,哪裏不對。不科學,通常來講不可能這麼順利的,這必定是錯覺。果真項目上線沒多久後馬上原地爆炸了,在一些機型上,經過Applications#context()
獲取到的Context恆爲null。
(╯>д<)╯⁽˙³˙⁾ 對嘛,這才科學嘛。
經過測試發現,在4.1.1系統的機型上,會穩定出現獲取結果爲null的現象,看來是系統源碼的實現上有一些出入致使,總之先看看源碼吧。
public final class ActivityThread { public static ActivityThread currentActivityThread() { return sThreadLocal.get(); } private void attach(boolean system) { sThreadLocal.set(this); ... } }
原來是這麼一個幺蛾子,在4.1.1系統上,ActivityThread是使用一個ThreadLocal實例來存放靜態ActivityThread實例的。至於ThreadLocal是幹什麼用的這裏暫不展開,簡單說來,就是系統只有在UI線程使用sThreadLocal來保存靜態ActivityThread實例,因此咱們只能在UI線程經過sThreadLocal獲取到這個保存的實例,在Worker線程sThreadLocal會直接返回空。
這樣以來解決方案也很明朗,只須要在事先如今UI線程觸發一次Applications#context()
調用保存Application實例便可。不過項目的代碼一直在變化,咱們很難保證不會有誰不當心觸發了一次優先的Worker線程的調用,那就GG了,因此最好在Applications#context()
方法裏處理,咱們只須要確保能在Worker線程得到ActivityThread實例就Okay了。不過一時半會我想不出切確的辦法,也找不到適合的切入點,只作了下簡單的處理:若是是優先在Worker線程調用,就先使用UI線程的Handler提交一個任務去獲取Context實例,Worker線程等待UI線程獲取完Context實例,再接着返回這個實例。
最終完成的代碼能夠參考 Applications。
(補充 2017-04-13)
在這裏須要特別強調的時候,經過這樣的方法獲取Context實例,只要在Application#attachBaseContext(Context)
執行以後才能獲取到對象,在以前或者以內獲取到的對象都是null,具體緣由能夠參考上面調用流程中的ActivityThread#handleBindApplication
。因此,膜法什麼的,仍是少用爲妙吧。