Android Context的設(shè)計思想和源碼分析(轉(zhuǎn))

本文轉(zhuǎn)自http://duanqz.github.io/2017-12-25-Android-Context
發(fā)現(xiàn)了一篇寫的很好的文章

做了好些年Android,終于可以聊一聊既熟悉又陌生的Context了褥伴,每個剛?cè)腴T的Android開發(fā)人員都會接觸到它宵晚;然而要讀懂Context的設(shè)計哲學(xué)孟岛,卻又要經(jīng)過好多輪的認(rèn)知升級人弓。很多時候狰贯,大家是感知不到Context的存在的歼跟,筆者最開始“被迫”使用Context轩性,是在自定義控件的時候声登,布局中有一個按鈕,點擊一次就發(fā)送一次廣播揣苏,其代碼片段如下所示:

public class CustomLayout extends LinearLayout implements View.OnClickListener {
    private Context mContext;
    private Button mBtnBroadcast;

    // 1\. 強制傳入Context
    public CustomLayout(Context context) {
        super(context);
        LayoutInflater.from(context).inflate(R.layout.custom_layout, this);
        mContext = context;
        mBtnBroadcast = (Button) findViewById(R.id.btn_broadcast);
        mBtnBroadcast.setOnClickListener(this);
    }

    public void onClick(View v) {
        ...
        // 2\. 通過Context發(fā)送廣播
        mContext.sendBroadcast(intent);
    }
}

之所以說“被迫”使用Context悯嗓,是因為:1. 構(gòu)造函數(shù)就強制要傳入Context,否則會導(dǎo)致編譯報錯卸察;2. 在點擊按鈕發(fā)送廣播時绅作,又必須使用Context,于是乎蛾派,又“被迫”設(shè)計一個對象屬性mContext來保存外部傳入的Context俄认。各種疑問涌上心頭:

  • 好不容易想實現(xiàn)控件的代碼解耦,為什么要把Context傳來傳去呢洪乍?
  • 為什么不能像在Activity中一樣眯杏,直接調(diào)用sendBroadcast()就可以了呢?
  • 通過Context可以調(diào)用到很多Android接口壳澳,譬如getString(), getColor(), startActivity()等等岂贩,它到底是何方神圣呢?

本文會結(jié)合Context的設(shè)計思想和源碼分析來進行解構(gòu)巷波。

1. Context的設(shè)計思想

1.1 面向應(yīng)用程序的設(shè)計

Android有意淡化進程的概念萎津,在開發(fā)一個Android應(yīng)用程序時,通常都不需要關(guān)心目標(biāo)對象運行在哪個進程抹镊,只需要表明意圖(Intent)锉屈,譬如撥打電話、查看圖片垮耳、打開鏈接等颈渊;也不需要關(guān)心系統(tǒng)接口是在哪個進程實現(xiàn)的,只需要通過Context發(fā)起調(diào)用终佛。對于一個Android應(yīng)用程序而言俊嗽,Context就像運行環(huán)境一樣,無處不在铃彰。有了Context绍豁,一個Linux進程就搖身一變,成為了Android進程牙捉,變成了Android世界的公民竹揍,享有Android提供的各種服務(wù)敬飒。那么,一個Android應(yīng)用程序需要一些什么服務(wù)呢鬼佣?

  • 獲取應(yīng)用資源驶拱,譬如:drawable霜浴、string晶衷、assets
  • 操作四大組件,譬如:啟動界面阴孟、發(fā)送廣播晌纫、綁定服務(wù)、打開數(shù)據(jù)庫
  • 操作文件目錄永丝,譬如:獲取/data/分區(qū)的數(shù)據(jù)目錄锹漱、獲取sdcard目錄
  • 檢查授予權(quán)限,譬如:應(yīng)用向外提供服務(wù)時慕嚷,可以判定申請者是否具備訪問權(quán)限
  • 獲取其他服務(wù)哥牍,有一些服務(wù)有專門的提供者,譬如:包管理服務(wù)喝检、Activity管理服務(wù)嗅辣、窗口管理服務(wù)

在應(yīng)用程序中,隨處都可訪問這些服務(wù)挠说,這些服務(wù)的訪問入口就是Context澡谭。Android對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.

其意為:Context是一個抽象類,提供接口损俭,用于訪問應(yīng)用程序運行所需服務(wù)蛙奖,譬如啟動Activity、發(fā)送廣播杆兵、接受Intent等雁仲。Android是構(gòu)建在Linux之上的,然而對于Android的應(yīng)用開發(fā)者而言琐脏,已經(jīng)不需要關(guān)心Linux進程的概念了伯顶,正是因為有了Context,為應(yīng)用程序的運行提供了一個Android環(huán)境骆膝,開發(fā)者只需要關(guān)心Context提供了哪些接口祭衩。

1.2 關(guān)于Decorator設(shè)計模式

在類的世界里面,要為一個類增加新的功能阅签,最直接的方式就是繼承掐暮,子類可以基于父類進行擴展。然而政钟,當(dāng)要增加的功能維度有很多路克,并且功能相互疊加的時候樟结,要擴展的子類會變得非常多。

舉個例子精算,基類是衣服瓢宦,需求是分別生產(chǎn)防水、透氣和速干的三個不同功能的衣服灰羽,便會擴展出三個子類:防水衣服驮履、透氣衣服和速干衣服。如果又有新的需求廉嚼,即防水又速干玫镐,便會擴展出一個新的子類:防水速干衣服。然后怠噪,又有新的需求恐似,即保暖又速干,這可能會擴展出兩個子類:保暖衣服和保暖速干衣服傍念。長此以往矫夷,市場需求不斷變化,要擴展的類就會越來越多憋槐。

在GOF設(shè)計模式里面双藕,把繼承看成靜態(tài)的類擴展,擴展功能的增多會導(dǎo)致子類膨脹秦陋。為了有效緩解這種情況蔓彩,便產(chǎn)生了動態(tài)的類擴展:修飾器模式(Decorator Pattern),先上UML圖:

[圖片上傳失敗...(image-91dff-1533784477310)]

Decorator就是所謂的修飾器驳概,包裝了(Wrapper)一個Component類型對象赤嚼,修飾器可以在已有Component的基礎(chǔ)上,增加新的屬性(addedState)行為(addedBehavior)顺又,從而形成不同的ConcreteDecorator更卒。Decorator通過包裝的手段呀袱,在外圍擴展了Component的功能骑素。 說到這里,讀者們一定心生詫異第焰,實現(xiàn)不同擴展功能的ConcreteDecorator果录,還是得繼承實現(xiàn)多個不同子類吧险怼!確實如此弱恒,擴展不同維度的功能需要實現(xiàn)不同的子類辨萍,但要實現(xiàn)這些功能的組合,卻不需要新的子類了返弹,因為一個修飾器可以修飾另外一個修飾器锈玉,通過修飾器的疊加便可實現(xiàn)功能的組合爪飘。

還是上面的例子,基類是衣服拉背,有三個修飾器:防水师崎、透氣和速干,在這三個修飾器的包裝下椅棺,便可生成三種不同的衣服:防水衣服犁罩、透氣衣服和速干衣服。如果又有新需求土陪,即防水又速干昼汗,只需要在防水衣服上再疊加一個速干修飾器肴熏,便生成了防水速干衣服鬼雀。然后,又有新需求蛙吏,即保暖又速干源哩,這時,只需要增加一個修飾器:保暖鸦做,將這個修飾器疊加到速干衣服上励烦,便可生成保暖速干衣服。這樣一來泼诱,便能有效緩解類的膨脹坛掠。

理解Decorator模式,有助于大家理解Context類簇的設(shè)計治筒,前文說過Context是一個抽象類屉栓,圍繞Context還有很多實現(xiàn)類,這些類的結(jié)構(gòu)設(shè)計就是Decorator模式耸袜。

