哥白尼·罗斯福·马丁路德·李开复·嫁衣曾经说过

Where there is an Android App, there is an Application context.

没毛病,扎心了。App运行的时候,肯定是存在至少一个Application实例的。同时,Context我们再熟悉不过了,写代码的时候经常需要使用到Context实例,它一般是通过构造方法传递进来,通过方法的形式参数传递进来,或者是通过attach方法传递进我们需要用到的类。Context实在是太重要了,以至于我经常恨不得着藏着掖着,随身带着,这样需要用到的时候就能立刻掏出来用用。但是换个角度想想,既然App运行的时候,Application实例总是存在的,那么为何不设置一个全局可以访问的静态方法用于获取Context实例,这样以来就不需要上面那些繁琐的传递方式。

说到这里,有的人可能说想这不是我们经常干的好事吗,有必要说的这么玄乎?少侠莫急,请听吾辈徐徐道来。

获取Context实例的一般方式

这再简单不过了。

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不是吗。

获取Context实例的二般方式

现在许多开发者喜欢设计一个全局可以访问的静态方法,这样以来在设计API的时候,就不需要依赖Context了,代码看起来像是这样的。

/*
 * 全局获取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同时继承BaseApplicationSdkApplication啊?(场面一度很尴尬,这里省略一万字。)

以上谈到的,都是以前我们在获取Context实例的时候遇到的一些麻烦:

  1. 类API设计需要依赖Context(这是一种好习惯,我可没说这不好);

  2. 持有静态的Context实例容易引发的内存泄露问题;

  3. 需要提注册Context实例(或者释放);

  4. 污染程序的Application类;

那么,有没有一种方式,能够让我们在整个项目中可以全局访问到Context实例,不要提前注册,不会污染Application类,更加不会引发静态Context实例带来的内存泄露呢?

一种全局获取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。所以,膜法什么的,还是少用为妙吧。

参考链接

ActivityThread.java

你可能感兴趣的文章

L_Fork · 9月5日

没人点赞吗?

回复

载入中...
Kaede Kaede

1.6k 声望

发布于专栏

中二病也要开发ANDROID

Talk is cheap, let me show you the code. Hey guys, 这边是Kaede中二属性满载的Android开发讲座,在这里你不仅能得到技术上的愉♀悦,还能学到各种各样的姿♂势。 一発やらないか?アンドロイド最高!

177 人关注