Android Context 熟悉還是陌生?

一、什么是Context
二、Context的創建時機和獲取
  1. Context的創建時機
  2. Context的獲取
三、Application使用相關問題
  1. 什么時候初始化全局變量
  2. 自定義Application?
四、Context引起的內存泄露


Android應用都是使用Java語言來編寫的,本質上也是一個對象,那么Activity可以new嗎?一個Android程序和一個Java程序,他們最大的區別在哪里?劃分界限又是什么呢?其實簡單點分析,Android程序不像Java程序一樣,隨便創建一個類,寫個main()方法就能跑了,Android應用模型是基于Activity、Service、BroadcastReceiver等組件的應用設計模式,組件的運行要有一個完整的Android工程環境,在這個環境下,這些組件并不是像一個普通的Java對象new一下就能創建實例的了,而是要有它們各自的上下文環境Context。可以這樣講,Context是維持Android程序中各組件能夠正常工作的一個核心功能類。

什么是Context

一個Activity是一個Context,一個Service也是一個Context。在程序中,我們把可以把Context理解為當前對象在程序中所處的一個環境,一個與系統交互的過程。用戶和操作系統的每一次交互都是一個場景,比如微信聊天,此時的“環境”是指聊天的界面以及相關的數據請求與傳輸,Context在加載資源、啟動Activity、獲取系統服務、創建View等操作都要參與。打電話、發短信,這些都是一個有界面的場景,還有一些沒有界面的場景,比如后臺運行的服務(Service)。一個應用程序可以認為是一個工作環境,用戶在這個環境中會切換到不同的場景,這就像一個前臺秘書,她可能需要接待客人,可能要打印文件,還可能要接聽客戶電話,而這些就稱之為不同的場景,前臺秘書可以稱之為一個應用程序。下面我們來看一下Context的繼承結構:


Context類,一個純Abstract類,有ContextImpl和ContextWrapper兩個實現類:

  • ContextWrapper包裝類
    其構造函數中必須包含一個真正的Context引用。ContextWrapper中提供了attachBaseContext()(由系統調用)方法,用于給ContextWrapper對象中指定真正的Context對象,即ContextImpl對象,調用ContextWrapper的方法都會被轉向ContextImpl的方法。
  • ContextImpl類
    上下文功能的實現類。
  • ContextThemeWrapper類
    一個帶主題的封裝類,其內部包含了與Theme相關的接口,這里所說的主題是指在AndroidManifest.xml中通過android:theme為Application元素或者Activity元素指定的主題。當然,只有Activity才需要主題,Service是不需要主題的,因為Service是沒有界面的后臺場景,所以Service直接繼承于ContextWrapper,Application同理。

總結:Context的兩個子類分工明確,其中ContextImpl是Context的具體實現類,ContextWrapper是Context的包裝類。Activity,Application,Service雖都繼承自ContextWrapper,但它們初始化的過程中都會創建ContextImpl對象,由ContextImpl實現Context中的方法。
  那么,Context到底可以實現哪些功能呢?這個就實在是太多了,彈出Toast、啟動Activity、啟動Service、發送廣播、操作數據庫等等都需要用到Context。由于Context的具體能力是由ContextImpl類去實現的,因此在絕大多數場景下,Activity、Service和Application這三種類型的Context都是可以通用的,但在使用場景上是有一些規則,以下表格中列出了各Context的使用場景:

以上表格中NO上添加了一些數字,其實這些從能力上來說是YES,但是為什么說是NO呢?下面一個一個解釋:

  • NO^1:啟動Activity在這些類中是可以的,但是需要創建一個新的task。不推薦。
    如果我們用Application Context或Service Context去啟動一個LaunchMode為standard的Activity的時候會報錯,這是因為非Activity類型的Context并沒有所謂的任務棧,所以待啟動的Activity就找不到棧了。解決這個問題的方法就是為待啟動的Activity指定FLAG_ACTIVITY_NEW_TASK標記位,這樣啟動的時候就為它創建一個新的任務棧,而此時Activity是以singleTask模式啟動的。
  • NO^2:在這些類中去layout inflate是合法的,但是會使用系統默認的主題樣式,如果你自定義了某些樣式可能不會被使用。不推薦。

