JAVA中Context的詳細介紹和實例分析
最熟悉的陌生人——Context
剛剛學android或者js等,都會看見這個頻繁的字眼——Context。意為”上下文“。
本文主要記述,Context到底是什么、如何理解Context、一個APP可以有幾個Context、Context能干啥、Context的作用域、獲取Context、全局獲取Context技巧。
思考:
Java:萬物皆對象。Flutter:萬物皆組件。俗語:”沒對象嗎?自己new一個啊~“既然大多數情況可以new一個實例,那么,我們在android中的Activity實例怎么獲取呢?Activity.instance可以獲取activity。既然Activity也大致歸屬于一個類,那么可不可以用 Activity activity=new Activity(); 呢?安卓不像Java程序一樣,隨便創建一個類,寫個main()方法就能運行,**Android應用模型是基于組件的應用設計模式,組件的運行要有一個完整的Android工程環境。在這個環境下,Activity、Service等系統組件才能正常工作,而這些組件不能采用普通的java對象創建方式,new一下是不能創建實例的,而是要有它們各自的上下文環境,也就是Context.所以說,Context是維持android各組件能夠正常工作的一個核心功能類。
what ’s Context:
(本圖為沙拉查詞給出的中文翻譯)
有點晦澀難懂。但在程序中,我們可理解為當前對象在程序中所處的一個環境,一個與系統交互的過程。 比如QQ和你們自己的女朋友聊天時(沒有grilfriend的可自己跳過舉例),此時的context是指的聊天界面以及相關的數據請求與傳輸,Context在加載資源、啟動Activity、獲取系統服務、創建View等操作都要參與。
所以,一個Activity就是一個Context(getActivity()==getContext),一個Service也是一個Context。Android把場景抽象為Context類,用戶和操作系統的每一次交互都是一個場景,比如:打電話、發短信等,都有activity,還有一些我們肉眼看不見的后臺服務。一個應用程序可以認為是一個工作環境,用戶在這個環境中切換到不同的場景,這就像服務員,客戶可能是外賣小哥、也可能是農民工等,這些就是不同的場景,而服務員就是一個應用程序。
How to understand the ‘Context’:
Context理解為”上下文“/”場景“,可能還是很抽象。那么我們可以做一個比喻:一個APP是仙劍奇俠傳3電視劇,Activity、Service、BroadcastReceiver、ContentProvider這四大組件就是電視劇的主角。它們是導演(系統)一開始就確定好試鏡成功的人。換言之, 不是我們每個人都能被導演認可的。有了演員,就要有鏡頭啊,這個鏡頭便是(Context)。通過鏡頭,我們才能看見帥氣 的胡歌。演員們都是在鏡頭(Context環境)下表演的。那么Button這些組件子類型就是配角,它們沒有那么重要,隨便一個組件都能參與演出(即隨便new 一個實例),但是它們也需要參與鏡頭,不然一部戲只有主角多沒意思,魔尊重樓還是要的,魔尊也要露面(工作在Context環境下),所以可以用代碼new Button();或者xml布局定義一個button。
打開AndroidStudio,輸入Context,然后ctrl+鼠標左鍵追朔其源碼(看源碼一般都先看注釋便于理解):import android.content.Context;
看注釋,TMD,是English,那么筆者這里就用小學生英語水平來翻譯一哈哈:Context提供了關于應用環境全局信息的接口。它是一個abstract類,它的執行被Android系統提供,允許獲取以應用為特征的資源和類型,是一個統領一些資源APP環境變量等的上下文。通過它可以獲取應用程序的資源和類(包括應用級別操作,如啟動Activity,發廣播,接收intent等)。abstract會有它的實現類。在源碼中,我們可以通過AndroidStudio去查看它的子類,得到以下關系:它有2個具體實現子類:ContextImpl、ContextWrapper。
其中,ContextWrapper類,只是一個包裝類,其構造函數中必須包含一個Context引用,同時它提供了attachBaseContext()用于給ContextWrapper對象中指定真正的Context對象,調用它的方法都會被轉向其所包含的真正的Context對象。 ContextThemeWrapper類其內部包含了與主題相關的接口。主題就是清單文件中android:theme為Application或Activity元素指定的主題。(Activity才需要主題,Serviceu不需要,因為服務是沒有界面的后臺場景,所以服務直接繼承ContextWrapper。Application同理。)而Contextlmpl類則是真正實現了Context中的所有函數,應用程序中所調用的各種Context類的方法,其實現均來自這個類。 換言之:Context的2個實現子類分工的,其中ContextImpl是Context的具體是實現類,而ContextWrapper則是Context的包裝類。Activity、Application、Service都繼承自ContextWrapper(Activity繼承自ContextWrapper的子類ContextThemeWrapper),但它們的初始化過程中都會創建ContextImpl對象,由ContextImpl實現Context中的方法。How much has Context in a App:
關鍵在于對COntext的理解。從上面提到的實現子類可以看出,在APP中,Context的具體實現子類是Acitivity、Service、Applicaiton。所以Context’s number=Activity’s number + Service’s number+1(1個APP只有一個Application)。為啥不是4大組件,上面不是說四大組件也是主角嗎?看看BroadcastReceiver和ContentProvider的源碼可以知道它們并不是Context的子類,它們持有的Context都是其他地方傳遞過去的(比如我們發送廣播intent中的context就是外部傳遞過來的),所以不計數它們。
Context’s method:
Context哪里會用到它。剛開始了解Android的時候不知道它是個啥玩意兒,但是久了發現有些地方就不得不傳這個參數。比如Toast、啟動Activity、啟動Service、發送廣播、操作數據庫等等都需要傳Context參數,具體例子就不說了。詳細可以看后文將提到的如何獲取它。
Context’s 作用域
不是隨便獲取一個Context實例就可以的,它的使用有一些規則和限制。因為Context的具體實例是由ContextImpl類去實現的,因此,Activity、Service、Application3種類型的Context都是等價的。但是,需要注意的是,,有些場景,比如啟動Activity、彈出Dialog等。為了安全,Android不允許Activity或者Dialog憑空出現,一個Activity的啟動肯定是由另一個Activity負責的,也就是以此形成的返回棧(具體可以看看任主席的《Android開發藝術探索》)而Dialog則必須是在一個Activity上彈出(系統Alert類型的Dialog除外),這種情況下, 我們只能用Activity類型的Context,否則報錯。
Context作用域 Application Activity Service Show a Dialog No Yes No Start an Activity 不推薦 Yes 不推薦 Layout Inflation 不推薦 Yes 不推薦 Start a Service Yes Yes Yes Send a Broadcast Yes Yes Yes Register Broadcast Receiver Yes Yes Yes Load Resource Values Yes Yes Yes
Activity繼承自ContextThemeWrapper,而Application和Service繼承ContextWrapper,所以ContextThemeWrapper在ContextWrapper的基礎上作了一些操作,使得Activity更加厲害。
關于表格中提到的Application和Service不推薦的2種情況:
1.如果用ApplicationContext去啟動一個LaunchMode為standard的Activity的時候會報錯:androud,util.AndroidRuntimeException:Calling startActivity from outside of an Activity context require the FLAG_ACTIVITY_NEW_TASK flag。Is this really what you want?翻譯一下,并了解這個FLAG的都知道,此時的非Activity類型的Context并沒有所謂的返回棧,因此帶啟動的Activity就找不到棧。它還給我們明確之處了FLAG的解決辦法,這樣啟動的時候就為它創建一個新的任務棧,而此時Activity是以Single Task模式啟動的。所以這種用Application Context啟動Activity的方式不推薦,Service同理。
2.在Application和Service中去layout inflate也是合法的,但是會使用系統默認的主題樣式,如果自定義了某些樣式可能不會被使用,所以也不推薦。
注:和UI相關的,都應該使用Activity Context來處理。其他的一些操作,Service、Activity、Application等實例都是可以的。同時要注意Context的引用持有,防止內存泄漏。可在被銷毀的時候,置Context為null。
How to get the ‘Context’:
常用4種方法獲取Context對象:
1.View.getContext():返回當前View對象的Context對象。通常是當前正在展示的Activity對象。1.Activity,getApplicationContext()[后文會詳細介紹這個方法]:獲取當前Activity所在應用進程的Context對象,通常我們使用3.Context對象時,要優先考慮這個全局的進程Context。ContextWrapper.getBaseContext():用來獲取一個ContextWrapper進行裝飾之前的Context。實際開發很少用,也不建議使用。4.Activity.this:返回當前Activity的實例,如果的UI控件需要使用Activity作為Context對象,但默認的Toast實際上使用的ApplicationContext也可以。實現View.OnClick監聽方法中,寫Toast,不要用this,因為this,在onClick(View view)指的是view對象而不是Activity實例,所以在這個方法中,應該使用”當前的Activity名.this“,這是入門者比較容易混淆的地方。
getApplication()和getApplicationContext():獲取當前Application對象用getApplicationContext.但是getApplication又是什么。我們可以自己寫代碼打印一下:
Application app=(Application)getApplication();Log.e(TAG,'getApplication is '+app);Context context=getApplicationContext();Log.e(TAG,'getApplicationContext is '+ context);
運行后看logcat,效果圖就不貼了(電腦卡)。從打印結果可以看出它們2個的內存地址是相同的,即它們是同一個對象。 因為Application本來就是一個Context,那么這里獲取的getApplicationContext()自然也是Application本身的實例了。那這2個相同方法存在的意義是啥?(雙胞胎?)實際上這2個方法在作用域上有比較大的區別。 getApplication()一看就知道是用來獲取Application實例的(道理可以聯想getActivity())。但getApplication()只有在Activity和Service中才能調用的到。 對于比如BroadcastReceiver等中也想要獲取Application實例,這時就需要getApplicationContext()方法。
//繼承BroadcastReceiver并重寫onReceive()方法@Overridepublic void onReceive(Context context.Intent intent){ Application app=(Application)context.getApplicationContext();}
內存泄漏之Context:
我們經常會遇到內存泄漏,比如Activity銷毀了,但是Context還持有該Activity的引用,造成了內存泄漏。(經常遇到)
2種典型的錯誤引用方式:
1.錯誤的單例模式:
public class Singleton{ private static Singleton instancel private Context context; private Singleton(Context context){ this.context=context; } public static Singleton getInstance(Context context){ if(instance == null ){ instance=new Singleton(context); } return instance; }}
熟悉單例模式的都知道,這是一個非線程安全的單例模式,instance作為靜態對象,其生命周期要長于普通的對象(單例直到APP退出后臺才銷毀),其中也包含了Activity。比如Activity A去getInstance()得到instance對象,傳入this,常駐內存的Singleton保存了我們傳入的A對象,并一直持有,即使Activity被銷毀掉,但因為它的引用還存在于一個Singleton中,就不可能被GC掉,這樣就導致了內存泄漏。比如典型的數據庫操作,存儲數據,需要重復的去索取數據,用單例保持數據和拿到Activity持有context引用,因為單例可以看作是上帝,它幫我們保存數據。所以即使Activity被finish掉,還有它的引用在Singleton中。
View持有Activity引用:
public class MainActivity extend Activity{ private static Drawable mDrawable; @Override protected void onCreate(Bundle saveInstanceState){ super.onCreate(); setContentView(R.layout.activity_main); ImageView imageview=new ImageView(this);//通過代碼動態的創建組件,而不是傳統的xml配置組件,這里的ImageView持有當前Activity的引用。 mDrawable=getResources().getDrawable(R.drawable.ic_launcher); imageview.setImageDrawable(mDrawable); }}
上述代碼中,有一個static的Drawable對象。當ImageView設置這個Drawable的時候,ImageView保存了這個mDrawable的引用,而ImageView初始化的時候又傳入了this,此處的this是指MainActivity的context。因為被static修飾的mDrawable是常駐內存的(比類還要早加載)。MainActivity是它的間接引用了,當MainActivity被銷毀的時候,也不能被GC掉,就造成了內存泄漏。
How to get the context in the whole :
大量的地方都需要使用Context,我們常常會因為不知道怎么得到這個Context而苦惱。那么,全局獲取Context無疑是最好的解決方案。很多時候,我們也不是經常為得不到Context而發愁,畢竟我們很多的操作都是在活動中進行的,而活動本身就是一個Context對象。但APP架構復雜后,很多邏輯代碼都脫離了Activity類,此時又需要使用Context,所以我們需要采取全局獲取Context的方法。舉例, 我們平常經常會寫網絡工具類,比如下面的這些代碼:
public calss HttpUtil{ public static void sendHttpRequest(final String address,final HttpCallbackListener listener){ new Thread(new Runnable()){ @Override public void run(){ HttpURLConnection connection=null; try{ URL url =new URL(address); connection=(HttpURLConnection)url.openConnection(); connection.setRequestMethod('GET'); connection.setConnectTimeout(8000); connection.setReadTimeout(8000); connection.setDoInput(true); connection.setDoOutput(true); InputStream in =connection.getInputStream(); BufferedReader reader=new BufferedReader(new InputStreamReader(in)); StringBuilder response=new StringBuilder(); String line; while((line=reader.readLine())!=nulll){ response.append(line); } if(listener!=null){ //回調onFinish() listener.onFinish(response.toString); } }catch(Execption e){ if(listener!=null){ //回調onError() listener.onError(e); } }finally{ if(connection!=null){ connection.disconnect(); } } }}.start(); }}
上述代碼中使用sendHttpRequest()方法來發送HTTP請求顯然沒問題。并且還可以在回調方法中處理服務器返回的數據。但是這個方法還可以被優化。當檢測不到網絡存在的時候就給用戶一個Toast,并不再執行后面的代碼。問題來了,Toast需要一個Context參數,但是在本來沒有可以傳遞的Context對象。。。一般思路:在方法中添加一個COntext參數:
public static void sendHttpRequest(final String address,final HttpCallbackListener listener,final Context context){ if(!isNetWorkAvailable()){ Toast.makeText(context,……); …… } ……
看似可以,但是有點甩鍋。我們將獲取Context的任務轉移到了sendHttpRequest()方法的調用方。至于調用方能不能得到COntext對象就不是我們要考慮的問題了。
甩鍋不一定是通用的解決方案。于是這里介紹哈如何獲取全局Context的步驟:,通過它在項目的任何地方都能輕松的獲取到Context。:
Android提供了一個Application類,每當APP啟動的時候,系統就會自動將這個類進行初始化。我們可以定制一個自己的Application類,以便管理程序內一些全局的狀態信息,比如說全局Context。定制一個自己的Application并不復雜,首先, 需要創建一個MyApplication類繼承自系統的Application:
public calss MyApplication extends Application{ private static Context context; @Overrride public void onCreate(){ context=getApplicationContext(); } public static Context getContext(){ return context; }}
代碼很簡單,容易理解。重寫了父類的onCreate()方法,并通過調用getApplicationContext()方法得到一個應用程序級別的Context,然后又提供了一個靜態的getContext()方法,在這里將剛才獲取到的COntext進行返回。
接下來,我們需要告訴系統,當程序啟動的時候應該初始化MyApplication類,而不是系統默認的Application類。這一步需要在清單文件里面實現,找到清單文件的<application>標簽下進行指定就可以了:
<manifest …………><application android :name='com.example.myContext.MyApplication' //這里輸入.MyApplication也可以,或者輸入MyApplication根據AS提示自動補全包名 ..></application>
注意:這里一定要加上完整的包名,不然系統將無法找到這個類。
以上就是實現了一種全局獲取Context的機制,在這個項目的任何地方使用Context,只需要調用MyApplication.getContext()就可以了。
關于自定義Application和LitePal配置沖突的問題:
自定義需要在清單文件寫出android.name='……'。而為了讓LitePal可以正常工作,也需要在清單文件下,配置:
android:name='org.litepal.LitePalApplication'
道理也是一樣的,這樣配置后,LitePal就能在內部自動獲取到Context了。問題:當都已經配置過自定義的Application怎么辦?豈不是和LitePalApplication沖突了?解答:任何一個項目都只能配置一個Application. 對于這種情況,LitePalApplication給出了很簡單的解決方案,在自定義的Application中去調用LitePal的初始化方法就可以了:
public calss MyApplication extends Application{ private static Context context; @Overrride public void onCreate(){ context=getApplicationContext(); LitePalApplication.initialize(context); } public static Context getContext(){ return context; }}
這種寫法就相當于我們把全局Context對象通過參數傳遞給了LitePal,效果和在清單文件配置LitePalApplication是一樣的。
總結,如何在程序中正確的使用Context:
一般Context造成的內存泄漏,幾乎都是當Context銷毀的時候,因為被引用導致銷毀失敗。而Application的Context對象可以簡單的理解為伴隨著進程存在的(它的生命周期也很長,畢竟APP加載的時候先加載Application,我們可以自定義Application然后繼承系統的Application)。正確使用:
當Applicatin的Context能搞定的情況下,并且生命周期長的對象,優先使用Application的Context;不要讓生命周期長于Activity的對象持有Activity的引用。盡量不要在Activity中使用非靜態內部類。非靜態內部類會隱式持有外部類實例的引用。如果使用靜態內部類,將外部實例引用作為弱引用持有。
獲取全局context的另一種思路:
ActivityThread是主進程的入口,它的currentApplication返回值是application.
import android.app.Application;import java.lang.reflect.InvocationTargetException;/** * 這種方式獲取全局的Application 是一種拓展思路。 * <p> * 對于組件化項目,不可能把項目實際的Application下沉到Base,而且各個module也不需要知道Application真實名字 * <p> * 這種一次反射就能獲取全局Application對象的方式相比于在Application#OnCreate保存一份的方式顯示更加通用了 */public class AppGlobals { private static Application sApplication; public static Application getApplication() { if (sApplication == null) { try { sApplication = (Application) Class.forName('android.app.ActivityThread') .getMethod('currentApplication') .invoke(null, (Object[]) null); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } return sApplication; }}
到此這篇關于Context的詳細介紹和實例分析的文章就介紹到這了,更多相關Context的詳細介紹內容請搜索好吧啦網以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持好吧啦網!
相關文章: