【Android 源碼解析】Context 及其子類

一氓涣、Context 類繼承關(guān)系

Context 類繼承關(guān)系

上圖描述了 Context 類的主要繼承關(guān)系劳吠,Context 是抽象類,提供了應(yīng)用環(huán)境全局信息的接口痒玩;Context 有兩個(gè)直接子類 ContextWrapper 和 ContextImpl凰荚,顧名思義褒脯,ContextWrapper 是上下文功能的封裝類,ContextImpl 是上下文功能的實(shí)現(xiàn)類到涂;Applicaiton 和 Service 直接繼承自 ContextWrapper 類颁督,Activity 繼承自 ContextThemeWrapper 類沉御,因?yàn)?Activity 提供 UI 顯示,需要有主題。

  • Context 類提供了一組通用的 API烂完;
  • ContextImpl 實(shí)現(xiàn)了 Context 所有的功能抠蚣,為 Activity 等應(yīng)用組件提供 Context 對象履澳;
  • ContextWrapper 包含一個(gè)真正的 ContextImpl 的引用 mBase,然后就是 ContextImpl 的裝飾者模式柄冲;
  • ContextThemeWrapper 內(nèi)部包含了 Theme 相關(guān)的接口忠蝗,即 android:theme 指定的屬性;

二长赞、Context 的數(shù)量及作用域

Context 數(shù)量 = Activity 數(shù)量 + Service 數(shù)量 + 1(Application 數(shù)量)


Context 作用域

由于 Context 的具體實(shí)例是由 ContextImpl 類去實(shí)現(xiàn)的得哆,因此在絕大多數(shù)場景下哟旗,Activity闸餐、Service 和 Application 這三種類型的 Context 都是可以通用的。
不過有幾種場景比較特殊:

  1. Dialog 則必須在一個(gè) Activity 上面彈出(除非是 System Alert類型的 Dialog)近上,因此在這種場景下拂铡,我們只能使用 Activity 類型的 Context,否則將會出錯(cuò)斗锭。
  2. 非 Activity 類型的 Context 并沒有所謂的任務(wù)棧失球,所以待啟動(dòng)的 Activity 就找不到棧了。解決這個(gè)問題的方法就是為待啟動(dòng)的 Activity 指定 FLAG_ACTIVITY_NEW_TASK 標(biāo)記位豺撑,這樣啟動(dòng)的時(shí)候就為它創(chuàng)建一個(gè)新的任務(wù)棧前硫,而此時(shí) Activity 是以 singleTask 模式啟動(dòng)的。所有這種用 Application 啟動(dòng) Activity 的方式不推薦使用阶剑,Service 同 Application危号。
  3. 在 Application 和 Service 中去 layout inflate 也是合法的,但是會使用系統(tǒng)默認(rèn)的主題樣式猪半,如果你自定義了某些樣式可能不會被使用偷线。所以這種方式也不推薦使用声邦。

三、獲取 Context 對象

  1. View 的 getContext 方法亥曹,返回 View 的 Context媳瞪,通常是當(dāng)前正在展示的 Activity 的對象,通常在自定義 View 時(shí)會用到句葵;
  2. Activity.this 返回當(dāng)前 Activity 實(shí)例龙巨,如果是 UI 控件需要使用 Activity 作為 Context 對象旨别,但是默認(rèn)的 Toast 實(shí)際上使用 ApplicationContext 也可以汗茄;
  3. Activity 和 Service 的 getApplication 方法,返回 Application 的實(shí)例递览;
  4. getApplicationContext 方法,獲取當(dāng)前應(yīng)用的 Context 對象镜雨,也就是 Application 的實(shí)例儿捧,與上一個(gè)方法不同的是依附的對象不同菲盾;

四、Context 引起的內(nèi)存泄漏

一般 Context 造成的內(nèi)存泄漏都是 Activity 被銷毀后還被其它對象強(qiáng)引用也就是長生命周期對象持有短生命周期對象的引用而造成 Activity 不能被 GC 回收而導(dǎo)致的诡挂。
造成內(nèi)存泄漏的原因主要有兩個(gè):

  1. 靜態(tài)的 Activity Context 或任何包含 Activity Context 的對象(如 View)沒有在該 Activity 銷毀的時(shí)候置空引用临谱;
  2. 非靜態(tài)內(nèi)部類或匿名內(nèi)部類持有外部類的引用,在 Activity 銷毀后依然執(zhí)行任務(wù)悉默;

常見的場景如下:

static 直接或間接修飾 Activity Context

解決方法:

  1. 在任何時(shí)候都不建議用 static 修飾 Activity麦牺,因?yàn)?static 變量和應(yīng)用的存活時(shí)間是一樣的,這樣 Activity 生命周期結(jié)束時(shí)魏颓,引用仍然被持有吱晒;
  2. 在適當(dāng)?shù)牡胤奖热?onDestroy 方法中置空 Activity Context 的引用;
  3. 使用軟引用解決叹话,確保在 Activity 銷毀時(shí)驼壶,垃圾回收機(jī)制可以將其回收喉酌。
