揭秘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作用域ApplicationActivityService
Show a DialogNoYesNo
Start an Activity不推荐Yes不推荐
Layout Inflation不推荐Yes不推荐
Start a ServiceYesYesYes
Send a BroadcastYesYesYes
Register Broadcast ReceiverYesYesYes
Load Resource ValuesYesYesYes

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对象。
  2. Activity,getApplicationContext()[后文会详细介绍这个方法]:获取当前Activity所在应用进程的Context对象,通常我们使用Context对象时,要优先考虑这个全局的进程Context。
  3. 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()方法
@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作为静态对象,其生命周期要长于普通的对象(单例直到APP退出后台才销毁),其中也包含了Activity。比如Activity A去getInstance()得到instance对象,传入this,常驻内存的Singleton保存了我们传入的A对象,并一直持有,即使Activity被销毁掉,但因为它的引用还存在于一个Singleton中,就不可能被GC掉,这样就导致了内存泄漏。比如典型的数据库操作,存储数据,需要重复的去索取数据,用单例保持数据和拿到Activity持有context引用,因为单例可以看作是上帝,它帮我们保存数据。所以即使Activity被finish掉,还有它的引用在Singleton中。

  1. 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)。
正确使用:

  1. 当Applicatin的Context能搞定的情况下,并且生命周期长的对象,优先使用Application的Context;
  2. 不要让生命周期长于Activity的对象持有Activity的引用。
  3. 尽量不要在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;
    }
}

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 撸撸猫 设计师:C马雯娟 返回首页
实付 9.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值