Context的徹底的解析

                            Context的徹底的解析

1.Context的使用場景

咱們回想一下,你在加載資源、啓動一個新的Activity、Service、獲取系統服務、獲取內部文件(夾)路徑、建立View操做時等都須要Context的參與,可見Context的常見性。你們可能會問到底什麼是Context,Context字面意思上下文,或者叫作場景,也就是用戶與操做系統操做的一個過程,好比你打電話,場景包括電話程序對應的界面,以及隱藏在背後的數據;java

2.Context類型

咱們知道,Android應用都是使用java語言來編寫的,那麼你們能夠思考一下,一個Android程序和一個Java程序,他們最大的區別在哪裏?劃分界限又是什麼呢?其實簡單點分析,Android程序不像Java程序同樣,隨便建立一個類,寫個main()方法就能跑了,而是要有一個完整的Android工程環境,在這個環境下,咱們有像Activity、Service、BroadcastReceiver等系統組件,而這些組件並非像一個普通的Java對象new一下就能建立實例的了,而是要有它們各自的上下文環境,也就是咱們這裏討論的Context。能夠這樣講,Context是維持Android程序中各組件可以正常工做的一個核心功能類。android

下面咱們來看一下Context的繼承結構:app

Context的繼承結構仍是稍微有點複雜的,能夠看到,直系子類有兩個,一個是ContextWrapper,一個是ContextImpl。那麼從名字上就能夠看出,ContextWrapper是上下文功能的封裝類,而ContextImpl則是上下文功能的實現類。而ContextWrapper又有三個直接的子類,ContextThemeWrapper、Service和Application。其中,ContextThemeWrapper是一個帶主題的封裝類,而它有一個直接子類就是Activity。ide

那麼在這裏咱們至少看到了幾個所比較熟悉的面孔,Activity、Service、還有Application。由此,其實咱們就已經能夠得出結論了,Context一共有三種類型,分別是Application、Activity和Service。這三個類雖然分別各類承擔着不一樣的做用,但它們都屬於Context的一種,而它們具體Context的功能則是由ContextImpl類去實現的。工具

3.Activity與Application在做爲Context時的區別

你們在須要Context的時候,若是是在Activity中,大多直接傳個this,當在匿名內部類的時候,由於this不能用,須要寫XXXActivity.this,不少哥們會偷懶,直接就來個getApplicationContext。那麼你們有沒有想過,XXXActivity.this和getApplicationContext的區別呢?oop

XXXActivity和getApplicationContext返回的確定不是一個對象,一個是當前Activity的實例,一個是項目的Application的實例。既然區別這麼明顯,那麼各自的使用場景確定不一樣,亂使用可能會帶來一些問題。測試

如今咱們來介紹使用Context須要注意的問題:this

4.getApplication() VS getApplicationContext()

如今不少的Application都是被看成通用工具類來使用的,那麼既然做爲一個通用工具類,咱們要怎樣才能獲取到它的實例呢?以下所示:spa

public class MainActivity extends Activity {  
      
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
        MyApplication myApp = (MyApplication) getApplication();  
        Log.d("TAG", "getApplication is " + myApp);  
    }  
      
}

打印結果以下所示:操作系統

11-23 08:17:12.853 21364-21364/qu.com.handlerthread E/TAG: 
getApplication is qu.com.handlerthread.MyApplication@531ecb70

咱們將代碼修改一下:

public class MainActivity extends Activity {  
      
    @Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(R.layout.activity_main);  
        MyApplication myApp = (MyApplication) getApplication();  
        Log.d("TAG", "getApplication is " + myApp);  
        Context appContext = getApplicationContext();  
        Log.d("TAG", "getApplicationContext is " + appContext);  
    }  
      
}

打印狀況:

11-23 08:20:01.581 25659-25659/qu.com.handlerthread E/TAG: 
getApplication is qu.com.handlerthread.MyApplication@531ed258

11-23 08:20:01.585 25659-25659/qu.com.handlerthread E/TAG: 
getApplicationContext is qu.com.handlerthread.MyApplication@531ed258