單例引用 Activity Context

解決方法:

  1. 使用 Applicaion Context 代替 Activity Context 泵喘;
  2. 在調(diào)用的地方使用弱引用
非靜態(tài)或匿名內(nèi)部類執(zhí)行耗時(shí)任務(wù)

非靜態(tài)內(nèi)部類或匿名內(nèi)部類都會擁有所在外部類的引用
解決方法:
用 static 修飾內(nèi)部類或者匿名內(nèi)部類所在的方法纪铺;

靜態(tài)內(nèi)部類持有外部類靜態(tài)成員變量

雖然靜態(tài)內(nèi)部類的生命周期和外部類無關(guān)碟渺,但是如果在內(nèi)部類中想要引入外部成員變量的話苫拍,這個(gè)成員變量必須是靜態(tài)的了,這也可能導(dǎo)致內(nèi)存泄露浆洗。
解決方法:
使用弱引用

Handler 引起的內(nèi)存泄漏

這種情況和非靜態(tài)或匿名內(nèi)部類執(zhí)行耗時(shí)任務(wù)的原因一樣集峦。
總結(jié)一下:

  1. 當(dāng) Application 的 Context 能搞定的情況下塔淤,并且生命周期長的對象,優(yōu)先使用 Application 的 Context聪黎。
    2:不要讓生命周期長于 Activity 的對象持有到 Activity 的引用备恤。引用了要及時(shí)置空
    3:盡量不要在 Activity 中使用非靜態(tài)內(nèi)部類喉镰,因?yàn)榉庆o態(tài)內(nèi)部類會隱式持有外部類實(shí)例的引用侣姆,如果使用靜態(tài)內(nèi)部類沉噩,將外部實(shí)例引用作為弱引用持有。

五蚜厉、各種 Context 在 ActivityThread 中實(shí)例化過程源碼分析

Context 的實(shí)現(xiàn)是 ContextImpl派歌,Activity胶果、Application 和 Service 的創(chuàng)建都是在 ActivityThread 中完成的。

Activity 中 ContextImpl 實(shí)例化源碼分析

startActivity 啟動(dòng)一個(gè)新的 Activity 時(shí)系統(tǒng)會回調(diào) ActivityThread 的 handleLaunchActivity() 方法霎烙,該方法內(nèi)部會調(diào)用 performLaunchActivity() 方法去創(chuàng)建一個(gè) Activity 實(shí)例蕊连,然后回調(diào) Activity 的 onCreate() 等方法甘苍。所以 Activity 的 ContextImpl 實(shí)例化是在 ActivityThread 類的 performLaunchActivity 方法中,如下:

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        ......
            //已經(jīng)創(chuàng)建好新的activity實(shí)例
            if (activity != null) {
                //創(chuàng)建一個(gè)Context對象
                Context appContext = createBaseContextForActivity(r, activity);
                ......
                //將上面創(chuàng)建的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;
    }

上面是 performLaunchActivity 的核心代碼,在 performLaunchActivity 方法里面創(chuàng)建了 Activity 的實(shí)例囚聚,然后然后調(diào)用 createBaseContextForActivity 方法創(chuàng)建一個(gè) Context 對象顽铸,這個(gè)對象是 ContextImpl,將前面創(chuàng)建的 Context 對象傳入到 Activity 的 attach 方法中去星压。

private Context createBaseContextForActivity(ActivityClientRecord r,
            final Activity activity) {
        //實(shí)質(zhì)就是new一個(gè)ContextImpl對象鬼譬,調(diào)運(yùn)ContextImpl的有參構(gòu)造初始化一些參數(shù)    
        ContextImpl appContext = ContextImpl.createActivityContext(this, r.packageInfo, r.token);
        //特別特別留意這里E◆ぁ!贾富!
        //ContextImpl中有一個(gè)Context的成員叫mOuterContext牺六,通過這條語句就可將當(dāng)前新Activity對象賦值到創(chuàng)建的ContextImpl的成員mOuterContext(也就是讓ContextImpl內(nèi)部持有Activity)淑际。
        appContext.setOuterContext(activity);
        //創(chuàng)建返回值并且賦值
        Context baseContext = appContext;
        ......
        //返回ContextImpl對象
        return baseContext;
    }

