Android——应用上下文Context

1. 简介

Context是Android的一个总父类,启动四大组件,获取资源,获取类加载器等都是通过Context来完成,Activity,Service,Application也都是派生于Context。

  • 是应用程序环境的全局信息的接口。
  • 是一个抽象类,由Android系统提供。
  • 允许访问特定于应用程序的资源和类,以及调用应用程序级操作,如启动活动,广播和接收意图等。

Context是维持Android程序各组件能够正常工作的一个核心功能类,而这个核心功能类相当于一个大的环境,只有在这个环境下,Android的资源才能被获取以及Android的各项组件才能被调用。

通俗的方式来说:Android应用程序就像一部电影,Android的四大组件相当于电影的四大主角,而Context就相当于是摄像镜头,只有通过摄像镜头我们才能看到四大主角。主角是被内定好的,不能随便new出来。而其他跑龙套的并不是内定的,也就是说不是那么重要,他们可以被new出来,但是他们也需要通过摄像镜头才可以看到,所以才有了Button mButton=new Button(Context)。

类图

  • Context:抽象类,定义了顶层接口
  • ContextImpl:Context的实现类
  • ContextWrapper:Context的包装类,成员变量mBase是一直指向ContextImpl的对象
  • ContextThemeWrapper:ContextWrapper的子类,包含Theme相关操作

Activity、Service和Application这三种类型的Context,在多数情况下,都是可以通用的,而为了安全考虑,在某些情况下,是不可以通用的,诸如启动Activity、弹出Dialog等。一个Activity的启动必须建立在另一个Actvity的基础上;Dialog也必须在Activity上面弹出,这些情况下,也只能使用Activity的Context。

也就是说:凡是跟UI相关的,都应该使用Activity做为Context来处理;其他的一些操作,Service,Activity,Application等实例都可以,如下图所示:
在这里插入图片描述
注意看到有一些NO上添加了一些数字,其实这些从能力上来说是YES,但是为什么说是NO呢?下面一个一个解释:

数字1:启动Activity在这些类中是可以的,但是需要创建一个新的task。一般情况不推荐。

数字2:在这些类中去layout inflate是合法的,但是会使用系统默认的主题样式,如果你自定义了某些样式可能不会被使用。

数字3:在receiver为null时允许,在4.2或以上的版本中,用于获取黏性广播的当前值。(可以无视)

注:ContentProvider、BroadcastReceiver之所以在上述表格中,是因为在其内部方法中都有一个context用于使用。

2. Tips

2.1 Context数量

Context数量 = Activity数量 + Service数量 + 1(Application)

2.2 getApplication()、getApplicationContext()和getBaseContext()的关系

在Activity中获取分别打印这个三个方法:

MyApplication myApp = (MyApplication) getApplication();  

Context appContext = getApplicationContext();  

Context baseContext = getBaseContext();

打印结果:

getApplication::::com.beidou.mvptest.MyApplication@53502fac

getApplicationContext::::com.beidou.mvptest.MyApplication@53502fac

baseContext::::android.app.ContextImpl@53505ce4

结论:

  • getApplication和getApplicationContext得到的是一个对象MyApplication。
  • getBaseContext得到的是ContextImpl。

疑问:

getApplication和getApplicationContext得到的对象是一样的,那为何设计两个方法呢?

  • 两者范围不同,后者比前者适用范围更广。getApplication只适用于Activity和Service,而getApplicationContext还用于其他场景,比如BroadcastReceiver中。

ContextImpl是什么?

  • ContextImpl是Context功能的实现类。Application和Service、Activity并不会去实现Context的功能,只是做了接口的封装,具体的功能由ContextImpl完成。
  • 因为Application、Activity、Service都是直接或间接继承自ContextWrapper的,我们就直接看ContextWrapper的源码,就会发现所有ContextWrapper中方法的实现都非常统一,就是调用了mBase对象中对应当前方法名的方法。
  • 那么这个mBase对象又是什么呢?我们来看第16行的attachBaseContext()方法,这个方法中传入了一个base参数,并把这个参数赋值给了mBase对象。而attachBaseContext()方法其实是由系统来调用的,它会把ContextImpl对象作为参数传递到attachBaseContext()方法当中,从而赋值给mBase对象,之后ContextWrapper中的所有方法其实都是通过这种委托的机制交由ContextImpl去具体实现的,所以说ContextImpl是上下文功能的实现类。
  • 再看一下我们刚刚打印的getBaseContext()方法,在第26行。这个方法只有一行代码,就是返回了mBase对象而已,而mBase对象其实就是ContextImpl对象,因此刚才的打印结果也得到了印证。

2.3 使用Application出现的问题和解决办法

