在上一篇博客中,我講了初次開發安卓必須知道的 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靜態引用都是“欺騙”
靜態的獲取全局變量並沒有將它們(全局變量)構造函數和方法的依賴關係告訴給閱讀代碼的人。全局變量和單例通過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靜態引用的對象不是封裝的
(封裝)保障了一個對象的行爲只能被它的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靜態引用的對象可能難以進行單元測試
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靜態引用的對象更可能違背迪米特法則(最少知道法則、最少引用法則)
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;
}
}
這就違背了迪米特法則。我實際上也抱怨過違背迪米特法則使得一個程序難以進行單元測試。但是即使你不關心單元測試,違背迪米特法則通常被視爲一種噁心的代碼風格。