Android開發 - Context解析

阿俊学JAVA發表於2024-07-20

Context是什麼

  • Context的中文翻譯為:語境; 上下文; 背景; 環境,在開發中我們經常說稱之為“上下文”,那麼這個“上下文”到底是指什麼意思呢?在語文中,我們可以理解為語境,在程式中,我們可以理解為當前物件在程式中所處的一個環境,一個與系統互動的過程。比如微信聊天,此時的“環境”是指聊天的介面以及相關的資料請求與傳輸,Context在載入資源、啟動Activity、獲取系統服務、建立View等操作都要參與

  • Context到底是什麼呢?一個Activity就是一個Context,一個Service也是一個ContextAndroid程式設計師把“場景”抽象為Context類,他們認為使用者和作業系統的每一次互動都是一個場景,比如打電話、發簡訊,這些都是一個有介面的場景,還有一些沒有介面的場景,比如後臺執行的服務(Service)。一個應用程式可以認為是一個工作環境,使用者在這個環境中會切換到不同的場景,這就像一個前臺秘書,她可能需要接待客人,可能要列印檔案,還可能要接聽客戶電話,而這些就稱之為不同的場景,前臺秘書可以稱之為一個應用程式

  • 原始碼中的註釋是這麼來解釋Context的:Context提供了關於應用環境全域性資訊的介面。它是一個抽象類,它的執行被Android系統所提供。它允許獲取以應用為特徵的資源和型別,是一個統領一些資源(應用程式環境變數等)的上下文。就是說,它描述一個應用程式環境的資訊(即上下文);是一個抽象類Android提供了該抽象類的具體實現類透過它我們可以獲取應用程式的資源和類包括應用級別操作如啟動Activity發廣播接受Intent等)。既然上面Context是一個抽象類,那麼肯定有他的實現類咯,我們在Context的原始碼中透過IDE可以檢視到他的子類最終可以得到如下關係圖:

image-20240720094349605

  • Context類本身是一個純abstract類,它有兩個具體的實現子類:ContextImplContextWrapper。其中ContextWrapper類,如其名所言,這只是一個包裝而已,ContextWrapper建構函式中必須包含一個真正的Context引用,同時ContextWrapper中提供了attachBaseContext()用於給ContextWrapper物件中指定真正的Context物件,呼叫ContextWrapper的方法都會被轉向其所包含的真正的Context物件。ContextThemeWrapper類,如其名所言,其內部包含了與主題(Theme)相關的介面,這裡所說的主題就是指在AndroidManifest.xml中透過android:themeApplication元素或者Activity元素指定的主題。當然,只有Activity才需要主題Service是不需要主題的,因為Service是沒有介面的後臺場景,所以Service直接繼承ContextWrapperApplication同理而ContextImpl類則真正實現了Context中的所有函式應用程式中所呼叫的各種Context類的方法其實現均來自於該類一句話總結Context的兩個子類分工明確其中ContextImpl是Context的具體實現類ContextWrapper是Context的包裝類ActivityApplicationService雖都繼承自ContextWrapperActivity繼承自ContextWrapper的子類ContextThemeWrapper),但它們初始化的過程中都會建立ContextImpl物件由ContextImpl實現Context中的方法

Context能幹什麼

  • Context到底可以實現哪些功能呢?這個就實在是太多了:彈出Toast啟動Activity啟動Service傳送廣播運算元據庫等等都需要用到Context,部分例項如下:

    TextView tv = new TextView(getContext());  
      
    ListAdapter adapter = new SimpleCursorAdapter(getApplicationContext(), …);  
      
    AudioManager am = (AudioManager)getContext().getSystemService(Context.AUDIO_SERVICE);
    getApplicationContext().getSharedPreferences(name, mode);  
      
    getApplicationContext().getContentResolver().query(uri, …);  
      
    getContext().getResources().getDisplayMetrics().widthPixels * 5 / 8;  
      
    getContext().startActivity(intent);  
      
    getContext().startService(intent);  
      
    getContext().sendBroadcast(intent); 
    

Context作用域

  • 雖然Context神通廣大,但並不是隨便拿到一個Context例項就可以為所欲為,它的使用還是有一些規則限制的。由於Context的具體例項是由ContextImpl類去實現的,因此在絕大多數場景下,ActivityServiceApplication這三種型別的Context都是可以通用的。不過有幾種場景比較特殊比如啟動Activity還有彈出Dialog出於安全原因的考慮Android是不允許Activity或Dialog憑空出現的一個Activity的啟動必須要建立在另一個Activity的基礎之上也就是以此形成的返回棧。而Dialog則必須在一個Activity上面彈出(除非是System Alert型別的Dialog),因此在這種場景下,我們只能使用Activity型別的Context,否則將會出錯

  • 其中Activity所持有的Context的作用域最廣,無所不能。因為Activity繼承自ContextThemeWrapper,而ApplicationService繼承自ContextWrapper,很顯然ContextThemeWrapperContextWrapper的基礎上又做了一些操作使得Activity變得更強大。以下說明Application和Service所不推薦的兩種使用情況

    1. 如果我們用ApplicationContext去啟動一個LaunchModestandardActivity的時候會報錯android.util.AndroidRuntimeException: Calling startActivity from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?這是因為非Activity型別的Context並沒有所謂的任務棧,所以待啟動的Activity就找不到棧了。解決這個問題的方法就是為待啟動的Activity指定FLAG_ACTIVITY_NEW_TASK標記位,這樣啟動的時候就為它建立一個新的任務棧,而此時Activity是以singleTask模式啟動的。所有這種用Application啟動Activity的方式不推薦使用ServiceApplication

    2. ApplicationService中去layout inflate也是合法的,但是會使用系統預設的主題樣式如果你自定義了某些樣式可能不會被使用。所以這種方式也不推薦使用

    • 一句話總結:凡是跟UI相關的,都應該使用Activity做為Context來處理;其他的一些操作,ServiceActivityApplication等例項都可以,當然了,注意Context引用的持有防止記憶體洩漏

