标签: 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:
(本图为沙拉查词给出的中文翻译)
%title插图%num
有点晦涩难懂。但在程序中,我们可理解为当前对象在程序中所处的一个环境,一个与系统交互的过程。 比如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;

%title插图%num
看注释,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,否则报错。

%title插图%num

Activity继承自ContextThemeWrapper,而Application和Service继承ContextWrapper,所以ContextThemeWrapper在ContextWrapper的基础上作了一些操作,使得Activity更加厉害。
关于表格中提到的Application和Service不推荐的2种情况:

如果用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同理。
在Application和Service中去layout inflate也是合法的,但是会使用系统默认的主题样式,如果自定义了某些样式可能不会被使用,所以也不推荐。
注:和UI相关的,都应该使用Activity Context来处理。其他的一些操作,Service、Activity、Application等实例都是可以的。同时要注意Context的引用持有,防止内存泄漏。可在被销毁的时候,置Context为null。

How to get the ‘Context’:
常用4种方法获取Context对象:

View.getContext():返回当前View对象的Context对象。通常是当前正在展示的Activity对象。
Activity,getApplicationContext()[后文会详细介绍这个方法]:获取当前Activity所在应用进程的Context对象,通常我们使用Context对象时,要优先考虑这个全局的进程Context。
ContextWrapper.getBaseContext():用来获取一个ContextWrapper进行装饰之前的Context。实际开发很少用,也不建议使用。
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()方法
@Override
public void onReceive(Context context.Intent intent){
Application app=(Application)context.getApplicationContext();
}

内存泄漏之Context:
我们经常会遇到内存泄漏,比如Activity销毁了,但是Context还持有该Activity的引用,造成了内存泄漏。(经常遇到)

2种典型的错误引用方式:

错误的单例模式:
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类。这一步需要在清单文件里面实现,找到清单文件的标签下进行指定就可以了:

注意:这里一定要加上完整的包名,不然系统将无法找到这个类。

以上就是实现了一种全局获取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 是一种拓展思路。
*

* 对于组件化项目,不可能把项目实际的Application下沉到Base,而且各个module也不需要知道Application真实名字
*

* 这种一次反射就能获取全局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这个词翻译的不太好,不应该叫上下文,应该直接就叫“环境”,不过都这么叫,就叫上下文好了

所谓的上下文就是指语境,每一段程序都有很多的外部变量。只有想Add这种简单的函数才是没有外部变量的。一旦写的一段程序中有了外部变量,这段程序就是不完整的,不能独立运行,要想让他运行,就必须把所有的外部变量的值一个一个的全部传进去,这些值的集合就叫上下文。

说的通俗一点就是一段程序的执行需要依赖于外部的一些环境(外部变量等等),如果没有这些外部环境,这段程序是运行不起来的。

今天在segmentfault社区看见一则回答,生动的解释了什么是程序的上下文:
%title插图%num
可能上面的例子有点绕,但是多看一下就明白了,其实说白了,程序上下文可以理解为context实例中的全局变量,你给它什么样的值,它就呈现对应的值或者状态

子程序之于程序,进程之于操作系统,甚至app的一屏之于app,都是一个道理

当程序执行了一部分,要跳转到其他的地方,而跳转到的地方需要之前程序的一些结果(包括但不限于外部变量,外部对象等等)。

app点击一个按钮进入一个新的界面,也要保存你是在那个屏幕跳过来的等等信息,以便你点击返回的时候能正确跳回

上面这些都是上下文的典型例子,所以把“上下文”理解为环境就可以了。(而且上下文虽然是上下文,但是程序里面一般都只有上文而已,只是叫的好听叫上下文。进程中断在操作系统中是有上有下的)。

所以说,通俗一点理解就是,当程序从一个位置调到另一个位置的时候,这个时候就叫上下文的切换(因为他要保存现场,各种的压栈,出栈等等),进程之间切换也叫上下文切换,因为也要保存现场,以便切换回之前的线程

以我自己的认识水平来说,在C或者C++中,context一般就是一个结构体,用来存储一些关键信息,比如切换上下文时,要保存切换之前的状态和数据,这需要一个结构体来承担,然后将contex中的状态和数据重新赋值为新的,这样就切换了,等运行完了之后,又要切换回来,那么之前保存的那些状态和数据又要重新启用了,就是这么回事。

我想之所以在多线程网络编程中,现在采用的多是one loop per thread + thread poll,而不是采用一个socket一个线程,就是因为在切换线程(也就是切换上下文)的时候,需要保留大量的现场数据,而在重新切回到该线程时,又要恢复这个保存的数据(即保存的环境),保存/恢复都是需要花费cpu大量的资源和时间的,所以不如one loop per thread + thread poll的模式,当然one loop per thread + thread poll还有其他的好处,这只是其中一个好处,不需要大量的切换上下文,占用cpu大量的资源。

友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速