哥白尼・罗斯福・马丁路德・李开复・嫁衣曾经说过
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 public static class Foo1 { public Foo1 (Context context) { } } public static class Foo2 { public Foo2 attach (Context context) { } } public static class Foo2 { public void foo (Context context) { } }
这种方式应该是最常见的获取 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 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 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 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 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 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); } 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 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 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 public final class ActivityThread { private void attach (boolean system) { sCurrentActivityThread = this ; mSystemThread = system; if (!system) { ... } else { 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 (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 实例了。啊,这用反射搞起来简直再简单不过了!说搞就搞。
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 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; } } 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)); Assert.assertTrue(context instanceof MyApplication); } }
这样以来, 无论在项目的什么地方,无论是在 App 模块还是 Library 模块,都可以通过 Applications#context () 获取 Context 实例,而且不需要做任何初始化工作,也不用担心静态 Context 实例带来的问题,测试代码跑起来没问题,接入项目后也没有发现什么异常,我们简直要上天了。不对,哪里不对。不科学,一般来说不可能这么顺利的,这一定是错觉。果然项目上线没多久后立刻原地爆炸了,在一些机型上,通过 Applications#context () 获取到的 Context 恒为 null。
(╯>д<)╯⁽˙³˙⁾ 对嘛,这才科学嘛。
通过测试发现,在 4.1.1 系统的机型上,会稳定出现获取结果为 null 的现象,看来是系统源码的实现上有一些出入导致,总之先看看源码吧。
1 2 3 4 5 6 7 8 9 10 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 。
在这里需要特别强调的时候,通过这样的方法获取 Context 实例,只要在 Application#attachBaseContext (Context) 执行之后才能获取到对象,在之前或者之内获取到的对象都是 null,具体原因可以参考上面调用流程中的 ActivityThread#handleBindApplication。所以,膜法什么的,还是少用为妙吧。
参考链接 ActivityThread.java