Android基礎之Context

Android基礎之Context

Context

對于Android開發者來說Context應該是非常熟悉的。我們在使用Toast,啟動Activity,啟動Service,加載資源,操作數據庫,獲取App相關的文件路徑,創建View等操作時,都會涉及到一個Context引用。那么Context到底是什么,Activity和Application作為Context到底有什么區別,哪個更好呢?以及我們在項目建立最初自定義的Application提供全局Context該怎么實現呢?

什么是Context

寫過一個小Android demo的應該會有一個疑問為什么一個Android應用不需要一個main()方法來啟動整個程序,如果是通過一個main()方法來啟動程序,那么我們就可以在里面執行例如new一個Activity等操作。Android的應用模型是基于組件的應用設計模式,組件能夠運行需要一個完整的工程環境。同時,各個組件擁有自己獨立的場景(Context),并不能采用new的方式創建一個組件對象。Context是維持Android程序中各組件能夠正常工作的一個核心功能類。

總結:一個應用是一個完整的工程環境,在這個應用中我們有許多功能,例如打電話,發短信,發郵件。這些功能都算是這個環境中的一個場景。比如發短信場景包含界面的顯示以及數據的傳遞等等。所以我們可以把場景(Context)理解為用戶與操作系統交互的一個過程。

Context類

首先來看Context類的介紹

/**
 * Interface to global information about an application environment.  This is
 * an abstract class whose implementation is provided by
 * the Android system.  It
 * allows access to application-specific resources and classes, as well as
 * up-calls for application-level operations such as launching activities,
 * broadcasting and receiving intents, etc.
 */
public abstract class Context {
    ......
}

他的注釋:Context提供了關于應用環境全局信息的接口。它是一個抽象類,它的執行被Android系統所提供。它允許獲取以應用為特征的資源和類型,是一個統領一些資源(應用程序環境變量等)的上下文。也就是說Context描述一個應用程序環境的信息(即上下文);是一個抽象類,Android提供了該抽象類的具體實現類;通過它我們可以獲取應用程序的資源和類(包括應用級別操作,如啟動Activity,發廣播,接受Intent等)。

引用自博客Android應用Context詳解及源碼解析

那么在平常開發中我們用到哪些這個Context抽象類的實現呢?

盜用大牛郭霖的圖片.png

可以發現Application,Service,Activity都是者間接繼承自Context。在上面的繼承關系中我們看到ContextImpl和ContextWrapper是直接繼承自Context。ContextWrapper是Context類的一個封裝類,而ContextImpl是Context抽象類的實現類。ContextWrapper的構造函數中有一個真正的Context的引用(ContextImpl對象,mBase)。ContextWrapper的子類有一個ContextThemeWrapper,是帶主題封裝的類,即通過android:theme屬性指定的(例如Activity帶UI顯示)。

那么我們一個應用中有多少個Context呢?

Android應用程序只有四大組件,而其中兩大組件都繼承自
Context,另外每個應用程序還有一個全局的Application對象。所以在我們了解了上面繼承關系之后我們就可以計算出來Context總數,如下:

appContextCount = Application + ActivityCount + ServiceCount;

Context的實例化

Activity,Application,Service實例都是在ActivityThread中創建的。我參考了博客Android應用Context詳解及源碼解析(因為自己還沒有深入了解到App的啟動,只是想了解Context基礎知識)

Activity中ContextImpl實例化源碼分析

通過startActivity啟動一個新的Activity時系統會回調ActivityThread的handleLaunchActivity()方法,該方法內部會調用performLaunchActivity()方法去創建一個Activity實例,然后回調Activity的onCreate()等方法。所以Activity的ContextImpl實例化是在ActivityThread類的performLaunchActivity方法中,如下:

    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        ......
            //已經創建好新的activity實例
            if (activity != null) {
                //創建一個Context對象
                Context appContext = createBaseContextForActivity(r, activity);
                ......
                //將上面創建的appContext傳入到activity的attach方法
                activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor);
                ......
            }
        ......
        return activity;
    }
    

通過createBaseContextForActivity(r, activity);創建appContext,然后通過activity.attach設置值。