1.3 Context類簇的設(shè)計

先奉上Context類簇的類圖如下:

image

一個典型的Decorator模式友多,基類Context定義了各種接口,ContextImpl負(fù)責(zé)實現(xiàn)接口的具體功能堤框。對外提供使用時域滥,ContextImpl需要被包裝(Wrapper)一下,這就有了ContextWrapper這個修飾器蜈抓。修飾器一般只是一個傳遞者启绰,修飾器所有的方法實現(xiàn)都是調(diào)用具體的實現(xiàn)類ContextImpl,所以修飾器ContextWrapper需要持有一個ContextImpl的引用沟使。

修飾器存在的價值是為了擴展類的功能委可,Context已經(jīng)提供了豐富的系統(tǒng)功能,但仍不能滿足最終應(yīng)用程序編程的需要格带,因此Android又?jǐn)U展了一些修飾器撤缴,包括Application刹枉、ActivityService∏唬虎軀一震微宝,這些東西竟然就是Context,原來Context真的是無處不在盎⒄!蟋软!在Activity中調(diào)用startActivity啟動另外的界面,原來就是通過父類Context發(fā)起的調(diào)用嗽桩!

Application擴展了應(yīng)用程序的生命周期岳守,Activity擴展了界面顯示的生命周期,Service擴展了后臺服務(wù)的生命周期碌冶,它們在父類Context的基礎(chǔ)上進行了不同維度的擴展湿痢,同時也仍可以將它們作為Context使用,這可以解釋很多Applicaiton扑庞、Activity和Service的使用方式譬重,但很多問題也隨之而來:

  • 為什么四大組件的另外兩個BroadcastReceiverContentProvider不是Context的子類呢?
  • 為什么Application罐氨、Activity和Service不直接繼承ContextImpl呢臀规,不是更直接嗎?所謂的Decorator模式栅隐,也沒看到有多大實際用處八摇?

看一下ContentProvider的構(gòu)造函數(shù)和BroadcastReceiver.onReceive()函數(shù):

// ContentProvider
public ContentProvider(
        Context context,
        String readPermission,
        String writePermission,
        PathPermission[] pathPermissions) {
    mContext = context;
    mReadPermission = readPermission;
    mWritePermission = writePermission;
    mPathPermissions = pathPermissions;
}

// BroadcastReceiver
public abstract void onReceive(Context context, Intent intent);

ContentProvider和BroadcastReceiver都需要把Context作為參數(shù)傳入租悄,雖然它們不繼承于Context谨究,但它們都依賴于Context,換個角度看:它們就是修飾器恰矩,包裝了Context记盒。因為這兩個組件在使用上與Activity和Service存在較大的區(qū)別,所以它們的實現(xiàn)方式存在較大差異外傅。

往深一步理解纪吮,Decorator模式的優(yōu)勢也體現(xiàn)出來了,譬如Application萎胰、Activity和Service都可以作為BroadcastReceiver的載體碾盟,只需要通過它們各自的Context去注冊廣播接收器就可以了,將BroadcastReceiver修飾在它們之上技竟,就形成了新的功能擴展冰肴,而不是去擴展一個可以接收廣播的Applicaiton、Activity或Service類。

題外話熙尉,Decorator模式在Android中隨處可見联逻,除了Context類簇,還有Window類簇检痰。

至此包归,我們已經(jīng)領(lǐng)會了Context的設(shè)計思想,Context無處不在铅歼,它是應(yīng)用進程與系統(tǒng)對話的一個接口:從使用的角度公壤,更是可以將Context理解為應(yīng)用進程的Android運行環(huán)境,想要什么資源椎椰,都可以向Context索认梅;從實現(xiàn)的角度慨飘,Context類簇利用Decorator設(shè)計模式确憨,Android最核心的四大組件都可以理解為“修飾器”,它們從不同的功能維度擴展了Context的功能套媚。

