1. 程式人生 > >你需要知道的Android上下文Context

你需要知道的Android上下文Context

Context對於Android開發者來說肯定不陌生,在我們跳轉新的Activity、彈出Toast,建立View等行為時都需要用到Context,可見Context每天都伴隨著我們,但是Context究竟是什麼意思呢?Context從中文翻譯上是上下文、環境、場景。我個人更傾向用場景來理解Context,例如建立一個View,我們在構造方法傳一個Context的,而傳進去的Context則代表著這個View所處的一個佈局的父類場景中,相當於該View(子View)與父View進行了一個場景關聯。

原始碼的Context

這裡寫圖片描述
原始碼對Context的解析是Context是一個關於應用程式環境全域性資訊的介面。Context提供了一個抽象類的實現Android系統。它允許訪問特定於應用程式的資源和類,包括應用程式級操作,如啟動Activity,發廣播,接收Intent等。

下面我們來看一下Context的繼承結構:
這裡寫圖片描述

在原始碼我們可以看到Context是一個抽象類,從上圖我們可以看出實現Context抽象類有ContextImpl與ContextWrapper,從類名我們可以理解到分別是實現類與包裝類。我們可以在圖中看到我們很熟悉的Activity、Service、Application,他們都是ContextWrapper的子類,ContextWrapper建構函式中必須包含一個真正的Context引用,同時ContextWrapper中提供了attachBaseContext()用於給ContextWrapper物件中指定真正的Context物件,所以在Activity、Service、Application類中的attch()中都有attachBaseContext()方法,呼叫該方法都會被轉向其所包含的真正的Context物件。而ContextImpl類則真正實現了Context中的所有函式,應用程式中所呼叫的各種Context類的方法,其實現均來自於該類。
這裡寫圖片描述

Context數量

理解Context的作用,可以通過Context的個數來進行理解。從上圖的繼承關係可以看出,Context數量 = Activity數量 + Service數量 + Application數量。但是有人會有這個疑問,那麼我們的四大元件,還有Broadcast Receiver和Content Provide並不是Context的子類,使用他們的時候用到的Context是從其他地方傳過去的,所以是不算進Context的總數中,從上面的種種可以看出Context類在Android系統中地位是多麼崇高。

Context的功能

Context實在太多了,啟動Activity、彈出Toast、啟動Service、建立View等等都用到Context。
TextView textView = new TextView(context);

context.getSharedPreferences(name, mode);

LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

context().startActivity(intent);

context().startService(intent);

context().sendBroadcast(intent);

···

Context應用場景

用到Context的地方是很多,但是Context也有一定的規則。參考Context,What Context?

這裡寫圖片描述

大家注意看到有一些NO上添加了一些數字,其實這些從一定程度上來說是YES,但是為什麼說是NO呢?下面一個一個解釋:

1.應用程式可以從這裡開始一個Activity,但它需要一個新的Task被建立。這可能適合特定的用例,但是一般情況不推薦。
2.這是合法的,但inflation將使用系統預設主題您正在執行的系統,而不是在應用程式中定義的。
3.允許如果Receiver是null,用於獲取的當前值的廣播,在安卓4.2及以上。

另外上圖我選Dialog來說一下。記得我剛開發時犯的錯。就是在建立Dialog的時候將getApplication()傳進去,因為封裝Dialog的方法有tryCatch,導致自己沒及時發現,釋出到線上後在統計後臺中發現報錯,這個是我對這Context的時候印象最深刻的。

當時以為Activity的Context與Application的Context是通用的,就直接用了,但XXXActivity和getApplicationContext返回的肯定不是一個物件,一個是當前Activity的例項,一個是專案的Application的例項。既然區別這麼明顯,那麼各自的使用場景肯定不同,亂使用可能會帶來一些問題。
那麼令有一個問題,我們如果正確獲取Context?

獲取Context

通常我們獲取Context物件有下面的方式:
1.View.getContext(),返回當前View物件的Context物件,通常是當前正在展示的Activity物件。
2.Activity.getApplicationContext,獲取當前Activity所在程序的全域性Context物件。
3.ContextWrapper.getBaseContext():用來獲取一個ContextWrapper進行裝飾之前的Context,可以使用這個方法,這個方法在實際開發中使用並不多,也不建議使用。
4.Activity.this 返回當前的Activity例項,如果場景在Activity進行,如UI控制元件需要使用Activity作為Context物件。

另外在輸出getApplication時候,IDE會提示getApplicationContext 、getApplication,那麼它們究竟有什麼不同呢?
這裡寫圖片描述
我們通過上面的程式碼,列印得出兩者的記憶體地址都是相同的,看來它們是同一個物件。其實這個結果也很好理解,因為前面已經說過了,Application本身就是一個Context,所以這裡獲取getApplicationContext()得到的結果就是Application本身的例項。那麼問題來了,既然這兩個方法得到的結果都是相同的,那麼Android為什麼要提供兩個功能重複的方法呢?實際上這兩個方法在作用域上有比較大的區別。getApplication()方法的語義性非常強,一看就知道是用來獲取Application例項的,但是這個方法只有在Activity和Service中才能呼叫的到。那麼也許在絕大多數情況下我們都是在Activity或者Service中使用Application的,但是如果在一些其它的場景,比如BroadcastReceiver中也想獲得Application的例項,這時就可以藉助getApplicationContext()方法了。

Context引起的記憶體洩露

public class CustomManager {
    private static CustomManager sInstance;

    public static CustomManager getInstance(Context context) {
        if (sInstance == null) {
            sInstance = new CustomManager(context);
        }

        return sInstance;
    }

    private Context mContext;

    private CustomManager(Context context) {
        mContext = context;
    }
}

這是很常見的單例寫法,內部持有一個Context引用,當我們某Activity在使用該封裝類時,為了方便會直接使用Activity.this傳進該類中,問題來了,該類的sInstance是一個靜態強引用,然而內部引用了一個Activity的Context,換一句話說,專案一直活著,sInstance就會一直劫持該Context,記憶體無法被回收,因此導致記憶體洩露。
那麼我們解決方法就是:

public class CustomManager {
    private static CustomManager sInstance;

    public static CustomManager getInstance(Context context) {
        if (sInstance == null) {
            //Always pass in the Application Context
            sInstance = new CustomManager(context.getApplicationContext());
        }

        return sInstance;
    }

    private Context mContext;

    private CustomManager(Context context) {
        mContext = context;
    }
}

將封裝類的context.getApplicationContext,讓ApplicationContext指向這個靜態sInstance單例,sInstance與ApplicationContext的生命週期一直,這樣就可以解決了記憶體洩露的問題,Activity也能finish掉後正常讓垃圾回收機制回收。

正確使用Context

一般Context造成的記憶體洩漏,幾乎都是當Context銷燬的時候,卻因為被引用導致銷燬失敗,而Application的Context物件可以理解為隨著程序存在的,那麼使用Context的正確姿勢是:
1:當Application的Context能搞定的情況下,並且生命週期長的物件,優先使用Application的Context。
2:不要讓生命週期長於Activity的物件持有到Activity的引用。
3:儘量不要在Activity中使用非靜態內部類,因為非靜態內部類會隱式持有外部類例項的引用,如果使用靜態內部類,將外部例項引用作為弱引用持有。


最後總結下,Context的分析基本就如上述內容,Context伴隨著我們每天的開發,我們需要的是在使用Context的時候,注意使用Activity.Context是否合適,會不會造成記憶體洩露的可能。