一、什么是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
- 避免非靜態的內部類,盡量使用靜態類,避免生命周期問題,注意內部類對外部對象引用導致的生命周期變化