2. Context的源碼分析

Context本身作為一個最高層的抽象類缚态,僅僅是定義接口磁椒,方法的實現(xiàn)都在ContextImpl中堤瘤。因為Context是為應(yīng)用程序設(shè)計的,筆者試圖通過兩條主線來滲透Context的各項知識點:

  • 第一條主線:應(yīng)用程序Application的Context構(gòu)建過程
  • 第二條主線:應(yīng)用界面Activity的Context構(gòu)建過程

2.1 Application的Context的構(gòu)建過程

應(yīng)用進程與系統(tǒng)進程之間的通信一文中浆熔,介紹過應(yīng)用進程啟動時本辐,需要和系統(tǒng)進程進行通信:

  • 當(dāng)應(yīng)用進程在初始化自己的主線程ActivityThread時,便會發(fā)起跨進程調(diào)用IActivityManager.attachApplication()医增,告訴系統(tǒng)進程(SystemServer):我已經(jīng)在Linux的世界誕生了慎皱,現(xiàn)在需要增加Android的屬性(應(yīng)用的包信息、四大組件信息叶骨、Android進程名等)茫多,才能成為一個真正的Android進程。

  • 系統(tǒng)進程在進行包解析時忽刽,就獲取了所有應(yīng)用程序的靜態(tài)信息天揖。在AMS中執(zhí)行一個應(yīng)用進程的attachApplication()時,便會將這些信息的數(shù)據(jù)結(jié)構(gòu)準(zhǔn)備好跪帝,發(fā)起跨進程調(diào)用IApplicationThread.bindApplication()今膊,傳送應(yīng)用進程啟動所必需的信息。

經(jīng)過以上的交互伞剑,應(yīng)用進程就進入ActivityThread.handleBindApplication()斑唬,開始構(gòu)建自己所需的Android環(huán)境了:

image

從時序圖的第一個函數(shù)開始分析:

// ActivityThread.handleBindApplication()
private void handleBindApplication(AppBindData data) {
    ...
    Application app = data.info.makeApplication(data.restrictedBackupMode, null);
    mInitialApplication = app;
    ...
    try {
        mInstrumentation.callApplicationOnCreate(app);
    } catch (Exception e) {...}
}

ActivityThread.handleBindApplication()完成大量變量的初始化后,便開始創(chuàng)建一個Application類型的對象了,有了這個對象后便開始調(diào)用其onCreate()方法恕刘,就進入到了大家非常熟悉的一個系統(tǒng)回調(diào)函數(shù)Application.onCreate()缤谎。該函數(shù)片段的關(guān)鍵點是調(diào)用LoadedApk.makeApplication()創(chuàng)建Application對象,data.info是之前調(diào)用ActivityThread.getPackageInfoNoCheck()生成的LoadedApk對象褐着,表示一個已經(jīng)加載解析過的APK文件弓千。

// LoadedApk.makeApplication()
public Application makeApplication(boolean forceDefaultAppClass, Instrumentation instrumentation) {
    ...
    if (forceDefaultAppClass || (appClass == null)) {
        appClass = "android.app.Application";
    }
    try {
        java.lang.ClassLoader cl = getClassLoader();
        ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
        app = mActivityThread.mInstrumentation.newApplication(
                cl, appClass, appContext);
        appContext.setOuterContext(app);
    } catch (Exception e) {...}
    ...
}

該函數(shù)片段中,創(chuàng)建了一個ClassLoader對象和ContextImpl對象献起,連同將要構(gòu)建的Application類名appClass洋访,一起作為參數(shù)傳送給Instrumentation.newApplication()方法,可想而知谴餐,最終的Application對象是反射構(gòu)建的姻政。

// ContextImpl.createAppContext()
static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
    if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
    return new ContextImpl(null, mainThread,
            packageInfo, null, null, 0, null, null, Display.INVALID_DISPLAY);
}