所以:

  • 凡是跟UI相關的,都應使用Activity做為Context來處理;其他的一些操作,Service,Activity,Application等都可以,當然得注意Context引用的持有,防止內存泄漏。
    比如啟動Activity,還有彈出Dialog。出于安全原因的考慮,Android是不允許Activity或Dialog憑空出現的,一個Activity的啟動必須要建立在另一個Activity的基礎之上,也就是以此形成的返回棧。而Dialog則必須在一個Activity上面彈出(除非是System Alert類型的Dialog),因此在這種場景下,我們只能使用Activity類型的Context,否則將會出錯。

了解了Context,那在一個應用程序中,Context的數量又是多少呢?由以上的介紹可以知道:**Context數量 = Activity數量 + Service數量 + 1 **

Context的創建時機和獲取

1.Context的創建時機

(1)創建Application對象的時機
  每個應用程序在第一次啟動時,都會首先創建Application對象。在應用程序啟動一個Activity(startActivity)的流程中,創建Application的時機是創建handleBindApplication()方法中,該函數位于 ActivityThread.java類中,如下:

//創建Application時同時創建的ContextIml實例
  private final void handleBindApplication(AppBindData data){
      …
      ///創建Application對象
      Application app = data.info.makeApplication(data.restrictedBackupMode, null);
      …
  }
  public Application makeApplication(boolean forceDefaultAppClass, Instrumentation instrumentation) {
      …
     try {
         java.lang.ClassLoader cl = getClassLoader();
         ContextImpl appContext = new ContextImpl();    //創建一個ContextImpl對象實例
         appContext.init(this, null, mActivityThread);  //初始化該ContextIml實例的相關屬性
         ///新建一個Application對象
         app = mActivityThread.mInstrumentation.newApplication(
                 cl, appClass, appContext);
        appContext.setOuterContext(app);  //將該Application實例傳遞給該ContextImpl實例
     }
     …
 }

(2)創建Activity對象的時機
  通過startActivity()或startActivityForResult()請求啟動一個Activity時,如果系統檢測需要新建一個Activity對象時,就會回調handleLaunchActivity()方法,該方法繼而調用performLaunchActivity()方法,去創建一個Activity實例,并且回調onCreate(),onStart()方法等, 函數都位于 ActivityThread.java類 ,如下:

//創建一個Activity實例時同時創建ContextIml實例
private final void handleLaunchActivity(ActivityRecord r, Intent customIntent) {
    …
    Activity a = performLaunchActivity(r, customIntent);  //啟動一個Activity
}
private final Activity performLaunchActivity(ActivityRecord r, Intent customIntent) {
    …
    Activity activity = null;
    try {
        //創建一個Activity對象實例
        java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
        activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
    }
    if (activity != null) {
        ContextImpl appContext = new ContextImpl();      //創建一個Activity實例
        appContext.init(r.packageInfo, r.token, this);   //初始化該ContextIml實例的相關屬性
        appContext.setOuterContext(activity);            //將該Activity信息傳遞給該ContextImpl實例
        …
    }
    …
}

(3)創建Service對象的時機
  通過startService或者bindService時,如果系統檢測到需要新創建一個Service實例,就會回調handleCreateService()方法,完成相關數據操作。handleCreateService()函數位于 ActivityThread.java類,如下:

//創建一個Service實例時同時創建ContextIml實例
private final void handleCreateService(CreateServiceData data){
    …
    //創建一個Service實例
    Service service = null;
    try {
        java.lang.ClassLoader cl = packageInfo.getClassLoader();
        service = (Service) cl.loadClass(data.info.name).newInstance();
    } catch (Exception e) {
    }
    …
    ContextImpl context = new ContextImpl(); //創建一個ContextImpl對象實例
    context.init(packageInfo, null, this);   //初始化該ContextIml實例的相關屬性
    //獲得我們之前創建的Application對象信息
    Application app = packageInfo.makeApplication(false, mInstrumentation);
    //將該Service信息傳遞給該ContextImpl實例
    context.setOuterContext(service);
    …
}

