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
  • 避免非靜態的內部類,盡量使用靜態類,避免生命周期問題,注意內部類對外部對象引用導致的生命周期變化
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容