使用ApplicationContext作爲全局變量引用的缺陷

在上一篇博客中,我講了初次開發安卓必須知道的 6件事(6 THINGS I WISH I KNEW BEFORE I WROTE MY FIRST ANDROID APP)。其中一條就是:不要有一個Context的靜態引用。我這麼警告的原因是一個Context的靜態引用可能引發內存泄露。但是一位讀者指出:一個Application Context的靜態引用不會造成內存泄露,因爲只要程序還在運行,Application Context的生命週期就不會結束。我則反駁到:技術上來說,你可以擁有一個Application Context的靜態引用而不造成內存泄露,但是我推薦你這樣做。

在這篇博客中,我想解釋一下爲什麼擁有和使用一個Application Context的靜態引用不是一個理想的選擇。之所以強調“理想的選擇”,因爲我並不是說使用Application Context的靜態引用每次都會造成程序崩潰。相對的,我這篇博客所說的是一些使用Context靜態引用的缺陷,以至於說這並不是開發安卓應用最簡潔的方式。

1. 對象/方法 使用Application Context靜態引用都是“欺騙”

這一點是出自谷歌對於編寫可測試代碼的指南(Google’s Guide to Writing testable code)。在這個指南中,他們指出了:

靜態的獲取全局變量並沒有將它們(全局變量)構造函數和方法的依賴關係告訴給閱讀代碼的人。全局變量和單例通過API掩蓋了它們真實的依賴關係。如果想要真正理解依賴關係,開發人員必須逐行閱讀代碼。(Accessing global state statically doesn’t clarify those shared dependencies to readers of the constructors and methods that use the Global State. Global State and Singletons make APIs lie about their true dependencies. To really understand the dependencies, developers must read every line of code.)

一個Application Context的全局靜態引用也正如這點所說:讀這個對象的人無法知道,這個對象依賴Context只是爲了使用它的API。當一個對象擁有一個清晰真實的API來表達它的依賴,就能夠更容易的理解類或者方法的功能以及它將如何實現這個功能。

下面用一個簡單的例子進行闡述。假設你當你閱讀代碼時,遇到了一個這樣的方法名稱:

public void displayString(String stringToDisplay)

當你遇到這個名稱的時候,你沒有辦法知道這個方法會怎樣顯示參數傳入的字符串。現在,假設你閱讀到的是這樣的方法名:

public void displayString(Context context, String stringToDisplay)

對於這樣的方法名,你有一個線索(譯者:線索指Context參數):這個方法也許是用Toast顯示字符串。因爲Context是一個“萬能類”,對於一個特定的對象或者方法使用了它並不總是能夠顯示這個對象/方法的功能或者它如何實現這個功能的。但是,一點點的提示也比沒有任何提示要強。

(譯者:可能翻譯的有些不通順了,這裏總的解釋一下。就是說使用ApplicationContext的靜態引用去使用一些方法的話,你是無法判斷這個方法是不是需要用到Context的。而如果不使用ApplicationContext的靜態引用的話,當一個方法需要用到Context對象時(如Toast),就必須多一個Context參數。而不需要時,則不會有Context參數。閱讀代碼的時候就可以根據這一點對方法的實現上有一定的估計)。

2. 使用了Application Context靜態引用的對象不是封裝的

封裝雖然經常被提及,但是並沒有一個準確的定義。我也不打算使這個定義更復雜。當說“封裝”時,我指的是Steve FreemanNat Pryce基於測試的面向對象編程(Growing Object Oriented Software Guided by Tests)所提到的概念:

(封裝)保障了一個對象的行爲只能被它的API所影響。它保障了無關對象之間不會產生未知的引用,從而使得我們可以控制一個對象的修改對系統其它部分的影響。([It] Ensures that the behavior of an object can only be affected through its API. It lets us control how much a change to one object will impact other parts of the system by ensuring that there are no unexpected dependencies between unrelated components. -Pg. 92)

