欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

一种 Android 应用内全局获取 Context 实例的装置

程序员文章站 2022-04-06 23:51:14
一种 Android 应用内全局获取 Context 实例的装置。App 运行的时候,肯定是存在至少一个 Application 实例的。同时,Context 我们再熟悉不过了...

一种 Android 应用内全局获取 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同时继承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)的。

 
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
 * .
 */
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 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 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。

(╯>д<)╯?˙3˙? 对嘛,这才科学嘛。

通过测试发现,在 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 实例,再接着返回这个实例。

在这里需要特别强调的时候,通过这样的方法获取 Context 实例,只要在Application#attachBaseContext(Context)执行之后才能获取到对象,在之前或者之内获取到的对象都是 null,具体原因可以参考上面调用流程中的ActivityThread#handleBindApplication。所以,膜法什么的,还是少用为妙吧。