Android中Context解析

前言

Context在android中的作用不言而喻,當我們訪問當前應用的資源,啓動一個新的activity的時候都需要提供Context,而這個Context到底是什麼呢,這個問題好像很好回答又好像難以說清楚。從字面意思,Context的意思是“上下文”,或者也可以叫做環境、場景等,儘管如此,還是有點抽象。從類的繼承來說,Context作爲一個抽象的基類,它的實現子類有三種:Application、Activity和Service(估計這麼說,暫時不管ContextWrapper等類),那麼這三種有沒有區別呢?爲什麼通過任意的Context訪問資源都得到的是同一套資源呢?getApplication和getApplicationContext有什麼區別呢?應用中到底有多少個Context呢?本文將圍繞這些問題一一展開,所用源碼版本爲Android4.4。

什麼是Context

Context是一個抽象基類,我們通過它訪問當前包的資源(getResources、getAssets)和啓動其他組件(Activity、Service、Broadcast)以及得到各種服務(getSystemService),當然,通過Context能得到的不僅僅只有上述這些內容。對Context的理解可以來說:Context提供了一個應用的運行環境,在Context的大環境裏,應用纔可以訪問資源,才能完成和其他組件、服務的交互,Context定義了一套基本的功能接口,我們可以理解爲一套規範,而Activity和Service是實現這套規範的子類,這麼說也許並不準確,因爲這套規範實際是被ContextImpl類統一實現的,Activity和Service只是繼承並有選擇性地重寫了某些規範的實現。

Application、Activity和Service作爲Context的區別

首先,它們都間接繼承了Context,這是它們的相同點。

不同點,可以從幾個方面來說:首先看它們的繼承關係

Activity的繼承關係

Service和Application的繼承關係

通過對比可以清晰地發現,Service和Application的類繼承關係比較像,而Activity還多了一層繼承ContextThemeWrapper,這是因爲Activity有主題的概念,而Service是沒有界面的服務,Application更是一個抽象的東西,它也是通過Activity類呈現的。

下面來看一下三者在Context方面的區別

上文已經指出,Context的真正實現都在ContextImpl中,也就是說Context的大部分方法調用都會轉到ContextImpl中,而三者的創建均在ActivityThread中完成,我之前寫過一篇文章Android源碼分析-Activity的啓動過程,在文中我指出Activity啓動的核心過程是在ActivityThread中完成的,這裏要說明的是,Application和Service的創建也是在ActivityThread中完成的。下面我們看下三者在創建時是怎麼和ContextImpl相關聯的。

Activity對象中ContextImpl的創建

代碼爲ActivityThread中的performLaunchActivity方法 

  1. if (activity != null) {  
  2.     Context appContext = createBaseContextForActivity(r, activity);  
  3.     /** 
  4.      *  createBaseContextForActivity中創建ContextImpl的代碼 
  5.      *  ContextImpl appContext = new ContextImpl(); 
  6.      *  appContext.init(r.packageInfo, r.token, this); 
  7.      *  appContext.setOuterContext(activity); 
  8.      */  
  9.     CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());  
  10.     Configuration config = new Configuration(mCompatConfiguration);  
  11.     if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity "  
  12.             + r.activityInfo.name + " with config " + config);  
  13.     activity.attach(appContext, this, getInstrumentation(), r.token,  
  14.             r.ident, app, r.intent, r.activityInfo, title, r.parent,  
  15.             r.embeddedID, r.lastNonConfigurationInstances, config);  
  16.   
  17.     if (customIntent != null) {  
  18.         activity.mIntent = customIntent;  
  19.     }  
  20.     ...  
  21. }  

可以看出,Activity在創建的時候會new一個ContextImpl對象並在attach方法中關聯它,需要注意的是,創建Activity使用的數據結構是ActivityClientRecord。

 

Application對象中ContextImpl的創建