createBaseContextForActivity(r, activity)部分源代碼:

    private Context createBaseContextForActivity(ActivityClientRecord r,
            final Activity activity) {
        //實質就是new一個ContextImpl對象,調運ContextImpl的有參構造初始化一些參數    
        ContextImpl appContext = ContextImpl.createActivityContext(this, r.packageInfo, r.token);
        //特別特別留意這里!!!
        //ContextImpl中有一個Context的成員叫mOuterContext,通過這條語句就可將當前新Activity對象賦值到創建的ContextImpl的成員mOuterContext(也就是讓ContextImpl內部持有Activity)。
        appContext.setOuterContext(activity);
        //創建返回值并且賦值
        Context baseContext = appContext;
        ......
        //返回ContextImpl對象
        return baseContext;
    }

Activity.attach()部分源代碼:

    final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
        //特別特別留意這里!!!
        //與上面createBaseContextForActivity方法中setOuterContext語句類似,不同的在于:
        //通過ContextThemeWrapper類的attachBaseContext方法,將createBaseContextForActivity中實例化的ContextImpl對象傳入到ContextWrapper類的mBase變量,這樣ContextWrapper(Context子類)類的成員mBase就被實例化為Context的實現類ContextImpl
        attachBaseContext(context);
        ......
    }

總結:Activity通過ContextWrapper的成員mBase來引用了一個ContextImpl對象,這樣,Activity組件以后就可以調用這個ContextImpl對象的一系列方法(啟動Service等);同時ContextImpl類又通過自己的成員mOuterContext引用了與它關聯的Activity,這樣ContextImpl類也可以操作Activity。

由此說明一個Activity就有一個Context,而且生命周期和Activity類相同。在使用Activity作為Context引用時要考慮到內存泄漏。

Service中ContextImpl實例化源碼分析

通過startService或者bindService方法創建一個新Service時就會回調ActivityThread類的handleCreateService()方法完成相關數據操作。具體handleCreateService方法代碼如下:

    private void handleCreateService(CreateServiceData data) {
        ......
        //類似上面Activity的創建,這里創建service對象實例
        Service service = null;
        try {
            java.lang.ClassLoader cl = packageInfo.getClassLoader();
            service = (Service) cl.loadClass(data.info.name).newInstance();
        } catch (Exception e) {
            ......
        }

        try {
            ......
            //不做過多解釋,創建一個Context對象
            ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
            //特別特別留意這里!!!
            //ContextImpl中有一個Context的成員叫mOuterContext,通過這條語句就可將當前新Service對象賦值到創建的ContextImpl的成員mOuterContext(也就是讓ContextImpl內部持有Service)。
            context.setOuterContext(service);

            Application app = packageInfo.makeApplication(false, mInstrumentation);
            //將上面創建的context傳入到service的attach方法
            service.attach(context, this, data.info.name, data.token, app,
                    ActivityManagerNative.getDefault());
            service.onCreate();
            ......
        } catch (Exception e) {
            ......
        }
    }

Service中的attach方法:

    public final void attach(
            Context context,
            ActivityThread thread, String className, IBinder token,
            Application application, Object activityManager) {
            //特別特別留意這里!!!
            //與上面handleCreateService方法中setOuterContext語句類似,不同的在于:
            //通過ContextWrapper類的attachBaseContext方法,將handleCreateService中實例化的ContextImpl對象傳入到ContextWrapper類的mBase變量,這樣ContextWrapper(Context子類)類的成員mBase就被實例化為Context的實現類ContextImpl
        attachBaseContext(context);
        ......
    }

說明一個Service就有一個Context,而且生命周期和Service類相同,在使用Service作為Context引用時要考慮到內存泄漏

Application中ContextImpl實例化源碼分析

