一、什么是Context
二、Context的創(chuàng)建時機(jī)和獲取
1. Context的創(chuàng)建時機(jī)
2. Context的獲取
三、Application使用相關(guān)問題
1. 什么時候初始化全局變量
2. 自定義Application?
四、Context引起的內(nèi)存泄露
Android應(yīng)用都是使用Java語言來編寫的,本質(zhì)上也是一個對象,那么Activity可以new嗎?一個Android程序和一個Java程序,他們最大的區(qū)別在哪里?劃分界限又是什么呢?其實(shí)簡單點(diǎn)分析,Android程序不像Java程序一樣,隨便創(chuàng)建一個類,寫個main()方法就能跑了,Android應(yīng)用模型是基于Activity、Service、BroadcastReceiver等組件的應(yīng)用設(shè)計(jì)模式,組件的運(yùn)行要有一個完整的Android工程環(huán)境,在這個環(huán)境下,這些組件并不是像一個普通的Java對象new一下就能創(chuàng)建實(shí)例的了,而是要有它們各自的上下文環(huán)境Context。可以這樣講,Context是維持Android程序中各組件能夠正常工作的一個核心功能類。
什么是Context
一個Activity是一個Context,一個Service也是一個Context。在程序中,我們把可以把Context理解為當(dāng)前對象在程序中所處的一個環(huán)境,一個與系統(tǒng)交互的過程。用戶和操作系統(tǒng)的每一次交互都是一個場景,比如微信聊天,此時的“環(huán)境”是指聊天的界面以及相關(guān)的數(shù)據(jù)請求與傳輸,Context在加載資源、啟動Activity、獲取系統(tǒng)服務(wù)、創(chuàng)建View等操作都要參與。打電話、發(fā)短信,這些都是一個有界面的場景,還有一些沒有界面的場景,比如后臺運(yùn)行的服務(wù)(Service)。一個應(yīng)用程序可以認(rèn)為是一個工作環(huán)境,用戶在這個環(huán)境中會切換到不同的場景,這就像一個前臺秘書,她可能需要接待客人,可能要打印文件,還可能要接聽客戶電話,而這些就稱之為不同的場景,前臺秘書可以稱之為一個應(yīng)用程序。下面我們來看一下Context的繼承結(jié)構(gòu):

Context類,一個純Abstract類,有ContextImpl和ContextWrapper兩個實(shí)現(xiàn)類:
- ContextWrapper包裝類
其構(gòu)造函數(shù)中必須包含一個真正的Context引用。ContextWrapper中提供了attachBaseContext()(由系統(tǒng)調(diào)用)方法,用于給ContextWrapper對象中指定真正的Context對象,即ContextImpl對象,調(diào)用ContextWrapper的方法都會被轉(zhuǎn)向ContextImpl的方法。 - ContextImpl類
上下文功能的實(shí)現(xiàn)類。 - ContextThemeWrapper類
一個帶主題的封裝類,其內(nèi)部包含了與Theme相關(guān)的接口,這里所說的主題是指在AndroidManifest.xml中通過android:theme為Application元素或者Activity元素指定的主題。當(dāng)然,只有Activity才需要主題,Service是不需要主題的,因?yàn)镾ervice是沒有界面的后臺場景,所以Service直接繼承于ContextWrapper,Application同理。
總結(jié):Context的兩個子類分工明確,其中ContextImpl是Context的具體實(shí)現(xiàn)類,ContextWrapper是Context的包裝類。Activity,Application,Service雖都繼承自ContextWrapper,但它們初始化的過程中都會創(chuàng)建ContextImpl對象,由ContextImpl實(shí)現(xiàn)Context中的方法。
那么,Context到底可以實(shí)現(xiàn)哪些功能呢?這個就實(shí)在是太多了,彈出Toast、啟動Activity、啟動Service、發(fā)送廣播、操作數(shù)據(jù)庫等等都需要用到Context。由于Context的具體能力是由ContextImpl類去實(shí)現(xiàn)的,因此在絕大多數(shù)場景下,Activity、Service和Application這三種類型的Context都是可以通用的,但在使用場景上是有一些規(guī)則,以下表格中列出了各Context的使用場景:

以上表格中NO上添加了一些數(shù)字,其實(shí)這些從能力上來說是YES,但是為什么說是NO呢?下面一個一個解釋:
- NO^1:啟動Activity在這些類中是可以的,但是需要創(chuàng)建一個新的task。不推薦。
如果我們用Application Context或Service Context去啟動一個LaunchMode為standard的Activity的時候會報(bào)錯,這是因?yàn)榉茿ctivity類型的Context并沒有所謂的任務(wù)棧,所以待啟動的Activity就找不到棧了。解決這個問題的方法就是為待啟動的Activity指定FLAG_ACTIVITY_NEW_TASK標(biāo)記位,這樣啟動的時候就為它創(chuàng)建一個新的任務(wù)棧,而此時Activity是以singleTask模式啟動的。 - NO^2:在這些類中去layout inflate是合法的,但是會使用系統(tǒng)默認(rèn)的主題樣式,如果你自定義了某些樣式可能不會被使用。不推薦。
所以:
- 凡是跟UI相關(guān)的,都應(yīng)使用Activity做為Context來處理;其他的一些操作,Service,Activity,Application等都可以,當(dāng)然得注意Context引用的持有,防止內(nèi)存泄漏。
比如啟動Activity,還有彈出Dialog。出于安全原因的考慮,Android是不允許Activity或Dialog憑空出現(xiàn)的,一個Activity的啟動必須要建立在另一個Activity的基礎(chǔ)之上,也就是以此形成的返回棧。而Dialog則必須在一個Activity上面彈出(除非是System Alert類型的Dialog),因此在這種場景下,我們只能使用Activity類型的Context,否則將會出錯。
了解了Context,那在一個應(yīng)用程序中,Context的數(shù)量又是多少呢?由以上的介紹可以知道:**Context數(shù)量 = Activity數(shù)量 + Service數(shù)量 + 1 **
Context的創(chuàng)建時機(jī)和獲取
1.Context的創(chuàng)建時機(jī)
(1)創(chuàng)建Application對象的時機(jī)
每個應(yīng)用程序在第一次啟動時,都會首先創(chuàng)建Application對象。在應(yīng)用程序啟動一個Activity(startActivity)的流程中,創(chuàng)建Application的時機(jī)是創(chuàng)建handleBindApplication()方法中,該函數(shù)位于 ActivityThread.java類中,如下:
//創(chuàng)建Application時同時創(chuàng)建的ContextIml實(shí)例
private final void handleBindApplication(AppBindData data){
…
///創(chuàng)建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(); //創(chuàng)建一個ContextImpl對象實(shí)例
appContext.init(this, null, mActivityThread); //初始化該ContextIml實(shí)例的相關(guān)屬性
///新建一個Application對象
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
appContext.setOuterContext(app); //將該Application實(shí)例傳遞給該ContextImpl實(shí)例
}
…
}
(2)創(chuàng)建Activity對象的時機(jī)
通過startActivity()或startActivityForResult()請求啟動一個Activity時,如果系統(tǒng)檢測需要新建一個Activity對象時,就會回調(diào)handleLaunchActivity()方法,該方法繼而調(diào)用performLaunchActivity()方法,去創(chuàng)建一個Activity實(shí)例,并且回調(diào)onCreate(),onStart()方法等, 函數(shù)都位于 ActivityThread.java類 ,如下:
//創(chuàng)建一個Activity實(shí)例時同時創(chuàng)建ContextIml實(shí)例
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 {
//創(chuàng)建一個Activity對象實(shí)例
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
}
if (activity != null) {
ContextImpl appContext = new ContextImpl(); //創(chuàng)建一個Activity實(shí)例
appContext.init(r.packageInfo, r.token, this); //初始化該ContextIml實(shí)例的相關(guān)屬性
appContext.setOuterContext(activity); //將該Activity信息傳遞給該ContextImpl實(shí)例
…
}
…
}
(3)創(chuàng)建Service對象的時機(jī)
通過startService或者bindService時,如果系統(tǒng)檢測到需要新創(chuàng)建一個Service實(shí)例,就會回調(diào)handleCreateService()方法,完成相關(guān)數(shù)據(jù)操作。handleCreateService()函數(shù)位于 ActivityThread.java類,如下:
//創(chuàng)建一個Service實(shí)例時同時創(chuàng)建ContextIml實(shí)例
private final void handleCreateService(CreateServiceData data){
…
//創(chuàng)建一個Service實(shí)例
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(); //創(chuàng)建一個ContextImpl對象實(shí)例
context.init(packageInfo, null, this); //初始化該ContextIml實(shí)例的相關(guān)屬性
//獲得我們之前創(chuàng)建的Application對象信息
Application app = packageInfo.makeApplication(false, mInstrumentation);
//將該Service信息傳遞給該ContextImpl實(shí)例
context.setOuterContext(service);
…
}
另外,通過對ContextImp的分析可知,其方法的大多數(shù)操作都是直接調(diào)用其屬性mPackageInfo(該屬性類型為PackageInfo)的相關(guān)方法而來。這說明ContextImp是一種輕量級類,而PackageInfo才是真正重量級的類。而一個App里的所有ContextIml實(shí)例,都對應(yīng)同一個packageInfo對象。
2.Context的獲取
(1)通常我們想要獲取Context對象,主要有以下四種方法
- View.getContext,返回當(dāng)前View對象的Context對象,通常是當(dāng)前正在展示的Activity對象
- Activity.getApplicationContext,獲取的context來自允許在應(yīng)用(進(jìn)程)application中的所有Activity,當(dāng)你需要用到的Context超出當(dāng)前Activity的生命周期時使用
- Activity.this 返回當(dāng)前的Activity實(shí)例,如果是UI控件需要使用Activity作為Context對象,但是默認(rèn)的Toast實(shí)際上使用ApplicationContext也可以
- ContextWrapper.getBaseContext()用來獲取一個ContextWrapper進(jìn)行裝飾之前的Context,也就是ContextImpl對象,如果想獲取另一個可以訪問的application里面的Context時可以使用
(2)再來看看getApplication()和getApplicationContext()
這兩個方法有什么區(qū)別呢?看看以下結(jié)果:

通過上面的代碼,可以看到它們是同一個對象。其實(shí)這個結(jié)果也很好理解,因?yàn)榍懊嬉呀?jīng)說過了,Application本身就是一個Context,所以這里獲取getApplicationContext()得到的結(jié)果就是Application本身的實(shí)例。那么問題來了,既然這兩個方法得到的結(jié)果都是相同的,那么Android為什么要提供兩個功能重復(fù)的方法呢?實(shí)際上這兩個方法在作用域上有比較大的區(qū)別。
getApplication()方法的語義性非常強(qiáng),一看就知道是用來獲取Application實(shí)例的,但這個方法只有在Activity和Service中才能調(diào)用。如果在一些其它的場景,比如BroadcastReceiver中也想獲得Application的實(shí)例,這時就需要借助getApplicationContext()方法了。也就是說,getApplicationContext()方法的作用域會更廣一些,任何一個Context的實(shí)例,只要調(diào)用getApplicationContext()方法都可以拿到我們的Application對象。
(3)getActivity()和getContext()
- getActivity()返回Activity,getContext()返回Context;
- 兩者是Fragment的方法,但Activity沒有,多數(shù)情況下兩者沒有什么區(qū)別,但新版Support Library包,F(xiàn)ragment不被Activity持有時,區(qū)別見這里;
- 參數(shù)是context的,可以使用getActivity() 。因?yàn)锳ctivity間接繼承了Context,但Context不是Activity;
- this和getContext() 并不是完全相同。在Activity類中可以使用this,因?yàn)锳ctivity繼承自Context,但是getContext()方法不在Activity類中。
Application使用相關(guān)問題
1.什么時候初始化全局變量
在應(yīng)用程序中常常會持有一個自己的Application,首先讓它繼承自系統(tǒng)的Application類,然后在自己的Application類中去封裝一些通用的操作。雖然Application的用法很簡單,但同時也存在著不少Application誤用的場景。Application是Context的其中一種類型,那么是否就意味著,只要是Application的實(shí)例,就能隨時使用Context的各種方法呢?做個實(shí)驗(yàn)試:
方式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的構(gòu)造方法和onCreate()方法中兩種方式來獲取當(dāng)前應(yīng)用程序的包名,并打印出來。獲取包名使用了getPackageName()方法,這個方法就是由Context提供的。那哪種方式能得到想要的結(jié)果呢?得到的結(jié)果是否又是一樣?
結(jié)果表明,方式一應(yīng)用程序一啟動就立刻崩潰了,報(bào)的是一個空指針異常:

方式二運(yùn)行正常:

這兩個方法之間到底發(fā)生了什么事情呢?我們重新回顧一下ContextWrapper類的源碼,ContextWrapper中有一個attachBaseContext()方法,這個方法會將傳入的一個Context參數(shù)賦值給mBase對象,之后mBase對象就有值了。而我們又知道,所有Context的方法都是調(diào)用這個mBase對象的同名方法,那么也就是說如果在mBase對象還沒賦值的情況下就去調(diào)用Context中的任何一個方法時,就會出現(xiàn)空指針異常,上面的代碼就是這種情況。
Application中方法的執(zhí)行順序?yàn)椋篈pplication構(gòu)造方法—>attachBaseContext()—>onCreate()。
Application中在onCreate()方法里去初始化各種全局變量數(shù)據(jù)是一種比較推薦的做法,但如果你想把初始化的時間提前到極致,也可以重寫attachBaseContext(),如下所示:
public class MyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
// 在這里調(diào)用Context的方法會崩潰
super.attachBaseContext(base);
// 在這里可以正常調(diào)用Context的方法
}
}
2.自定義Application?
其實(shí)Android官方并不太推薦我們使用自定義的Application,基本上只有需要做一些全局初始化的時候可能才需要用到自定義Application。多數(shù)項(xiàng)目只是把自定義Application當(dāng)成了一個通用工具類,而這個功能并不需要借助Application來實(shí)現(xiàn),使用單例可能是一種更加標(biāo)準(zhǔn)的方式。不過自定義Application也并沒有什么副作用,它和單例模式二選一都可以實(shí)現(xiàn)同樣的功能,但把自定義Application和單例模式混合到一起使用,就會出各種問題了。如下:
public class MyApplication extends Application {
private static MyApplication app;
public static MyApplication getInstance() {
if (app == null) {
app = new MyApplication();
}
return app;
}
}
就像單例模式一樣,這里提供了一個getInstance()方法,用于獲取MyApplication的實(shí)例,有了這個實(shí)例之后,就可以調(diào)用MyApplication中的各種工具方法了,然而事實(shí)卻非想的那么美好。因?yàn)槲覀冎繟pplication是屬于系統(tǒng)組件,系統(tǒng)組件的實(shí)例是要由系統(tǒng)來去創(chuàng)建的,如果這里我們自己去new一個MyApplication的實(shí)例,它就只是一個普通的Java對象而已,而不具備任何Context的能力,如果想通過該對象來進(jìn)行Context操作,就會發(fā)生空指針錯誤。那么如果真的想要提供一個獲取MyApplication實(shí)例的方法,比較標(biāo)準(zhǔn)的寫法又是什么樣的呢?其實(shí)這里我們只需謹(jǐn)記一點(diǎn),Application全局只有一個,它本身就已經(jīng)是單例了,無需再用單例模式去為它做多重實(shí)例保護(hù)了,代碼如下所示:
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就是當(dāng)前Application的實(shí)例,那么app也就是當(dāng)前Application的實(shí)例了。
Context引起的內(nèi)存泄露
context發(fā)生內(nèi)存泄露的話,就會泄露很多內(nèi)存。這里泄露的意思是gc沒有辦法回收activity的內(nèi)存,在傳遞Context時會增加對象指針的引用計(jì)數(shù),所以基于智能指針技術(shù)的GC無法釋放相應(yīng)的內(nèi)存。
當(dāng)屏幕旋轉(zhuǎn)的時候,系統(tǒng)會銷毀當(dāng)前的activity,保存狀態(tài)信息,再創(chuàng)建一個新的。比如我們寫了一個應(yīng)用程序,它需要加載一個很大的圖片,我們不希望每次旋轉(zhuǎn)屏幕的時候都銷毀這個圖片,重新加載。實(shí)現(xiàn)這個要求的簡單想法就是定義一個靜態(tài)的Drawable,這樣Activity 類創(chuàng)建銷毀它始終保存在內(nèi)存中。實(shí)現(xiàn)類似:
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);
}
}
這段程序看起來很簡單,但是卻問題很大。當(dāng)屏幕旋轉(zhuǎn)的時候會有內(nèi)存泄漏(即gc沒法銷毀Activity)。屏幕旋轉(zhuǎn)的時系統(tǒng)會銷毀當(dāng)前的activity,但是當(dāng)drawable和view關(guān)聯(lián)后,drawable保存了view的 reference,即sDrawable保存了textView的引用,而textView保存了Activity的引用。既然Drawable不能銷毀,它所引用和間接引用的都不能銷毀,這樣系統(tǒng)就沒有辦法銷毀當(dāng)前的Activity,于是造成了內(nèi)存泄露,gc對這種類型的內(nèi)存泄露是無能為力的。為了防止內(nèi)存泄露,我們應(yīng)該注意以下幾點(diǎn):
- 不要讓生命周期長的對象引用Activity Context,即保證引用activity的對象要與activity本身生命周期是一樣的
- 對于生命周期長的對象,可以使用Application Context
- 避免非靜態(tài)的內(nèi)部類,盡量使用靜態(tài)類,避免生命周期問題,注意內(nèi)部類對外部對象引用導(dǎo)致的生命周期變化