好像打印出的結果是同樣的呀,連後面的內存地址都是相同的,看來它們是同一個對象。其實這個結果也很好理解,由於前面已經說過了,Application自己就是一個Context,因此這裏獲取getApplicationContext()獲得的結果就是MyApplication自己的實例。

那麼有的朋友可能就會問了,既然這兩個方法獲得的結果都是相同的,那麼Android爲何要提供兩個功能重複的方法呢?實際上這兩個方法在做用域上有比較大的區別。getApplication()方法的語義性很是強,一看就知道是用來獲取Application實例的,可是這個方法只有在Activity和Service中才能調用的到。那麼也許在絕大多數狀況下咱們都是在Activity或者Service中使用Application的,可是若是在一些其它的場景,好比BroadcastReceiver中也想得到Application的實例,這時就能夠藉助getApplicationContext()方法了,以下所示:

public class MyReceiver extends BroadcastReceiver {  
  
    @Override  
    public void onReceive(Context context, Intent intent) {  
        MyApplication myApp = (MyApplication) context.getApplicationContext();  
        Log.d("TAG", "myApp is " + myApp);  
    }  
  
}

總結一下:getApplicationContext()方法的做用域會更廣一些,任何一個Context的實例,只要調用getApplicationContext()方法均可以拿到咱們的Application對象。

細心的人可能有人知道還有一個getBaseContext方法,那這個方法是什麼呢?咱們在前面的基礎上加個打印:

11-23 08:24:34.253 32708-32708/qu.com.handlerthread E/TAG: 
getApplication is qu.com.handlerthread.MyApplication@531ed944

11-23 08:24:34.253 32708-32708/qu.com.handlerthread E/TAG:
 getApplicationContext is qu.com.handlerthread.MyApplication@531ed944

11-23 08:24:34.257 32708-32708/qu.com.handlerthread E/TAG:
 getBaseContext is android.app.ContextImpl@531f0758

奇怪?此次獲得的是不一樣的對象了,getBaseContext()方法獲得的是一個ContextImpl對象。這個ContextImpl是否是感受有點似曾相識?回去看一下Context的繼承結構圖吧,ContextImpl正是上下文功能的實現類。也就是說像Application、Activity這樣的類其實並不會去具體實現Context的功能,而僅僅是作了一層接口封裝而已,Context的具體功能都是由ContextImpl類去完成的。那麼這樣的設計究竟是怎麼實現的呢?咱們仍是來看一下源碼吧。由於Application、Activity、Service都是直接或間接繼承自ContextWrapper的,咱們就直接看ContextWrapper的源碼,以下所示:

/**
 * Proxying implementation of Context that simply delegates all of its calls to
 * another Context.  Can be subclassed to modify behavior without changing
 * the original Context.
 */
public class ContextWrapper extends Context {
    Context mBase;
    
    /**
     * Set the base context for this ContextWrapper.  All calls will then be
     * delegated to the base context.  Throws
     * IllegalStateException if a base context has already been set.
     * 
     * @param base The new base context for this wrapper.
     */
    protected void attachBaseContext(Context base) {
        if (mBase != null) {
            throw new IllegalStateException("Base context already set");
        }
        mBase = base;
    }

    /**
     * @return the base context as set by the constructor or setBaseContext
     */
    public Context getBaseContext() {
        return mBase;
    }

    @Override
    public AssetManager getAssets() {
        return mBase.getAssets();
    }

    @Override
    public Resources getResources() {
        return mBase.getResources();
    }

    @Override
    public ContentResolver getContentResolver() {
        return mBase.getContentResolver();
    }

    @Override
    public Looper getMainLooper() {
        return mBase.getMainLooper();
    }
    
    @Override
    public Context getApplicationContext() {
        return mBase.getApplicationContext();
    }

    @Override
    public String getPackageName() {
        return mBase.getPackageName();
    }

    @Override
    public void startActivity(Intent intent) {
        mBase.startActivity(intent);
    }
    
    @Override
    public void sendBroadcast(Intent intent) {
        mBase.sendBroadcast(intent);
    }