因爲使用ApplicationContext靜態引用的對象關聯的是一個全局依賴,這些對象的行爲可能會被全局共享的Application Context所影響。因爲Application Context並不是這些對象API的一部分,這就意味着對象行爲的改變可能不是被該對象的API所影響的。換一句話說,這就意味着使用Application Context靜態引用會破壞封裝。

在大多數情況下,以這樣一種形式破壞封裝不會產生太大影響。事實上,我僅可以想象的幾個可能產生問題的例子也看起來像是故意編造的。但是,我仍然認爲,在其他條件相等的情況下,我們應當選擇能100%在全部情況下適用的結構,而不是99%都適用的結構。再一次的,使用ApplicationContext的靜態引用和對封裝的破壞並不會使你的程序崩潰,但是這並不是最穩定的結構。

3. 使用了Application Context靜態引用的對象可能難以進行單元測試

如果你的一個對象調用了Application Context裏的一個方法,並且你想要驗證這個方法在單元測試中被調用了,使用一個靜態引用不會讓你好受。正如我在爲什麼Android單元測試這麼困難中所說,你會在一些情況下想要進行這樣一個操作。假設你已經有一個啓動安卓Service的ServiceLauncher對象。如果你使用了依賴注入來在ServiceLaucher被引用的時候傳入一個Context對象,單元測試就很簡單:
public class ServiceLauncherTests {

    @Mock
    Context mContext;

    @Test
    public void launchesSessionCalendarService() {
        ServiceLauncher serviceLauncher = new ServiceLauncher(mContext);
        serviceLauncher.launchSessionCalendarService();
        verify(mContext).startService(any(Intent.class));
    }
}
如果這個ServiceLaucher使用了Application Context靜態引用,這個對象就很難進行單元測試了。在這個例子當中,你可以使用測試支持庫的UI測試來驗證Intent被髮送了,但是UI測試比單元測試要慢。並且,你也可能需要驗證Context中也一些不使用Intent的方法。所以,注入一個Context到目標對象中相比全局靜態變量更加靈活,即使你可以使用測試支持庫來幫助你驗證Intent的發送。

4. 使用了Application Context靜態引用的對象更可能違背迪米特法則(最少知道法則、最少引用法則)

我們經常使用Context去獲取一個我們需要的對象的引用。一個特定的對象可能會需要Resoureces, SharePreferences或者 PackageManager去實現它的功能。當我們有一個全局Application Context引用時,我們可能會嘗試去通過這樣一種方式去獲取這些對象的引用:
public class SmellySessionColorResolver {

    public SmellySessionColorResolver() {
    }

    public int resolveSessionColor(int sessionColor) {
        if (sessionColor == 0) {
            // no color -- use default
            sessionColor = IOApplication.getContext().getResources().getColor(R.color.default_session_color);
        } else {
            // make sure it's opaque
            sessionColor = UIUtils.setColorAlpha(sessionColor, 255);
        }
        return sessionColor;
    }
}
這就違背了迪米特法則。我實際上也抱怨過違背迪米特法則使得一個程序難以進行單元測試。但是即使你不關心單元測試,違背迪米特法則通常被視爲一種噁心的代碼風格。

結論

我不認爲我講得東西存在太大的爭議。我認爲我只是把從更聰明的人身上學到的通用編程課程給運用上了而已。當然,歡迎批評和指正。
如果你確信了你需要避免Application Context靜態引用的使用,向必要的對象和方法中注入Context應當不是一件難事。你甚至可能發現你在重構過程中能夠消除一大堆違背迪米特法則的代碼。Android Studio的推測和重構功能使得這項工作更加輕鬆,哪怕有點無聊。

譯者結論

這篇文章基本列舉了使用Application Context幾個公認的弊端。對於將Application Context當作全局變量的使用,可能是因爲這是安卓獨特的方法,一度被認爲是最合理的方法。然而谷歌官方文檔卻指出了,重寫Application類其實是一種不推薦的做法,因爲並沒有任何理由去說明它比傳統的JAVA全局變量要好。而且這樣做還容易使得Application變得又臭又長,引用的時候也需要添加很長的一句話。因此,我還是贊同不要使用Application Context的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章