// ContextImpl.constructor()
private ContextImpl(ContextImpl container, ActivityThread mainThread,
        LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags,
        Display display, Configuration overrideConfiguration, int createDisplayWithId) {
    mOuterContext = this; // 外圍包裝器,暫時用
    ...
    mMainThread = mainThread; // 主線程
    mActivityToken = activityToken; // 關(guān)聯(lián)到系統(tǒng)進程的ActivityRecord
    mFlags = flags;
    ...
    mPackageInfo = packageInfo; // LoadedApk對象
    mResourcesManager = ResourcesManager.getInstance();
    ...
    Resources resources = packageInfo.getResources(mainThread);
    ...
    mResources = resources; // 通過各種計算得到的資源
    ...
    mContentResolver = new ApplicationContentResolver(this, mainThread, user); // 訪問ContentProvider的接口
}

ContextImpl有三種不同的類型:

  • SystemContext:系統(tǒng)進程SystemServer的Context
  • AppContext:應(yīng)用進程的Context
  • ActivityContext:Activity的Context岂嗓,只有ActivityContext跟界面顯示相關(guān)汁展,需要傳入activityToken和有效的DisplayId

該函數(shù)片段是創(chuàng)建一個AppContext,要初始化的屬性其實不多厌殉,需要特別注意的是:Context中會初始化一個ContentResovler對象食绿,所以,可以通過Context操作數(shù)據(jù)庫公罕。Context創(chuàng)建完畢后器紧,會作為參數(shù)傳遞給Instrumentation對象去構(gòu)建一個Application對象:

// Instrumentation.newApplication()
static public Application newApplication(Class<?> clazz, Context context)
        throws InstantiationException, IllegalAccessException, ClassNotFoundException {
    Application app = (Application)clazz.newInstance();
    app.attach(context);
    return app;
}

// Application.attach()
final void attach(Context context) {
    attachBaseContext(context);
    mLoadedApk = ContextImpl.getImpl(context).mPackageInfo;
}

// ContextWrapper.attachBaseContext()
protected void attachBaseContext(Context base) {
    if (mBase != null) {
        throw new IllegalStateException("Base context already set");
    }
    mBase = base;
}

Application對象是通過反射構(gòu)建的,如果應(yīng)用程序沒有繼承實現(xiàn)Application楼眷,則默認(rèn)使用android.app.Application這個包名進行反射铲汪。構(gòu)建Application對象完成后,便會調(diào)用其attach()函數(shù)綁定一個Context罐柳,這一綁定就相當(dāng)于在ContextWrapper中關(guān)聯(lián)了一個ContextImpl掌腰,這一層Decorator的修飾包裝關(guān)系這就么套上了。

回顧一下時序圖张吉,ActivityThread中發(fā)起Application對象的創(chuàng)建操作齿梁,然后創(chuàng)建一個真實的ContextImpl對象(AppContext),最后將AppContext包裝進Application對象中肮蛹,才完成整個的修飾動作勺择,在這之后,Application便可作為一個真正的Context使用蔗崎,可以回調(diào)其生命周期的onCreate()方法了酵幕。

2.2 Activity的Context構(gòu)建過程

Activity的啟動過程一文中,介紹過一個Activity是如何從無到有缓苛,再到顯示狀態(tài)的芳撒,這個過程極其復(fù)雜邓深。本節(jié)將聚焦在Activity的Context構(gòu)建時機,當(dāng)要啟動一個Activity時笔刹,ActivityThread.performLaunchActivity()會被調(diào)用芥备,從這以后便會開始構(gòu)建Activity對象,時序圖如下:

image
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    ...
    try {
        java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
        // 1\. 反射構(gòu)建Activity對象
        activity = mInstrumentation.newActivity(
                cl, component.getClassName(), r.intent);
        StrictMode.incrementExpectedActivityCount(activity.getClass());
        r.intent.setExtrasClassLoader(cl);
        r.intent.prepareToEnterProcess();
        if (r.state != null) {
            r.state.setClassLoader(cl);
        }
    } catch (Exception e) {...}

    try {
        // 2\. 獲取已有的Application對象
        Application app = r.packageInfo.makeApplication(false, mInstrumentation);
        if (activity != null) {
            // 3\. 創(chuàng)建Activity的Context
            Context appContext = createBaseContextForActivity(r, activity);
            CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
            Configuration config = new Configuration(mCompatConfiguration);
            ...
            // 4\. 將Context包裝進Activity
            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, window);
            ...
            // 5\. 調(diào)用Activity.onCreate()
            if (r.isPersistable()) {
                    mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
                } else {
                    mInstrumentation.callActivityOnCreate(activity, r.state);
            }
        }
    } catch (Exception e) {...}
    ...
}

該函數(shù)片段完成了從一個Activity對象構(gòu)建到Activity.onCreate()函數(shù)被回調(diào)的過程:

  1. 調(diào)用Instrumentation.newActivity()函數(shù)舌菜,傳入這里通過ClassLoader和Activity的類名萌壳,反射構(gòu)建一個Activity對象。

  2. 獲取已有的Application日月。LoadedApk.makeApplication()這個函數(shù)在前文分析過袱瓮,當(dāng)已有創(chuàng)建了一個Application時,會直接返回爱咬。

  3. 調(diào)用ActivityThread.createBaseContextForActivity()函數(shù)尺借,該函數(shù)內(nèi)部會繼續(xù)調(diào)用ContextImpl.createActivityContext(),創(chuàng)建一個ActivityContext精拟。此處不再展開分析這兩個函數(shù)燎斩,ContextImpl對象的初始化過程與上節(jié)中一致,請讀者自行參考蜂绎。

  4. 以上過程都可以理解為在準(zhǔn)備參數(shù)栅表,真正將Context包裝進Activity是調(diào)用Activity.attach()函數(shù)完成的,這個函數(shù)我們在分析Activity與Window的關(guān)系時师枣,還會重點介紹怪瓶。此處只需要理解其中一行,就是調(diào)用ContextWrapper.attachBaseContext()函數(shù)坛吁,將之前創(chuàng)建的ContextImpl對象包裝到ContextWrapper中:

     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,
             Window window) {
         attachBaseContext(context);
         ...
     }
    
    
  5. Activity的Context構(gòu)建完成后劳殖,便可以回調(diào)大家熟悉的Activity.onCreate()函數(shù)了。

可見拨脉,Activity和Application的Context構(gòu)建過程極為相似,這也是符合邏輯的宣增,因為它們本質(zhì)上就是Context玫膀,只不過功能維度不同。其實Service的Context構(gòu)建過程也很相似爹脾。無非都是構(gòu)建ContextImpl對象帖旨,構(gòu)建Decorator對象(Application、Activity和Service)灵妨,再將Decorator對象包裝到ContextImpl對象之上解阅。

3. Context的注意事項

3.1 不同類型Context的區(qū)別

從前文中可以看出,Android中有好幾種不同的Context泌霍,在一個Activity中货抄,就可以獲取到以下幾種Context:

  • getApplication()
  • getApplicationContext()
  • getBaseContext()
  • Activity.this

它們都什么區(qū)別呢?分別在什么時候使用呢?可以在Activity中通過以下代碼蟹地,將這個Context分別打印出來:

// 在Activity.onCreate()中插入以下代碼:
Log.i(TAG, "Application: " + getApplication());
Log.i(TAG, "ApplicationContext: " + getApplicationContext());
Log.i(TAG, "Activity: " + this);
Log.i(TAG, "ActivityContext:" + this);
Log.i(TAG, "Application BaseContext: " + getApplication().getBaseContext());
Log.i(TAG, "Activity BaseContext: " + getBaseContext());

