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程序一樣,隨便創(chuàng)建一個類,寫個main()方法就能運行,**Android應用模型是基于組件的應用設計模式,組件的運行要有一個完整的Android工程環(huán)境。在這個環(huán)境下,Activity、Service等系統(tǒng)組件才能正常工作,而這些組件不能采用普通的java對象創(chuàng)建方式,new一下是不能創(chuàng)建實例的,而是要有它們各自的上下文環(huán)境,也就是Context.
所以說,Context是維持android各組件能夠正常工作的一個核心功能類。
what 's Context:
(本圖為沙拉查詞給出的中文翻譯)
有點晦澀難懂。但在程序中,我們可理解為當前對象在程序中所處的一個環(huán)境,一個與系統(tǒng)交互的過程。 比如QQ和你們自己的女朋友聊天時(沒有grilfriend的可自己跳過舉例),此時的context是指的聊天界面以及相關的數據請求與傳輸,Context在加載資源、啟動Activity、獲取系統(tǒng)服務、創(chuàng)建View等操作都要參與。
所以,一個Activity就是一個Context(getActivity()==getContext),一個Service也是一個Context。Android把場景抽象為Context類,用戶和操作系統(tǒng)的每一次交互都是一個場景,比如:打電話、發(fā)短信等,都有activity,還有一些我們肉眼看不見的后臺服務。一個應用程序可以認為是一個工作環(huán)境,用戶在這個環(huán)境中切換到不同的場景,這就像服務員,客戶可能是外賣小哥、也可能是農民工等,這些就是不同的場景,而服務員就是一個應用程序。
How to understand the ‘Context':
Context理解為”上下文“/”場景“,可能還是很抽象。那么我們可以做一個比喻:
一個APP是仙劍奇?zhèn)b傳3電視劇,Activity、Service、BroadcastReceiver、ContentProvider這四大組件就是電視劇的主角。它們是導演(系統(tǒng))一開始就確定好試鏡成功的人。換言之, 不是我們每個人都能被導演認可的。有了演員,就要有鏡頭啊,這個鏡頭便是(Context)。通過鏡頭,我們才能看見帥氣 的胡歌。演員們都是在鏡頭(Context環(huán)境)下表演的。那么Button這些組件子類型就是配角,它們沒有那么重要,隨便一個組件都能參與演出(即隨便new 一個實例),但是它們也需要參與鏡頭,不然一部戲只有主角多沒意思,魔尊重樓還是要的,魔尊也要露面(工作在Context環(huán)境下),所以可以用代碼new Button();或者xml布局定義一個button。
打開AndroidStudio,輸入Context,然后ctrl+鼠標左鍵追朔其源碼(看源碼一般都先看注釋便于理解):import android.content.Context;
看注釋,TMD,是English,那么筆者這里就用小學生英語水平來翻譯一哈哈:
Context提供了關于應用環(huán)境全局信息的接口。它是一個abstract類,它的執(zhí)行被Android系統(tǒng)提供,允許獲取以應用為特征的資源和類型,是一個統(tǒng)領一些資源APP環(huán)境變量等的上下文。通過它可以獲取應用程序的資源和類(包括應用級別操作,如啟動Activity,發(fā)廣播,接收intent等)。abstract會有它的實現(xiàn)類。在源碼中,我們可以通過AndroidStudio去查看它的子類,得到以下關系:
它有2個具體實現(xiàn)子類:ContextImpl、ContextWrapper。
- 其中,ContextWrapper類,只是一個包裝類,其構造函數中必須包含一個Context引用,同時它提供了attachBaseContext()用于給ContextWrapper對象中指定真正的Context對象,調用它的方法都會被轉向其所包含的真正的Context對象。
- ContextThemeWrapper類其內部包含了與主題相關的接口。主題就是清單文件中android:theme為Application或Activity元素指定的主題。(Activity才需要主題,Serviceu不需要,因為服務是沒有界面的后臺場景,所以服務直接繼承ContextWrapper。Application同理。)而Contextlmpl類則是真正實現(xiàn)了Context中的所有函數,應用程序中所調用的各種Context類的方法,其實現(xiàn)均來自這個類。
- 換言之:Context的2個實現(xiàn)子類分工的,其中ContextImpl是Context的具體是實現(xiàn)類,而ContextWrapper則是Context的包裝類。Activity、Application、Service都繼承自ContextWrapper(Activity繼承自ContextWrapper的子類ContextThemeWrapper),但它們的初始化過程中都會創(chuàng)建ContextImpl對象,由ContextImpl實現(xiàn)Context中的方法。
How much has Context in a App:
關鍵在于對COntext的理解。從上面提到的實現(xiàn)子類可以看出,在APP中,Context的具體實現(xiàn)子類是Acitivity、Service、Applicaiton。所以Context's number=Activity's number + Service's number+1(1個APP只有一個Application)。為啥不是4大組件,上面不是說四大組件也是主角嗎?看看BroadcastReceiver和ContentProvider的源碼可以知道它們并不是Context的子類,它們持有的Context都是其他地方傳遞過去的(比如我們發(fā)送廣播intent中的context就是外部傳遞過來的),所以不計數它們。
Context's method:
Context哪里會用到它。剛開始了解Android的時候不知道它是個啥玩意兒,但是久了發(fā)現(xiàn)有些地方就不得不傳這個參數。
比如Toast、啟動Activity、啟動Service、發(fā)送廣播、操作數據庫等等都需要傳Context參數,具體例子就不說了。詳細可以看后文將提到的如何獲取它。
Context's 作用域
不是隨便獲取一個Context實例就可以的,它的使用有一些規(guī)則和限制。因為Context的具體實例是由ContextImpl類去實現(xiàn)的,因此,Activity、Service、Application3種類型的Context都是等價的。但是,需要注意的是,,有些場景,比如啟動Activity、彈出Dialog等。為了安全,Android不允許Activity或者Dialog憑空出現(xiàn),一個Activity的啟動肯定是由另一個Activity負責的,也就是以此形成的返回棧(具體可以看看任主席的《Android開發(fā)藝術探索》)而Dialog則必須是在一個Activity上彈出(系統(tǒng)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的解決辦法,這樣啟動的時候就為它創(chuàng)建一個新的任務棧,而此時Activity是以Single Task模式啟動的。所以這種用Application Context啟動Activity的方式不推薦,Service同理。
2.在Application和Service中去layout inflate也是合法的,但是會使用系統(tǒng)默認的主題樣式,如果自定義了某些樣式可能不會被使用,所以也不推薦。
注:和UI相關的,都應該使用Activity Context來處理。其他的一些操作,Service、Activity、Application等實例都是可以的。同時要注意Context的引用持有,防止內存泄漏??稍诒讳N毀的時候,置Context為null。
How to get the ‘Context':
常用4種方法獲取Context對象:
1.View.getContext():返回當前View對象的Context對象。通常是當前正在展示的Activity對象。
1.Activity,getApplicationContext()[后文會詳細介紹這個方法]:獲取當前Activity所在應用進程的Context對象,通常我們使用3.Context對象時,要優(yōu)先考慮這個全局的進程Context。
ContextWrapper.getBaseContext():用來獲取一個ContextWrapper進行裝飾之前的Context。實際開發(fā)很少用,也不建議使用。
4.Activity.this:返回當前Activity的實例,如果的UI控件需要使用Activity作為Context對象,但默認的Toast實際上使用的ApplicationContext也可以。
實現(xiàn)View.OnClick監(jiān)聽方法中,寫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個方法在作用域上有比較大的區(qū)別。 getApplication()一看就知道是用來獲取Application實例的(道理可以聯(lián)想getActivity())。但getApplication()只有在Activity和Service中才能調用的到。 對于比如BroadcastReceiver等中也想要獲取Application實例,這時就需要getApplicationContext()方法。
//繼承BroadcastReceiver并重寫onReceive()方法 @Override public 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作為靜態(tài)對象,其生命周期要長于普通的對象(單例直到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);//通過代碼動態(tài)的創(chuàng)建組件,而不是傳統(tǒng)的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而發(fā)愁,畢竟我們很多的操作都是在活動中進行的,而活動本身就是一個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()方法來發(fā)送HTTP請求顯然沒問題。并且還可以在回調方法中處理服務器返回的數據。但是這個方法還可以被優(yōu)化。當檢測不到網絡存在的時候就給用戶一個Toast,并不再執(zhí)行后面的代碼。問題來了,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啟動的時候,系統(tǒng)就會自動將這個類進行初始化。我們可以定制一個自己的Application類,以便管理程序內一些全局的狀態(tài)信息,比如說全局Context。
定制一個自己的Application并不復雜,首先, 需要創(chuàng)建一個MyApplication類繼承自系統(tǒng)的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,然后又提供了一個靜態(tài)的getContext()方法,在這里將剛才獲取到的COntext進行返回。
接下來,我們需要告訴系統(tǒng),當程序啟動的時候應該初始化MyApplication類,而不是系統(tǒng)默認的Application類。這一步需要在清單文件里面實現(xiàn),找到清單文件的<application>標簽下進行指定就可以了:
<manifest …… ……> <application android :name="com.example.myContext.MyApplication" //這里輸入.MyApplication也可以,或者輸入MyApplication根據AS提示自動補全包名 ..> </application>
注意:這里一定要加上完整的包名,不然系統(tǒng)將無法找到這個類。
以上就是實現(xiàn)了一種全局獲取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然后繼承系統(tǒng)的Application)。
正確使用:
當Applicatin的Context能搞定的情況下,并且生命周期長的對象,優(yōu)先使用Application的Context;
不要讓生命周期長于Activity的對象持有Activity的引用。
盡量不要在Activity中使用非靜態(tài)內部類。非靜態(tài)內部類會隱式持有外部類實例的引用。如果使用靜態(tài)內部類,將外部實例引用作為弱引用持有。
獲取全局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的詳細介紹內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Mybatis Plus 字段為空值時執(zhí)行更新方法未更新解決方案
這篇文章主要介紹了Mybatis Plus 字段為空值時執(zhí)行更新方法未更新解決方案,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-09-09IDEA創(chuàng)建Servlet程序的兩種實現(xiàn)方法
Servlet是JavaWeb應用程序中不可或缺的組件之一,本文主要介紹了IDEA創(chuàng)建Servlet程序的兩種實現(xiàn)方法,具有一定的參考價值,感興趣的可以了解一下2023-10-10SpringData JPA快速上手之關聯(lián)查詢及JPQL語句書寫詳解
JPA都有SpringBoot的官方直接提供的starter,而Mybatis沒有,直到SpringBoot 3才開始加入到官方模版中,這篇文章主要介紹了SpringData JPA快速上手,關聯(lián)查詢,JPQL語句書寫的相關知識,感興趣的朋友一起看看吧2023-09-09關于Spring注解@Async引發(fā)其他注解失效的解決
這篇文章主要介紹了關于Spring注解@Async引發(fā)其他注解失效的解決,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-03-03RocketMQ消息隊列實現(xiàn)隨機消息發(fā)送當做七夕禮物
這篇文章主要為大家介紹了RocketMQ消息隊列實現(xiàn)隨機消息發(fā)送當做七夕禮物,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-08-08