前幾篇文章,我也是費勁心思寫了一個ListView系列的三部曲,雖然在內容上能夠說是絕對的精華,可是不少朋友都表示看不懂。好吧,這個系列不只是把你們給難倒了,也確實是把我給難倒了,以前爲了寫瀑布流ListView的Demo就寫了大半個月的時間。那麼本篇文章咱們就講點輕鬆的東西,不去分析那麼複雜的源碼了,而是來談一談你們都熟知的Context。android
Context相信全部的Android開發人員基本上天天都在接觸,由於它太常見了。可是這並不表明Context沒有什麼東西好講的,實際上Context有太多小的細節並不被你們所關注,那麼今天咱們就來學習一下那些你所不知道的細節。數據庫
Context類型安全
咱們知道,Android應用都是使用Java語言來編寫的,那麼你們能夠思考一下,一個Android程序和一個Java程序,他們最大的區別在哪裏?劃分界限又是什麼呢?其實簡單點分析,Android程序不像Java程序同樣,隨便建立一個類,寫個main()方法就能跑了,而是要有一個完整的Android工程環境,在這個環境下,咱們有像Activity、Service、BroadcastReceiver等系統組件,而這些組件並非像一個普通的Java對象new一下就能建立實例的了,而是要有它們各自的上下文環境,也就是咱們這裏討論的Context。能夠這樣講,Context是維持Android程序中各組件可以正常工做的一個核心功能類。app
下面咱們來看一下Context的繼承結構:ide
Context的繼承結構仍是稍微有點複雜的,能夠看到,直系子類有兩個,一個是ContextWrapper,一個是ContextImpl。那麼從名字上就能夠看出,ContextWrapper是上下文功能的封裝類,而ContextImpl則是上下文功能的實現類。而ContextWrapper又有三個直接的子類,ContextThemeWrapper、Service和Application。其中,ContextThemeWrapper是一個帶主題的封裝類,而它有一個直接子類就是Activity。工具
那麼在這裏咱們至少看到了幾個所比較熟悉的面孔,Activity、Service、還有Application。由此,其實咱們就已經能夠得出結論了,Context一共有三種類型,分別是Application、Activity和Service。這三個類雖然分別各類承擔着不一樣的做用,但它們都屬於Context的一種,而它們具體Context的功能則是由ContextImpl類去實現的。oop
那麼Context到底能夠實現哪些功能呢?這個就實在是太多了,彈出Toast、啓動Activity、啓動Service、發送廣播、操做數據庫等等等等都須要用到Context。因爲Context的具體能力是由ContextImpl類去實現的,所以在絕大多數場景下,Activity、Service和Application這三種類型的Context都是能夠通用的。不過有幾種場景比較特殊,好比啓動Activity,還有彈出Dialog。出於安全緣由的考慮,Android是不容許Activity或Dialog憑空出現的,一個Activity的啓動必需要創建在另外一個Activity的基礎之上,也就是以此造成的返回棧。而Dialog則必須在一個Activity上面彈出(除非是System Alert類型的Dialog),所以在這種場景下,咱們只能使用Activity類型的Context,不然將會出錯。學習
Context數量測試
那麼一個應用程序中到底有多少個Context呢?其實根據上面的Context類型咱們就已經能夠得出答案了。Context一共有Application、Activity和Service三種類型,所以一個應用程序中Context數量的計算公式就能夠這樣寫:this
Context數量 = Activity數量 + Service數量 + 1
上面的1表明着Application的數量,由於一個應用程序中能夠有多個Activity和多個Service,可是隻能有一個Application。
Application Context的設計
基本上每個應用程序都會有一個本身的Application,並讓它繼承自系統的Application類,而後在本身的Application類中去封裝一些通用的操做。其實這並非Google所推薦的一種作法,由於這樣咱們只是把Application當成了一個通用工具類來使用的,而實際上使用一個簡單的單例類也能夠實現一樣的功能。可是根據個人觀察,有太多的項目都是這樣使用Application的。固然這種作法也並無什麼反作用,只是說明仍是有很多人對於Application理解的還有些欠缺。那麼這裏咱們先來對Application的設計進行分析,講一些你們所不知道的細節,而後再看一下平時使用Application的問題。
首先新建一個MyApplication並讓它繼承自Application,而後在AndroidManifest.xml文件中對MyApplication進行指定,以下所示:
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
......
</application>
指定完成後,當咱們的程序啓動時Android系統就會建立一個MyApplication的實例,若是這裏不指定的話就會默認建立一個Application的實例。
前面提到過,如今不少的Application都是被看成通用工具類來使用的,那麼既然做爲一個通用工具類,咱們要怎樣才能獲取到它的實例呢?以下所示:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyApplication myApp = (MyApplication) getApplication();
Log.d("TAG", "getApplication is " + myApp);
}
}
能夠看到,代碼很簡單,只須要調用getApplication()方法就能拿到咱們自定義的Application的實例了,打印結果以下所示:
那麼除了getApplication()方法,其實還有一個getApplicationContext()方法,這兩個方法看上去好像有點關聯,那麼它們的區別是什麼呢?咱們將代碼修改一下:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyApplication myApp = (MyApplication) getApplication();
Log.d("TAG", "getApplication is " + myApp);
Context appContext = getApplicationContext();
Log.d("TAG", "getApplicationContext is " + appContext);
}
}
一樣,咱們把getApplicationContext()的結果打印了出來,如今從新運行代碼,結果以下圖所示:
咦?好像打印出的結果是同樣的呀,連後面的內存地址都是相同的,看來它們是同一個對象。其實這個結果也很好理解,由於前面已經說過了,Application自己就是一個Context,因此這裏獲取getApplicationContext()獲得的結果就是MyApplication自己的實例。
那麼有的朋友可能就會問了,既然這兩個方法獲得的結果都是相同的,那麼Android爲何要提供兩個功能重複的方法呢?實際上這兩個方法在做用域上有比較大的區別。getApplication()方法的語義性很是強,一看就知道是用來獲取Application實例的,可是這個方法只有在Activity和Service中才能調用的到。那麼也許在絕大多數狀況下咱們都是在Activity或者Service中使用Application的,可是若是在一些其它的場景,好比BroadcastReceiver中也想得到Application的實例,這時就能夠藉助getApplicationContext()方法了,以下所示:
public class MyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
MyApplication myApp = (MyApplication) context.getApplicationContext();
Log.d("TAG", "myApp is " + myApp);
}
}
也就是說,getApplicationContext()方法的做用域會更廣一些,任何一個Context的實例,只要調用getApplicationContext()方法均可以拿到咱們的Application對象。
那麼更加細心的朋友會發現,除了這兩個方法以外,其實還有一個getBaseContext()方法,這個baseContext又是什麼東西呢?咱們仍是經過打印的方式來驗證一下:
哦?此次獲得的是不一樣的對象了,getBaseContext()方法獲得的是一個ContextImpl對象。這個ContextImpl是否是感受有點似曾相識?回去看一下Context的繼承結構圖吧,ContextImpl正是上下文功能的實現類。也就是說像Application、Activity這樣的類其實並不會去具體實現Context的功能,而僅僅是作了一層接口封裝而已,Context的具體功能都是由ContextImpl類去完成的。那麼這樣的設計究竟是怎麼實現的呢?咱們仍是來看一下源碼吧。由於Application、Activity、Service都是直接或間接繼承自ContextWrapper的,咱們就直接看ContextWrapper的源碼,以下所示:
/**
* Proxying implementation of Context that simply delegates all of its calls to
* another Context. Can be subclassed to modify behavior without changing
* the original Context.
*/
public class ContextWrapper extends Context {
Context mBase;
/**
* Set the base context for this ContextWrapper. All calls will then be
* delegated to the base context. Throws
* IllegalStateException if a base context has already been set.
*
* @param base The new base context for this wrapper.
*/
protected void attachBaseContext(Context base) {
if (mBase != null) {
throw new IllegalStateException("Base context already set");
}
mBase = base;
}
/**
* @return the base context as set by the constructor or setBaseContext
*/
public Context getBaseContext() {
return mBase;
}
@Override
public AssetManager getAssets() {
return mBase.getAssets();
}
@Override
public Resources getResources() {
return mBase.getResources();
}
@Override
public ContentResolver getContentResolver() {
return mBase.getContentResolver();
}
@Override
public Looper getMainLooper() {
return mBase.getMainLooper();
}
@Override
public Context getApplicationContext() {
return mBase.getApplicationContext();
}
@Override
public String getPackageName() {
return mBase.getPackageName();
}
@Override
public void startActivity(Intent intent) {
mBase.startActivity(intent);
}
@Override
public void sendBroadcast(Intent intent) {
mBase.sendBroadcast(intent);
}
@Override
public Intent registerReceiver(
BroadcastReceiver receiver, IntentFilter filter) {
return mBase.registerReceiver(receiver, filter);
}
@Override
public void unregisterReceiver(BroadcastReceiver receiver) {
mBase.unregisterReceiver(receiver);
}
@Override
public ComponentName startService(Intent service) {
return mBase.startService(service);
}
@Override
public boolean stopService(Intent name) {
return mBase.stopService(name);
}
@Override
public boolean bindService(Intent service, ServiceConnection conn,
int flags) {
return mBase.bindService(service, conn, flags);
}
@Override
public void unbindService(ServiceConnection conn) {
mBase.unbindService(conn);
}
@Override
public Object getSystemService(String name) {
return mBase.getSystemService(name);
}
......
}
因爲ContextWrapper中的方法仍是很是多的,我就進行了一些篩選,只貼出來了部分方法。那麼上面的這些方法相信你們都是很是熟悉的,getResources()、getPackageName()、getSystemService()等等都是咱們常常要用到的方法。那麼全部這些方法的實現又是什麼樣的呢?其實全部ContextWrapper中方法的實現都很是統一,就是調用了mBase對象中對應當前方法名的方法。
那麼這個mBase對象又是什麼呢?咱們來看第16行的attachBaseContext()方法,這個方法中傳入了一個base參數,並把這個參數賦值給了mBase對象。而attachBaseContext()方法實際上是由系統來調用的,它會把ContextImpl對象做爲參數傳遞到attachBaseContext()方法當中,從而賦值給mBase對象,以後ContextWrapper中的全部方法其實都是經過這種委託的機制交由ContextImpl去具體實現的,因此說ContextImpl是上下文功能的實現類是很是準確的。
那麼另外再看一下咱們剛剛打印的getBaseContext()方法,在第26行。這個方法只有一行代碼,就是返回了mBase對象而已,而mBase對象其實就是ContextImpl對象,所以剛纔的打印結果也獲得了印證。
使用Application的問題
雖然說Application的用法確實很是簡單,可是咱們平時的開發工做當中也着實存在着很多Application誤用的場景,那麼今天就來看一看有哪些比較容易犯錯的地方是咱們應該注意的。
Application是Context的其中一種類型,那麼是否就意味着,只要是Application的實例,就能隨時使用Context的各類方法呢?咱們來作個實驗試一下就知道了:
public class MyApplication extends Application {
public MyApplication() {
String packageName = getPackageName();
Log.d("TAG", "package name is " + packageName);
}
}
這是一個很是簡單的自定義Application,咱們在MyApplication的構造方法當中獲取了當前應用程序的包名,並打印出來。獲取包名使用了getPackageName()方法,這個方法就是由Context提供的。那麼上面的代碼能正常運行嗎?跑一下就知道了,你將會看到以下所示的結果:
應用程序一啓動就馬上崩潰了,報的是一個空指針異常。看起來好像挺簡單的一段代碼,怎麼就會成空指針了呢?可是若是你嘗試把代碼改爲下面的寫法,就會發現一切正常了:
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
String packageName = getPackageName();
Log.d("TAG", "package name is " + packageName);
}
}
運行結果以下所示:
在構造方法中調用Context的方法就會崩潰,在onCreate()方法中調用Context的方法就一切正常,那麼這兩個方法之間到底發生了什麼事情呢?咱們從新回顧一下ContextWrapper類的源碼,ContextWrapper中有一個attachBaseContext()方法,這個方法會將傳入的一個Context參數賦值給mBase對象,以後mBase對象就有值了。而咱們又知道,全部Context的方法都是調用這個mBase對象的同名方法,那麼也就是說若是在mBase對象還沒賦值的狀況下就去調用Context中的任何一個方法時,就會出現空指針異常,上面的代碼就是這種狀況。Application中方法的執行順序以下圖所示:
Application中在onCreate()方法裏去初始化各類全局的變量數據是一種比較推薦的作法,可是若是你想把初始化的時間點提早到極致,也能夠去重寫attachBaseContext()方法,以下所示:
public class MyApplication extends Application {
@Override
protected void attachBaseContext(Context base) {
// 在這裏調用Context的方法會崩潰
super.attachBaseContext(base);
// 在這裏能夠正常調用Context的方法
}
}
以上是咱們平時在使用Application時須要注意的一個點,下面再來介紹另一種很是廣泛的Application誤用狀況。
其實Android官方並不太推薦咱們使用自定義的Application,基本上只有須要作一些全局初始化的時候可能才須要用到自定義Application,官方文檔描述以下:
可是就個人觀察而言,如今自定義Application的使用狀況基本上能夠達到100%了,也就是咱們平時本身寫測試demo的時候可能不會使用,正式的項目幾乎所有都會使用自定義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的能力。有不少人向我反饋使用 LitePal 時發生了空指針錯誤其實都是因爲這個緣由,由於你提供給LitePal的只是一個普通的Java對象,它沒法經過這個對象來進行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更多的細節,而且不要去犯使用Context時的一些低級錯誤。