另外,通過對ContextImp的分析可知,其方法的大多數操作都是直接調用其屬性mPackageInfo(該屬性類型為PackageInfo)的相關方法而來。這說明ContextImp是一種輕量級類,而PackageInfo才是真正重量級的類。而一個App里的所有ContextIml實例,都對應同一個packageInfo對象。

2.Context的獲取

(1)通常我們想要獲取Context對象,主要有以下四種方法

  • View.getContext,返回當前View對象的Context對象,通常是當前正在展示的Activity對象
  • Activity.getApplicationContext,獲取的context來自允許在應用(進程)application中的所有Activity,當你需要用到的Context超出當前Activity的生命周期時使用
  • Activity.this 返回當前的Activity實例,如果是UI控件需要使用Activity作為Context對象,但是默認的Toast實際上使用ApplicationContext也可以
  • ContextWrapper.getBaseContext()用來獲取一個ContextWrapper進行裝飾之前的Context,也就是ContextImpl對象,如果想獲取另一個可以訪問的application里面的Context時可以使用

(2)再來看看getApplication()和getApplicationContext()
  這兩個方法有什么區別呢?看看以下結果:

通過上面的代碼,可以看到它們是同一個對象。其實這個結果也很好理解,因為前面已經說過了,Application本身就是一個Context,所以這里獲取getApplicationContext()得到的結果就是Application本身的實例。那么問題來了,既然這兩個方法得到的結果都是相同的,那么Android為什么要提供兩個功能重復的方法呢?實際上這兩個方法在作用域上有比較大的區別。
  getApplication()方法的語義性非常強,一看就知道是用來獲取Application實例的,但這個方法只有在Activity和Service中才能調用。如果在一些其它的場景,比如BroadcastReceiver中也想獲得Application的實例,這時就需要借助getApplicationContext()方法了。也就是說,getApplicationContext()方法的作用域會更廣一些,任何一個Context的實例,只要調用getApplicationContext()方法都可以拿到我們的Application對象。
(3)getActivity()和getContext()

  • getActivity()返回Activity,getContext()返回Context;
  • 兩者是Fragment的方法,但Activity沒有,多數情況下兩者沒有什么區別,但新版Support Library包,Fragment不被Activity持有時,區別見這里
  • 參數是context的,可以使用getActivity() 。因為Activity間接繼承了Context,但Context不是Activity;
  • this和getContext() 并不是完全相同。在Activity類中可以使用this,因為Activity繼承自Context,但是getContext()方法不在Activity類中。

Application使用相關問題

1.什么時候初始化全局變量

在應用程序中常常會持有一個自己的Application,首先讓它繼承自系統的Application類,然后在自己的Application類中去封裝一些通用的操作。雖然Application的用法很簡單,但同時也存在著不少Application誤用的場景。Application是Context的其中一種類型,那么是否就意味著,只要是Application的實例,就能隨時使用Context的各種方法呢?做個實驗試:

方式1:
public class MyApplication extends Application {      
    public MyApplication() {  
        String packageName = getPackageName();  
        Log.d("TAG", "package name is " + packageName);  
    }     
}  
  
方式2:
public class MyApplication extends Application {      
    @Override  
    public void onCreate() {  
        super.onCreate();  
        String packageName = getPackageName();  
        Log.d("TAG", "package name is " + packageName);  
    }     
} 

這是一個非常簡單的自定義Application,以上我們分別采用了在MyApplication的構造方法和onCreate()方法中兩種方式來獲取當前應用程序的包名,并打印出來。獲取包名使用了getPackageName()方法,這個方法就是由Context提供的。那哪種方式能得到想要的結果呢?得到的結果是否又是一樣?
結果表明,方式一應用程序一啟動就立刻崩潰了,報的是一個空指針異常:


方式二運行正常:


這兩個方法之間到底發生了什么事情呢?我們重新回顧一下ContextWrapper類的源碼,ContextWrapper中有一個attachBaseContext()方法,這個方法會將傳入的一個Context參數賦值給mBase對象,之后mBase對象就有值了。而我們又知道,所有Context的方法都是調用這個mBase對象的同名方法,那么也就是說如果在mBase對象還沒賦值的情況下就去調用Context中的任何一個方法時,就會出現空指針異常,上面的代碼就是這種情況。
Application中方法的執行順序為:Application構造方法—>attachBaseContext()—>onCreate()。
Application中在onCreate()方法里去初始化各種全局變量數據是一種比較推薦的做法,但如果你想把初始化的時間提前到極致,也可以重寫attachBaseContext(),如下所示:

public class MyApplication extends Application {      
    @Override  
    protected void attachBaseContext(Context base) {  
        // 在這里調用Context的方法會崩潰  
        super.attachBaseContext(base);  
        // 在這里可以正常調用Context的方法  
    }       
} 
2.自定義Application?

其實Android官方并不太推薦我們使用自定義的Application,基本上只有需要做一些全局初始化的時候可能才需要用到自定義Application。多數項目只是把自定義Application當成了一個通用工具類,而這個功能并不需要借助Application來實現,使用單例可能是一種更加標準的方式。不過自定義Application也并沒有什么副作用,它和單例模式二選一都可以實現同樣的功能,但把自定義Application和單例模式混合到一起使用,就會出各種問題了。如下:


public class MyApplication extends Application {  
      
    private static MyApplication app;  
      
    public static MyApplication getInstance() {  
        if (app == null) {  
            app = new MyApplication();  
        }  
        return app;  
    }       
} 

就像單例模式一樣,這里提供了一個getInstance()方法,用于獲取MyApplication的實例,有了這個實例之后,就可以調用MyApplication中的各種工具方法了,然而事實卻非想的那么美好。因為我們知道Application是屬于系統組件,系統組件的實例是要由系統來去創建的,如果這里我們自己去new一個MyApplication的實例,它就只是一個普通的Java對象而已,而不具備任何Context的能力,如果想通過該對象來進行Context操作,就會發生空指針錯誤。那么如果真的想要提供一個獲取MyApplication實例的方法,比較標準的寫法又是什么樣的呢?其實這里我們只需謹記一點,Application全局只有一個,它本身就已經是單例了,無需再用單例模式去為它做多重實例保護了,代碼如下所示:

public class MyApplication extends Application {      
    private static MyApplication app;  
      
    public static MyApplication getInstance() {  
        return app;  
    }  
      
    @Override  
    public void onCreate() {  
        super.onCreate();  
        app = this;  
    }      
}  

getInstance()方法可以照常提供,但是里面不要做任何邏輯判斷,直接返回app對象就可以了,而app對象又是什么呢?在onCreate()方法中我們將app對象賦值成this,this就是當前Application的實例,那么app也就是當前Application的實例了。

Context引起的內存泄露

context發生內存泄露的話,就會泄露很多內存。這里泄露的意思是gc沒有辦法回收activity的內存,在傳遞Context時會增加對象指針的引用計數,所以基于智能指針技術的GC無法釋放相應的內存。
  當屏幕旋轉的時候,系統會銷毀當前的activity,保存狀態信息,再創建一個新的。比如我們寫了一個應用程序,它需要加載一個很大的圖片,我們不希望每次旋轉屏幕的時候都銷毀這個圖片,重新加載。實現這個要求的簡單想法就是定義一個靜態的Drawable,這樣Activity 類創建銷毀它始終保存在內存中。實現類似:

public class myActivity extends Activity {
    private static Drawable sDrawable;
    protected void onCreate(Bundle state) {
    super.onCreate(state);
 
    TextView textView = new TextView(this);
    textView.setText("Leaks are bad");
    if (sDrawable == null) {
    sDrawable = getDrawable(R.drawable.large_bitmap);
    }
    textView.setBackgroundDrawable(sDrawable);//drawable attached to a view
    setContentView(label);
  }
}

這段程序看起來很簡單,但是卻問題很大。當屏幕旋轉的時候會有內存泄漏(即gc沒法銷毀Activity)。屏幕旋轉的時系統會銷毀當前的activity,但是當drawable和view關聯后,drawable保存了view的 reference,即sDrawable保存了textView的引用,而textView保存了Activity的引用。既然Drawable不能銷毀,它所引用和間接引用的都不能銷毀,這樣系統就沒有辦法銷毀當前的Activity,于是造成了內存泄露,gc對這種類型的內存泄露是無能為力的。為了防止內存泄露,我們應該注意以下幾點:

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

推薦閱讀更多精彩內容