1. 程式人生 > >一種 Android 應用內全域性獲取 Context 例項的裝置

一種 Android 應用內全域性獲取 Context 例項的裝置

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

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
18
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 的例項使用,而前者本身又實現了別的介面,比如以下程式碼。

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
15
/*
 * 全域性獲取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 例項帶來的問題,這不是本篇幅的關注點)。好吧,以我的聰明才智,很快就想到了優化方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
 * 全域性獲取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下移到基礎庫裡面也是一種辦法,不過以我的聰明才智又立刻想到了個好點子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
 * 全域性獲取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)的。

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
17
/**
 * 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)程式碼段裡。

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
/**
 * 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)

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
50
/**
 * 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 例項儲存起來,這樣我們就沒有辦法獲取到這個例項了。

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
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),光從名字我們就能猜出這個方法是幹什麼的,直覺告訴我們離目標不遠了~

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(