一種Android應用內全局獲取Context實例的裝置

哥白尼·羅斯福·馬丁路德·李開復·嫁衣曾經說過java

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

沒毛病,扎心了。App運行的時候,確定是存在至少一個Application實例的。同時,Context咱們再熟悉不過了,寫代碼的時候常常須要使用到Context實例,它通常是經過構造方法傳遞進來,經過方法的形式參數傳遞進來,或者是經過attach方法傳遞進咱們須要用到的類。Context實在是過重要了,以致於我常常巴不得着藏着掖着,隨身帶着,這樣須要用到的時候就能馬上掏出來用用。可是換個角度想一想,既然App運行的時候,Application實例老是存在的,那麼爲什麼不設置一個全局能夠訪問的靜態方法用於獲取Context實例,這樣以來就不須要上面那些繁瑣的傳遞方式。git

說到這裏,有的人可能說想這不是咱們常常乾的好事嗎,有必要說的這麼玄乎?少俠莫急,請聽吾輩徐徐道來。github

獲取Context實例的通常方式

這再簡單不過了。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

獲取Context實例的二般方式

如今許多開發者喜歡設計一個全局能夠訪問的靜態方法,這樣以來在設計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同時繼承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

相關文章
相關標籤/搜索