一種 Android 應用內全局獲取 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。所以,膜法什麼的,還是少用為妙吧。

You May Also Like