2.3.1 在Application的构造方法中去获取Context实现类的各种方法

public class MyApplication extends Application {
    
      
      
    public MyApplication() {
    
      
        String packageName = getPackageName();  
        Log.d("TAG", "package name is " + packageName);  
    }  
      
}

运行结果:

空指针:

java.lang.RuntimeException: Unable to instantiate application 

com.example.test.MyApplication: java.lang.NullPointerException

修改:

public class MyApplication extends Application {
    
      
      
    @Override  
    public void onCreate() {
    
      
        super.onCreate();  
        String packageName = getPackageName();  
        Log.d("TAG", "package name is " + packageName);  
    }  
      
}  

运行结果正常!发生了什么事情呢?回顾ContextWrapper源码,ContextWrapper中有一个attachBaseContext()方法,这个方法会将传入的一个Context参数赋值给mBase对象,之后mBase对象就有值了。而我们又知道,所有Context的方法都是调用这个mBase对象的同名方法,那么也就是说如果在mBase对象还没赋值的情况下就去调用Context中的任何一个方法时,就会出现空指针异常。Application中方法的执行顺序:
在这里插入图片描述
在onCreate()方法中去初始化全局的变量数据是一种比较推荐的做法,假如你想把初始化提前到极致,也可以重写attachBaseContext()方法:

public class MyApplication extends Application {
    
      
      
    @Override  
    protected void attachBaseContext(Context base) {
    
      
        // 在这里调用Context的方法会崩溃  
        super.attachBaseContext(base);  
        // 在这里调用Context的方法就没问题
    }  
}  

2.3.2 把Application当做工具类使用时获取实例采用new的方式

public class MyApplication extends Application {
    
      
      
    private static MyApplication app;  
      
    public static MyApplication getInstance() {
    
      
        if (app == null) {
    
      
            app = new MyApplication();  
        }  
        return app;  
    }  
}  

new MyApplication实例的方式,得到的对象并不具备Context的能力,如果进行Context操作就会报空指针,因为它只是一个java对象。而我们知道Application本身就是一个单例了,所以我们直接返回本身即可,不用再去new对象获取实例,否则弄巧成拙。

public class MyApplication extends Application {
    
      
      
    private static MyApplication app;  
      
    public static MyApplication getInstance() {
    
      
        return app;  
    }  
      
    @Override  
    public void onCreate() {
    
      
        super.onCreate();  
        app = this;  
    }  
      
}  

2.4 Context乱用导致的内存泄漏的问题和解决办法

package com.mooc.shader.roundimageview;
 
import android.content.Context;
 
public class CustomManager
{
    
    
	private static CustomManager sInstance;
	private Context mContext;
 
	private CustomManager(Context context)
	{
    
    
		this.mContext = context;
	}
 
	public static synchronized CustomManager getInstance(Context context)
	{
    
    
		if (sInstance == null)
		{
    
    
			sInstance = new CustomManager(context);
		}
		return sInstance;
	}
	
	//some methods 
	private void someOtherMethodNeedContext()
	{
    
    
		
	}
}

这么写是没有问题的,问题在于,这个Context哪来的我们不能确定,很大的可能性,你在某个Activity里面为了方便,直接传了个this;这样问题就来了,我们的这个类中的sInstance是一个static且强引用的,在其内部引用了一个Activity作为Context,也就是说,我们的这个Activity只要我们的项目活着,就没有办法进行内存回收。而我们的Activity的生命周期肯定没这么长,所以造成了内存泄漏。

那么,我们如何才能避免这样的问题呢?

有人会说,我们可以软引用,嗯,软引用,假如被回收了,你不怕NullPointException么。

把上述代码做下修改:

public static synchronized CustomManager getInstance(Context context)
	{
    
    
		if (sInstance == null)
		{
    
    
			sInstance = new CustomManager(context.getApplicationContext());
		}
		return sInstance;
	}

这样,我们就解决了内存泄漏的问题,因为我们引用的是一个ApplicationContext,它的生命周期和我们的单例对象一致。

2.5 注意

一般Context造成的内存泄漏,几乎都是当Context销毁的时候,却因为被引用导致销毁失败,而Application的Context对象可以理解为随着进程存在的,所以我们总结出使用Context的正确姿势:

  • 当Application的Context能搞定的情况下,并且生命周期长的对象,优先使用Application的Context。
  • 不要让生命周期长于Activity的对象持有到Activity的引用。
  • 尽量不要在Activity中使用非静态内部类,因为非静态内部类会隐式持有外部类实例的引用,如果使用静态内部类,将外部实例引用作为弱引用持有。

猜你喜欢

转载自blog.csdn.net/ly0724ok/article/details/122068413