代碼在ActivityThread中的handleBindApplication方法中,此方法內部調用了makeApplication方法 

  1. public Application makeApplication(boolean forceDefaultAppClass,  
  2.         Instrumentation instrumentation) {  
  3.     if (mApplication != null) {  
  4.         return mApplication;  
  5.     }  
  6.   
  7.     Application app = null;  
  8.   
  9.     String appClass = mApplicationInfo.className;  
  10.     if (forceDefaultAppClass || (appClass == null)) {  
  11.         appClass = "android.app.Application";  
  12.     }  
  13.   
  14.     try {  
  15.         java.lang.ClassLoader cl = getClassLoader();  
  16.         ContextImpl appContext = new ContextImpl();  
  17.         appContext.init(thisnull, mActivityThread);  
  18.         app = mActivityThread.mInstrumentation.newApplication(  
  19.                 cl, appClass, appContext);  
  20.         appContext.setOuterContext(app);  
  21.     } catch (Exception e) {  
  22.         if (!mActivityThread.mInstrumentation.onException(app, e)) {  
  23.             throw new RuntimeException(  
  24.                 "Unable to instantiate application " + appClass  
  25.                 + ": " + e.toString(), e);  
  26.         }  
  27.     }  
  28.     ...  
  29. }  

看代碼發現和Activity中ContextImpl的創建是相同的。

 

Service對象中ContextImpl的創建

通過查看代碼發現和Activity、Application是一致的。分析到這裏,那麼三者的Context有什麼區別呢?沒有區別嗎?儘管如此,有一些細節是確定的:Dialog的使用需要Activity,在桌面上我們採用Application的Context無法彈出對話框,同時在桌面上想啓動新的activity,我們需要爲intent設置FLAG_ACTIVITY_NEW_TASK標誌,否則無法啓動activity,這一切都說明,起碼Application的Context和Activity的Context還是有區別的,當然這也可能不是Context的區別,因爲在桌面上,我們的應用沒有界面,這意味着我們能幹的事情可能受到了限制,事情的細節目前我還沒有搞的很清楚。

Context對資源的訪問

很明確,不同的Context得到的都是同一份資源。這是很好理解的,請看下面的分析

得到資源的方式爲context.getResources,而真正的實現位於ContextImpl中的getResources方法,在ContextImpl中有一個成員 private Resources mResources,它就是getResources方法返回的結果,mResources的賦值代碼爲:

mResources = mResourcesManager.getTopLevelResources(mPackageInfo.getResDir(),
                    Display.DEFAULT_DISPLAY, null, compatInfo, activityToken);

下面看一下ResourcesManager的getTopLevelResources方法,這個方法的思想是這樣的:在ResourcesManager中,所有的資源對象都被存儲在ArrayMap中,首先根據當前的請求參數去查找資源,如果找到了就返回,否則就創建一個資源對象放到ArrayMap中。有一點需要說明的是爲什麼會有多個資源對象,原因很簡單,因爲res下可能存在多個適配不同設備、不同分辨率、不同系統版本的目錄,按照android系統的設計,不同設備在訪問同一個應用的時候訪問的資源可以不同,比如drawable-hdpi和drawable-xhdpi就是典型的例子。 

  1. public Resources getTopLevelResources(String resDir, int displayId,  
  2.         Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) {  
  3.     final float scale = compatInfo.applicationScale;  
  4.     ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, scale,  
  5.             token);  
  6.     Resources r;  
  7.     synchronized (this) {  
  8.         // Resources is app scale dependent.  
  9.         if (false) {  
  10.             Slog.w(TAG, "getTopLevelResources: " + resDir + " / " + scale);  
  11.         }  
  12.         WeakReference<Resources> wr = mActiveResources.get(key);  
  13.         r = wr != null ? wr.get() : null;  
  14.         //if (r != null) Slog.i(TAG, "isUpToDate " + resDir + ": " + r.getAssets().isUpToDate());  
  15.         if (r != null && r.getAssets().isUpToDate()) {  
  16.             if (false) {  
  17.                 Slog.w(TAG, "Returning cached resources " + r + " " + resDir  
  18.                         + ": appScale=" + r.getCompatibilityInfo().applicationScale);  
  19.             }  
  20.             return r;  
  21.         }  
  22.     }  
  23.   
  24.     //if (r != null) {  
  25.     //    Slog.w(TAG, "Throwing away out-of-date resources!!!! "  
  26.     //            + r + " " + resDir);  
  27.     //}  
  28.   
  29.     AssetManager assets = new AssetManager();  
  30.     if (assets.addAssetPath(resDir) == 0) {  
  31.         return null;  
  32.     }  
  33.   
  34.     //Slog.i(TAG, "Resource: key=" + key + ", display metrics=" + metrics);  
  35.     DisplayMetrics dm = getDisplayMetricsLocked(displayId);  
  36.     Configuration config;  
  37.     boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY);  
  38.     final boolean hasOverrideConfig = key.hasOverrideConfiguration();  
  39.     if (!isDefaultDisplay || hasOverrideConfig) {  
  40.         config = new Configuration(getConfiguration());  
  41.         if (!isDefaultDisplay) {  
  42.             applyNonDefaultDisplayMetricsToConfigurationLocked(dm, config);  
  43.         }  
  44.         if (hasOverrideConfig) {  
  45.             config.updateFrom(key.mOverrideConfiguration);  
  46.         }  
  47.     } else {  
  48.         config = getConfiguration();  
  49.     }  
  50.     r = new Resources(assets, dm, config, compatInfo, token);  
  51.     if (false) {  
  52.         Slog.i(TAG, "Created app resources " + resDir + " " + r + ": "  
  53.                 + r.getConfiguration() + " appScale="  
  54.                 + r.getCompatibilityInfo().applicationScale);  
  55.     }  
  56.   
  57.     synchronized (this) {  
  58.         WeakReference<Resources> wr = mActiveResources.get(key);  
  59.         Resources existing = wr != null ? wr.get() : null;  
  60.         if (existing != null && existing.getAssets().isUpToDate()) {  
  61.             // Someone else already created the resources while we were  
  62.             // unlocked; go ahead and use theirs.  
  63.             r.getAssets().close();  
  64.             return existing;  
  65.         }  
  66.   
  67.         // XXX need to remove entries when weak references go away  
  68.         mActiveResources.put(key, new WeakReference<Resources>(r));  
  69.         return r;  
  70.     }  
  71. }  