// 得到的運行結(jié)果:
I MainActivity: Application: com.duanqz.github.DemoApp@cf8644e
I MainActivity: ApplicationContext: com.duanqz.github.DemoApp@cf8644e
I MainActivity: Activity: com.duanqz.github.MainActivity@bbcadec
I MainActivity: Activity Context: com.duanqz.github.MainActivity@bbcadec
I MainActivity: Application BaseContext: android.app.ContextImpl@6a6a96f
I MainActivity: Activity BaseContext: android.app.ContextImpl@770267

可以看到积暖,有以下幾點不同:

  • getApplication()和getApplicationContext()返回的是同一個對象com.duanqz.github.DemoApp@cf8644e,雖然同一塊內(nèi)存區(qū)域怪与,但對象的類型不同:前者是Application夺刑,后者是Context。Java是強類型的語言分别,Application到Context相當(dāng)于向上轉(zhuǎn)型遍愿,會丟失掉一些接口的訪問入口。

  • 同理耘斩,Activity和Activity Context也是同一個對象错览,不同的類型。

  • Application和Activity的Base Context都是ContextImpl對象煌往,正是這個Context真正的實現(xiàn)類倾哺,被外圍的修飾器包裝了一下,才形成不同功能的類刽脖。

3.2 Context導(dǎo)致的內(nèi)存泄露問題

Context經(jīng)常會被作為參數(shù)傳遞羞海,很容易導(dǎo)致內(nèi)存泄露。以下代碼片段是一個很常見的Activity泄露問題:

public class MainActivity extends AppCompatActivity {
    protected void onCreate(Bundle savedInstanceState) {
       ... // Other codes
       Singleton.get(this);
    }
}

public class Singleton {
    private static Singleton sMe;
    private Singleton(Context context) {
        // Do something with context
    }

    public static synchronized Singleton get(Context context) {
        if (sMe == null) {
            sMe = new Singleton(context);
        }
        return sMe;
    }
}

本例中曲管,有一個需要Context才能初始化的單例却邓,在Activity中使用了這個單例,并且傳入的Context是Activity對象院水。這種使用方式很可能導(dǎo)致Activity的泄露腊徙,因為MainActivity對象在應(yīng)用進程的生命周期中可能會存在多個(譬如:多次進入/退出MainActivity界面、橫豎屏切換都可能導(dǎo)致Activity對象的創(chuàng)建和銷毀)檬某,但單例卻是存在于整個應(yīng)用進程的生命周期的撬腾,Activity作為Context傳送給單例,會導(dǎo)致Activity銷毀后恢恼,其對象不能被垃圾回收民傻,這樣一來Activity對象就泄露了。

往深一點說:單例的實現(xiàn)都包含一個靜態(tài)變量场斑,而在Java的垃圾回收機制中漓踢,靜態(tài)變量是GC ROOT,某對象只要存在到達(dá)GC ROOT的路徑漏隐,就不會被回收喧半。其實,所謂的內(nèi)存泄露青责,都是生命周期短的對象沒有被正確的回收挺据,之所以沒有被回收取具,是因為它們處在到GC ROOT的路徑上,像靜態(tài)變量吴菠、類加載器等都是GC ROOT者填,在使用過程如果關(guān)聯(lián)到了生命周期短的對象,而且沒有及時解除關(guān)聯(lián)做葵,就會產(chǎn)生內(nèi)存泄露占哟。

寫過Android代碼的朋友都知道,在單例中使用Context是一種剛需酿矢,那怎樣才能解決內(nèi)存泄露的問題呢榨乎?其實,只要傳入一個生命周期長的Context就可以瘫筐,自然就想到了與應(yīng)用程序生命周期一致的ApplicationContext:

public class MainActivity extends AppCompatActivity {
    protected void onCreate(Bundle savedInstanceState) {
       ... // Other codes
       Singleton.get(getApplicationContext());
    }
}