    @Override
    public Intent registerReceiver(
        BroadcastReceiver receiver, IntentFilter filter) {
        return mBase.registerReceiver(receiver, filter);
    }

    @Override
    public void unregisterReceiver(BroadcastReceiver receiver) {
        mBase.unregisterReceiver(receiver);
    }

    @Override
    public ComponentName startService(Intent service) {
        return mBase.startService(service);
    }

    @Override
    public boolean stopService(Intent name) {
        return mBase.stopService(name);
    }

    @Override
    public boolean bindService(Intent service, ServiceConnection conn,
            int flags) {
        return mBase.bindService(service, conn, flags);
    }

    @Override
    public void unbindService(ServiceConnection conn) {
        mBase.unbindService(conn);
    }

    @Override
    public Object getSystemService(String name) {
        return mBase.getSystemService(name);
    }

    ......
}

因爲ContextWrapper中的方法仍是很是多的,我就進行了一些篩選,只貼出來了部分方法。那麼上面的這些方法相信你們都是很是熟悉的,getResources()、getPackageName()、getSystemService()等等都是咱們常常要用到的方法。那麼全部這些方法的實現又是什麼樣的呢?其實全部ContextWrapper中方法的實現都很是統一,就是調用了mBase對象中對應當前方法名的方法。

那麼這個mBase對象又是什麼呢?咱們來看第16行的attachBaseContext()方法,這個方法中傳入了一個base參數,並把這個參數賦值給了mBase對象。而attachBaseContext()方法實際上是由系統來調用的,它會把ContextImpl對象做爲參數傳遞到attachBaseContext()方法當中,從而賦值給mBase對象,以後ContextWrapper中的全部方法其實都是經過這種委託的機制交由ContextImpl去具體實現的,因此說ContextImpl是上下文功能的實現類是很是準確的。

那麼另外再看一下咱們剛剛打印的getBaseContext()方法,在第26行。這個方法只有一行代碼,就是返回了mBase對象而已,而mBase對象其實就是ContextImpl對象,所以剛纔的打印結果也獲得了印證。

 

5.引用的保持

你們在編寫一些類時,例如工具類,可能會編寫成單例的方式,這些工具類大多須要去訪問資源,也就說須要Context的參與。

在這樣的狀況下,就須要注意Context的引用問題。

例如如下的寫法:

package com.mooc.shader.roundimageview;  
  
import android.content.Context;  
  
public class CustomManager  
{  
    private static CustomManager sInstance;  
    private Context mContext;  
  
    private CustomManager(Context context)  
    {  
        this.mContext = context;  
    }  
  
    public static synchronized CustomManager getInstance(Context context)  
    {  
        if (sInstance == null)  
        {  
            sInstance = new CustomManager(context);  
        }  
        return sInstance;  
    }  
      
    //some methods   
    private void someOtherMethodNeedContext()  
    {  
          
    }  
}

對於上述的單例,你們應該都不陌生(請別計較getInstance的效率問題),內部保持了一個Context的引用;

這麼寫是沒有問題的,問題在於,這個Context哪來的咱們不能肯定,很大的可能性,你在某個Activity裏面爲了方便,直接傳了個this;這樣問題就來了,咱們的這個類中的sInstance是一個static且強引用的,在其內部引用了一個Activity做爲Context,也就是說,咱們的這個Activity只要咱們的項目活着,就沒有辦法進行內存回收。而咱們的Activity的生命週期確定沒這麼長,因此形成了內存泄漏。

那麼,咱們如何才能避免這樣的問題呢?

有人會說,咱們能夠軟引用,嗯,軟引用,假如被回收了,你不怕NullPointException麼。

把上述代碼作下修改:

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

這樣,咱們就解決了內存泄漏的問題,由於咱們引用的是一個ApplicationContext,它的生命週期和咱們的單例對象一致。

這樣的話,可能有人會說,早說嘛,那咱們之後都這麼用不就好了,很遺憾的說,不行。上面咱們已經說過,Context和Application Context的區別是很大的,也就是說,他們的應用場景(你也能夠認爲是能力)是不一樣的,並不是全部Activity爲Context的場景,Application Context都能搞定。