根據上述代碼中資源的請求機制,再加上ResourcesManager採用單例模式,這樣就保證了不同的ContextImpl訪問的是同一套資源,注意,這裏說的同一套資源未必是同一個資源,因爲資源可能位於不同的目錄,但它一定是我們的應用的資源,或許這樣來描述更準確,在設備參數和顯示參數不變的情況下,不同的ContextImpl訪問到的是同一份資源。設備參數不變是指手機的屏幕和android版本不變,顯示參數不變是指手機的分辨率和橫豎屏狀態。也就是說,儘管Application、Activity、Service都有自己的ContextImpl,並且每個ContextImpl都有自己的mResources成員,但是由於它們的mResources成員都來自於唯一的ResourcesManager實例,所以它們看似不同的mResources其實都指向的是同一塊內存(C語言的概念),因此,它們的mResources都是同一個對象(在設備參數和顯示參數不變的情況下)。在橫豎屏切換的情況下且應用中爲橫豎屏狀態提供了不同的資源,處在橫屏狀態下的ContextImpl和處在豎屏狀態下的ContextImpl訪問的資源不是同一個資源對象。

 

代碼:單例模式的ResourcesManager類 

  1. public static ResourcesManager getInstance() {  
  2.     synchronized (ResourcesManager.class) {  
  3.         if (sResourcesManager == null) {  
  4.             sResourcesManager = new ResourcesManager();  
  5.         }  
  6.         return sResourcesManager;  
  7.     }  
  8. }  

getApplication和getApplicationContext的區別

 

getApplication返回結果爲Application,且不同的Activity和Service返回的Application均爲同一個全局對象,在ActivityThread內部有一個列表專門用於維護所有應用的application

    final ArrayList<Application> mAllApplications  = new ArrayList<Application>();

getApplicationContext返回的也是Application對象,只不過返回類型爲Context,看看它的實現

 

  1. @Override  
  2. public Context getApplicationContext() {  
  3.     return (mPackageInfo != null) ?  
  4.             mPackageInfo.getApplication() : mMainThread.getApplication();  
  5. }  

上面代碼中mPackageInfo是包含當前應用的包信息、比如包名、應用的安裝目錄等,原則上來說,作爲第三方應用,包信息mPackageInfo不可能爲空,在這種情況下,getApplicationContext返回的對象和getApplication是同一個。但是對於系統應用,包信息有可能爲空,具體就不深入研究了。從這種角度來說,對於第三方應用,一個應用只存在一個Application對象,且通過getApplication和getApplicationContext得到的是同一個對象,兩者的區別僅僅是返回類型不同。

應用中Context的數量

到此已經很明瞭了,一個應用中Context的數量等於Activity的個數 + Service的個數 + 1,這個1爲Application。


感謝“singwhatiwanna

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章