如此一來蜜暑,就將單例與Application綁定,解決了Activity泄露的問題策肝。這個案例提醒大家ApplicationContext和ActivityContext的使用場景是不同的:

  • 使用ApplicationContext的場景:Context的生命周期超出Activity或Service的生命周期時肛捍,譬如工具類
  • 使用ActivityContext的場景:Context的生命周期小于Activity,譬如初始化Activity的子控件之众、彈出對話框

4. 總結(jié)

本文從設(shè)計拙毫、源碼和使用三個方面剖析了Android Context這一重要的概念,它是應(yīng)用程序訪問Android資源的接口棺禾,它是應(yīng)用進程的運行環(huán)境缀蹄,它是四大組件的基礎(chǔ),它是開發(fā)者既熟悉的陌生人膘婶。Context采用Decorator模式這一頂層設(shè)計缺前,其對象構(gòu)建/銷毀都和四大組件緊密相關(guān),稍有使用不當(dāng)悬襟,便會導(dǎo)致內(nèi)存泄露衅码。

相信各位開發(fā)者在讀完本文后,會對Context有一個更加深刻的認(rèn)識古胆。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末肆良,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子逸绎,更是在濱河造成了極大的恐慌,老刑警劉巖夭谤,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件棺牧,死亡現(xiàn)場離奇詭異,居然都是意外死亡朗儒,警方通過查閱死者的電腦和手機颊乘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門参淹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人乏悄,你說我怎么就攤上這事浙值。” “怎么了檩小?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵开呐,是天一觀的道長。 經(jīng)常有香客問我规求,道長筐付,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任阻肿,我火速辦了婚禮瓦戚,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘丛塌。我一直安慰自己较解,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布赴邻。 她就那樣靜靜地躺著印衔,像睡著了一般。 火紅的嫁衣襯著肌膚如雪乍楚。 梳的紋絲不亂的頭發(fā)上当编,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機與錄音徒溪,去河邊找鬼忿偷。 笑死,一個胖子當(dāng)著我的面吹牛臊泌,可吹牛的內(nèi)容都是我干的鲤桥。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼渠概,長吁一口氣:“原來是場噩夢啊……” “哼茶凳!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起播揪,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤贮喧,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后猪狈,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體箱沦,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年雇庙,在試婚紗的時候發(fā)現(xiàn)自己被綠了谓形。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片灶伊。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖寒跳,靈堂內(nèi)的尸體忽然破棺而出聘萨,到底是詐尸還是另有隱情,我是刑警寧澤童太,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布米辐,位于F島的核電站,受9級特大地震影響康愤,放射性物質(zhì)發(fā)生泄漏儡循。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一征冷、第九天 我趴在偏房一處隱蔽的房頂上張望择膝。 院中可真熱鬧,春花似錦检激、人聲如沸肴捉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽齿穗。三九已至,卻和暖如春饺律,著一層夾襖步出監(jiān)牢的瞬間窃页,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工复濒, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留脖卖,地道東北人。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓巧颈,卻偏偏與公主長得像畦木,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子砸泛,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,792評論 2 345

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,510評論 25 707
  • Android插件化基礎(chǔ)的主要內(nèi)容包括 Android插件化基礎(chǔ)1-----加載SD上APKAndroid插件化基...
    隔壁老李頭閱讀 4,578評論 2 35
  • 一天工作過去了十籍,她把所有戒指收回柜子里后就下班了。她沒有選擇搭地鐵唇礁,而是選擇了公交車勾栗,盡管坐公交車回到家要一個半鐘...
    雨田君啊雨田君閱讀 144評論 0 0
  • 面對微信朋友圈各種婊,真希望微信推出匿名評論這一功能盏筐,也許能讓那些婊們(尤其那種各種變相自拍婊械姻,打著各種幌子在自拍...
    土疙蛋閱讀 362評論 0 0
  • 你就像茫茫大漠里 唯一矗立的白楊樹 帶給我無限的寬慰和希望 我一步步走向你 靠近你 輕撫著你身上每一處細(xì)紋 似乎能...
    CrazyTuring閱讀 215評論 0 0