一個APP以后每次重新啟動時都會首先創建Application對象(每個APP都有一個唯一的全局Application對象,與整個APP的生命周期相同)。創建Application的過程也在ActivityThread類的handleBindApplication()方法完成相關數據操作。而ContextImpl的創建是在該方法中調運LoadedApk類的makeApplication方法中實現,LoadedApk類的makeApplication()方法中源代碼如下:

    public Application makeApplication(boolean forceDefaultAppClass,
            Instrumentation instrumentation) {
        //只有新創建的APP才會走if代碼塊之后的剩余邏輯
        if (mApplication != null) {
            return mApplication;
        }
        //即將創建的Application對象
        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")) {
                initializeJavaContextClassLoader();
            }
            //不做過多解釋,創建一個Context對象
            ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
            //將Context傳入Instrumentation類的newApplication方法
            app = mActivityThread.mInstrumentation.newApplication(
                    cl, appClass, appContext);
            //特別特別留意這里!!!
            //ContextImpl中有一個Context的成員叫mOuterContext,通過這條語句就可將當前新Application對象賦值到創建的ContextImpl的成員mOuterContext(也就是讓ContextImpl內部持有Application)。
            appContext.setOuterContext(app);
        } catch (Exception e) {
            ......
        }
        ......
        return app;
    }

Instrumentation.newApplication方法部分源碼:

    public Application newApplication(ClassLoader cl, String className, Context context)
            throws InstantiationException, IllegalAccessException, 
            ClassNotFoundException {
        return newApplication(cl.loadClass(className), context);
    }

newApplication方法部分源碼:

    static public Application newApplication(Class<?> clazz, Context context)
            throws InstantiationException, IllegalAccessException, 
            ClassNotFoundException {
        ......
        //繼續傳遞context
        app.attach(context);
        return app;
    }

Application類的attach方法部分源碼:

final void attach(Context context) {
        //特別特別留意這里!!!
        //與上面makeApplication方法中setOuterContext語句類似,不同的在于:
        //通過ContextWrapper類的attachBaseContext方法,將makeApplication中實例化的ContextImpl對象傳入到ContextWrapper類的mBase變量,這樣ContextWrapper(Context子類)類的成員mBase就被實例化為Application的實現類ContextImpl
        attachBaseContext(context);
        ......
    }

說明一個Application就有一個Context,而且生命周期和Application類相同(然而一個App只有一個Application,而且與應用生命周期相同)。

通過Context引用獲取資源

看到這里我有個疑問不同的Context引用,獲取到的資源是同一份嗎?

class ContextImpl extends Context {
    ......
    private final ResourcesManager mResourcesManager;
    private final Resources mResources;
    ......
    @Override
    public Resources getResources() {
        return mResources;
    }
    ......
}

有了上面分析我們可以很確定平時寫的App中context.getResources方法獲得的Resources對象就是上面ContextImpl的成員變量mResources。那我們追蹤可以發現mResources的賦值操作如下:

    private ContextImpl(ContextImpl container, ActivityThread mainThread,
            LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,
            Display display, Configuration overrideConfiguration) {
        ......
        //單例模式獲取ResourcesManager對象
        mResourcesManager = ResourcesManager.getInstance();
        ......
        //packageInfo對于一個APP來說只有一個,所以resources 是同一份
        Resources resources = packageInfo.getResources(mainThread);
        if (resources != null) {
            if (activityToken != null
                    || displayId != Display.DEFAULT_DISPLAY
                    || overrideConfiguration != null
                    || (compatInfo != null && compatInfo.applicationScale
                            != resources.getCompatibilityInfo().applicationScale)) {
                //mResourcesManager是單例,所以resources是同一份
                resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
                        packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(),
                        packageInfo.getApplicationInfo().sharedLibraryFiles, displayId,
                        overrideConfiguration, compatInfo, activityToken);
            }
        }
        //把resources賦值給mResources
        mResources = resources;
        ......
    }

由此可以看出在設備其他因素不變的情況下我們通過不同的Context實例得到的Resources是同一套資源。

以上源碼分析完全是引用自別人的博客,自己對上面的分析也只是停留在一個字面意思上,只是通過上面的分析理解了不同Context的實例相對于整個App的生命周期,可以在實際開發中作為判斷到底使用哪個Context引用的依據。以及解決了心中對于不同Context引用獲取資源的疑問

getApplication和getApplicationContext的區別

getApplication()方法

Activity和Service提供了getApplication,而且返回類型都是Application。

public final Application getApplication() {
        return mApplication;
    }

這個mApplication都是在各自類的attach方法參數出入的,也就是說這個mApplication都是在ActivityThread中各自實例化時獲取的makeApplication方法返回值。而且不同的Activity和Service返回的Application均為同一個全局對象。