再看 createBaseContextForActivity 方法扇住,實(shí)質(zhì)就是通過 ContextImpl 的有參構(gòu)造初始化一些參數(shù)創(chuàng)建一個(gè) ContextImpl 對象艘蹋,創(chuàng)建一個(gè) Context 對象將創(chuàng)建的 ContextImpl 賦值給 Context 變量并返回票灰。
ContextImpl 有一個(gè)成員變量 mOuterContext屑迂,通過 ContextImpl 的 setOuterContext 方法將傳進(jìn)來的 Activity 對象賦值給 mOuterContext,這樣 ContextImpl 就持有了 Activity 的引用庸汗。

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) {
        //特別特別留意這里r遣铡C劣铡!
        //與上面createBaseContextForActivity方法中setOuterContext語句類似凶掰,不同的在于:
        //通過ContextThemeWrapper類的attachBaseContext方法懦窘,將createBaseContextForActivity中實(shí)例化的ContextImpl對象傳入到ContextWrapper類的mBase變量稚配,這樣ContextWrapper(Context子類)類的成員mBase就被實(shí)例化為Context的實(shí)現(xiàn)類ContextImpl
        attachBaseContext(context);
        ......
    }

attach 里面關(guān)鍵的代碼是調(diào)用 ContextThemeWrapper 類的 attachBaseContext 方法將 createBaseContextForActivity 創(chuàng)建的 ContextImpl 實(shí)例賦值給 ContextWrapper 的 mBase 變量道川,這樣 ContextWrapper 的成員 mBase 就被實(shí)例化為 Context 的實(shí)現(xiàn)類 ContextImpl

Service中ContextImpl實(shí)例化源碼分析

startService 或者 bindService 方法創(chuàng)建一個(gè)新 Service 時(shí)就會回調(diào) ActivityThread 類的 handleCreateService() 方法完成相關(guān)數(shù)據(jù)操作:

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

        try {
            ......
            //不做過多解釋臊岸,創(chuàng)建一個(gè)Context對象
            ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
            //特別特別留意這里KЫ洹Q录肌钟哥!
            //ContextImpl中有一個(gè)Context的成員叫mOuterContext腻贰,通過這條語句就可將當(dāng)前新Service對象賦值到創(chuàng)建的ContextImpl的成員mOuterContext(也就是讓ContextImpl內(nèi)部持有Service)装诡。
            context.setOuterContext(service);

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

與實(shí)例化 Activity 的過程相似鸦采,創(chuàng)建一個(gè) ContextImpl 對象賦值給 mBase 變量渔伯,同時(shí) ContextImpl 的 mOuterContext 變量也持有 Service 的引用肄程,這里創(chuàng)建 ContextImpl 對象是用的 ContextImpl.createAppContext 方法

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

Application中ContextImpl實(shí)例化源碼分析

創(chuàng)建 Application 的過程也在 ActivityThread 類的 handleBindApplication() 方法完成相關(guān)數(shù)據(jù)操作艇肴,而 ContextImpl 的創(chuàng)建是在該方法中調(diào)運(yùn) LoadedApk 類的 makeApplication 方法中實(shí)現(xiàn):

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

參考:
Android應(yīng)用Context詳解及源碼解析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末古拴,一起剝皮案震驚了整個(gè)濱河市黄痪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌是嗜,老刑警劉巖挺尾,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件遭铺,死亡現(xiàn)場離奇詭異,居然都是意外死亡航厚,警方通過查閱死者的電腦和手機(jī)锰蓬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進(jìn)店門芹扭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來舱卡,“玉大人,你說我怎么就攤上這事矫钓。” “怎么了赵辕?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵还惠,是天一觀的道長私杜。 經(jīng)常有香客問我衰粹,道長,這世上最難降的妖魔是什么嫉晶? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮泊柬,結(jié)果婚禮上诈火,老公的妹妹穿的比我還像新娘。我一直安慰自己刀崖,他們只是感情好拍摇,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布蜂莉。 她就那樣靜靜地躺著混卵,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蚁滋。 梳的紋絲不亂的頭發(fā)上辕录,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天,我揣著相機(jī)與錄音碎赢,去河邊找鬼速梗。 笑死姻锁,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的拷窜。 我是一名探鬼主播涧黄,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼笋妥,長吁一口氣:“原來是場噩夢啊……” “哼春宣!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起躏惋,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎嚷辅,沒想到半個(gè)月后簿姨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡潦蝇,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年款熬,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片攘乒。...
    茶點(diǎn)故事閱讀 38,094評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡贤牛,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出则酝,到底是詐尸還是另有隱情殉簸,我是刑警寧澤,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布般卑,位于F島的核電站武鲁,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏蝠检。R本人自食惡果不足惜沐鼠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望叹谁。 院中可真熱鬧饲梭,春花似錦、人聲如沸焰檩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽析苫。三九已至兜叨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間衩侥,已是汗流浹背国旷。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留顿乒,地道東北人议街。 一個(gè)月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像璧榄,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子吧雹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評論 2 345

推薦閱讀更多精彩內(nèi)容