下面就開始介紹各類Context的應用場景

你們注意看到有一些NO上添加了一些數字,其實這些從能力上來講是YES,可是爲何說是NO呢?下面一個一個解釋:

數字1:啓動Activity在這些類中是能夠的,可是須要建立一個新的task。通常狀況不推薦。

數字2:在這些類中去layout inflate是合法的,可是會使用系統默認的主題樣式,若是你自定義了某些樣式可能不會被使用。

數字3:在receiver爲null時容許,在4.2或以上的版本中,用於獲取黏性廣播的當前值。(能夠無視)

注:ContentProvider、BroadcastReceiver之因此在上述表格中,是由於在其內部方法中都有一個context用於使用。

6.使用Application的問題

上代碼:

public class MyApplication extends Application {  
      
    public MyApplication() {  
        String packageName = getPackageName();  
        Log.d("TAG", "package name is " + packageName);  
    }  
      
}

這是一個很是簡單的自定義Application,咱們在MyApplication的構造方法當中獲取了當前應用程序的包名,並打印出來。可是當我運行APP時就出現以下問題:

應用程序一啓動就馬上崩潰了,報的是一個空指針異常。看起來好像挺簡單的一段代碼,怎麼就會成空指針了呢?可是若是你嘗試把代碼改爲下面的寫法,就會發現一切正常了:

package qu.com.handlerthread;

import android.app.Application;
        import android.util.Log;

/**
 * Created by quguangle on 2016/11/23.
 */

public class MyApplication extends Application{
    @Override
    public void onCreate() {
        super.onCreate();
        String packName = getPackageName();
        Log.e("TAG","packName is"+packName);
    }
}

打印結果:

11-23 08:37:26.877 20574-20574/qu.com.handlerthread E/TAG: packName isqu.com.handlerthread

在構造方法中調用Context的方法就會崩潰,在onCreate()方法中調用Context的方法就一切正常,那麼這兩個方法之間到底發生了什麼事情呢?咱們從新回顧一下ContextWrapper類的源碼,ContextWrapper中有一個attachBaseContext()方法,這個方法會將傳入的一個Context參數賦值給mBase對象,以後mBase對象就有值了。而咱們又知道,全部Context的方法都是調用這個mBase對象的同名方法,那麼也就是說若是在mBase對象還沒賦值的狀況下就去調用Context中的任何一個方法時,就會出現空指針異常,上面的代碼就是這種狀況。Application中方法的執行順序以下圖所示:

Application中在onCreate()方法裏去初始化各類全局的變量數據是一種比較推薦的作法,可是若是你想把初始化的時間點提早到極致,也能夠去重寫attachBaseContext()方法,以下所示:

public class MyApplication extends Application {  
      
    @Override  
    protected void attachBaseContext(Context base) {  
        // 在這裏調用Context的方法會崩潰  
        super.attachBaseContext(base);  
        // 在這裏能夠正常調用Context的方法  
    }  
      
}

以上是咱們平時在使用Application時須要注意的一個點,下面再來介紹另一種很是廣泛的Application誤用狀況。

其實Android官方並不太推薦咱們使用自定義的Application,基本上只有須要作一些全局初始化的時候可能才須要用到自定義Application,官方文檔描述以下:

可是就個人觀察而言,如今自定義Application的使用狀況基本上能夠達到100%了,也就是咱們平時本身寫測試demo的時候可能不會使用,正式的項目幾乎所有都會使用自定義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的能力。

那麼若是真的想要提供一個獲取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;  
    }  
      
}

最後總結一下:根據上面的表格咱們重點看Activity和Application,能夠看到,和UI相關的方法基本都不建議或者不可以使用Application,而且,前三個操做基本不可能在Application中出現。實際上,只要把握住一點,凡是跟UI相關的,都應該使用Activity作爲Context來處理;其餘的一些操做,Service,Activity,Application等實例均可以,固然了,注意Context引用的持有,防止內存泄漏。

參考文章:http://blog.csdn.net/sinyu890807/article/details/47028975

http://blog.csdn.net/lmj623565791/article/details/40481055

相關文章
相關標籤/搜索