getApplicationContext()方法

通過ContextImpl對象調用getApplicationContext()方法

class ContextImpl extends Context {
    ......
    @Override
    public Context getApplicationContext() {
        return (mPackageInfo != null) ?
                mPackageInfo.getApplication() : mMainThread.getApplication();
    }
    ......
}

可以看到getApplicationContext方法是Context的方法,而且返回值是Context類型,返回對象和上面通過Service或者Activity的getApplication返回的是一個對象。

區別

雖然這兩個方法最終的得到的值都是同一個對象的引用,但是它們的類型不同(一個是Application,一個是Context)且作用域有很大的區別。getApplication()方法的語義性非常強,一看就知道是用來獲取Application實例的,但是這個方法只有在Activity和Service中才能調用的到。那么也許在絕大多數情況下我們都是在Activity或者Service中使用Application的,但是如果在一些其它的場景,比如BroadcastReceiver中也想獲得Application的實例,這時就可以借助getApplicationContext()方法。

還有一個得到Context的方法,getBaseContext()。getBaseContext()方法得到的是一個ContextImpl對象。也就是調用ContextWrapper的getBaseContext()方法返回存儲ContextImpl對象的mBase。

Activity和Application作為Context引用

Context引起的內存泄漏

在實際開發中我們需要用到工具類,一般都會采用單例模式:

public class Singleton {
    private static Singleton instance;
    private Context mContext;
    private Singleton(Context context) {
        this.mContext = context;
    }
    public static synchronized Singleton getInstance(Context context) {
        if (instance == null) {
            instance = new Singleton(context);
        }
        return instance;
    }
   
}

你在某個Activity調用了getInstance()方法,直接傳了個this;這樣問題就來了,我們的這個類中的sInstance是一個static且強引用的,在其內部引用了一個Activity作為Context,也就是說,我們的這個Activity只要我們的項目活著,就沒有辦法進行內存回收。而我們的Activity的生命周期肯定沒這么長,所以造成了內存泄漏。

Java強引用會導致GC內存無法回收

解決辦法:

public class Singleton {
    private static Singleton instance;
    private Context mContext;
    private Singleton(Context context) {
        this.mContext = context;
    }
    public static synchronized Singleton getInstance(Context context) {
        if (instance == null) {
            instance = new Singleton(context.getApplicationContext());
        }
        return instance;
    }
   
}

由于Application對象的生命周期如上面分析的那樣,和整個項目一樣長。也就是它的生命周期和我們的單例對象一致。

不同的Context適用場景

正如上面所說的Context引用不當會造成內存泄漏,那么我們是否可以都引用Application作為Context引用呢?答案是否定的,不同Context(指的是Activity,Application,Service)的應用場景是不同的,并非所有Activity為Context的場景,Application Context都能搞定。

Application Activity Service
show dialog N Y N
start Activity N Y N
Layout Inflation N Y N
start service Y Y Y
send a BroadCast Y Y Y
register a BroadCast Receiver Y Y Y
load Resource Y Y Y

上述表格中可以看到,在啟動一個Activity和Dialog時,不推薦使用Application和Service。Android系統出于安全原因的考慮,是不允許Activity或Dialog憑空出現的,一個Activity的啟動必須要建立在另一個Activity的基礎之上,也就是以此形成的返回棧。而Dialog則必須在一個Activity上面彈出(除非是System Alert類型的Dialog),因此在這種場景下,我們只能使用Activity類型的Context,否則將會出錯。調用除了Activity的,其他Context實例的LayoutInflation,會使用系統默認的主題樣式,如果你自定義了某些樣式可能不會被使用。

總結:和UI相關的方法基本都不建議或者不可使用Application,并且,前三個操作基本不可能在Application中出現。實際上,只要把握住一點,凡是跟UI相關的,都應該使用Activity做為Context來處理;其他的一些操作,Service,Activity,Application等實例都可以,當然了,注意Context引用的持有,防止內存泄漏。

參考

Context都沒弄明白,還怎么做Android開發?

Android應用Context詳解及源碼解析

Android Context完全解析,你所不知道的Context的各種細節

Android Context 上下文 你必須知道的一切

目標

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容