如何獲取Context

獲取Context物件

  • 主要提供以下2種方法

    1. getView().getContext():返回當前View物件的Context物件,通常是當前正在展示的Activity物件

    2. getActivity().getApplicationContext():獲取當前Activity所在的(應用)程序的Context物件,通常我們使用Context物件時,要優先考慮這個全域性的程序Context

getApplication()getApplicationContext()的區別

  • Application本身就是一個Context,所以這裡獲取getApplicationContext()得到的結果就是Application本身的例項。那麼問題來了,既然這兩個方法得到的結果都是相同的,那麼Android為什麼要提供兩個功能重複的方法呢?實際上這兩個方法在作用域上有比較大的區別getApplication()方法的語義性非常強,一看就知道是用來獲取Application例項的,但是這個方法只有在ActivityService中才能呼叫的到。那麼也許在絕大多數情況下我們都是在Activity或者Service中使用Application的,但是如果在一些其它的場景,比如BroadcastReceiver中也想獲得Application的例項,這時就可以藉助getApplicationContext()方法了,下面提供兩種獲取方法:

    public classMyReceiver extends BroadcastReceiver{  
        @Override  
        public void onReceive(Context context,Intent intent){  
            //第一種方法
            Application myApp = (Application)context.getApplicationContext();	
            ---
            //第二種方法
            context = getActivity().getApplicationContext();
            Application myApp = (Application)context; 
        }  
    } 
    

Context引起的記憶體洩露

  • 但Context並不能隨便亂用,用的不好有可能會引起記憶體洩露的問題,下面就示例兩種錯誤的引用方式

    1. 錯誤的單例模式

      public class Singleton {      
          private static Singleton instance;      
          private Context mContext;    
              
          private Singleton(Context context) {          
              this.mContext = context;  
          }      
                
          public static Singleton getInstance(Context context) {         
              if (instance == null) {  
                   instance = new Singleton(context);  
              }         
              return instance;  
          }  
      }  
      
      • 這是一個非執行緒安全的單例模式instance作為靜態物件,其生命週期要長於普通的物件,其中也包含Activity,假如Activity AgetInstance獲得instance物件,傳入this,常駐記憶體的Singleton儲存了你傳入的Activity A物件,並一直持有,即使Activity被銷燬掉,但因為它的引用還存在於一個Singleton中,就不可能被GC(咔擦)掉,這樣就導致了記憶體洩漏
    2. 錯誤的View持有Activity引用

      public class MainActivity extends Activity {  
          private static Drawable mDrawable;   
           
          @Override  
          protected void onCreate(Bundle saveInstanceState) {          
              super.onCreate(saveInstanceState);  
              setContentView(R.layout.activity_main);          
              ImageView iv = new ImageView(this);  
              mDrawable = getResources().getDrawable(R.drawable.ic_launcher);  
              iv.setImageDrawable(mDrawable);
          }  
      }  
      
      • 有一個靜態的Drawable物件當ImageView設定這個Drawable時,ImageView儲存了mDrawable的引用,而ImageView傳入的thisMainActivitymContext,因為被static修飾的mDrawable是常駐記憶體的,MainActivity是它的間接引用,MainActivity被銷燬時,也不能被GC(咔擦)掉,所以造成記憶體洩漏

正確使用Context

  • 一般Context造成的記憶體洩漏,幾乎都是當Context銷燬的時候,卻因為被引用導致銷燬失敗,而ApplicationContext物件可以理解為隨著程序存在的,所以我們總結出使用Context的正確姿勢:
    1. ApplicationContext能搞定的情況下,並且生命週期長的物件,優先使用ApplicationContext
    2. 不要讓生命週期長於Activity的物件持有到Activity的引用
    3. 儘量不要在Activity中使用非靜態內部類因為非靜態內部類會隱式持有外部類例項的引用,如果使用靜態內部類,外部例項將引用作為弱引用持有

總結

  • Context在Android系統中的地位很重要,它幾乎無所不能,但它也不是你想用就能隨便用的,謹防使用不當引起的記憶體問題

相關文章