Context对于Android开发者来说肯定不陌生,在我们跳转新的Activity、弹出Toast,创建View等行为时都需要用到Context,可见Context每天都伴随着我们,但是Context究竟是什么意思呢?Context从中文翻译上是上下文、环境、场景。我个人更倾向用场景来理解Context,例如创建一个View,我们在构造方法传一个Context的,而传进去的Context则代表着这个View所处的一个布局的父类场景中,相当于该View(子View)与父View进行了一个场景关联。
源码的Context
源码对Context的解析是Context是一个关于应用程序环境全局信息的接口。Context提供了一个抽象类的实现Android系统。它允许访问特定于应用程序的资源和类,包括应用程序级操作,如启动Activity,发广播,接收Intent等。
在源码我们可以看到Context是一个抽象类,从上图我们可以看出实现Context抽象类有ContextImpl与ContextWrapper,从类名我们可以理解到分别是实现类与包装类。我们可以在图中看到我们很熟悉的Activity、Service、Application,他们都是ContextWrapper的子类,ContextWrapper构造函数中必须包含一个真正的Context引用,同时ContextWrapper中提供了attachBaseContext()用于给ContextWrapper对象中指定真正的Context对象,所以在Activity、Service、Application类中的attch()中都有attachBaseContext()方法,调用该方法都会被转向其所包含的真正的Context对象。而ContextImpl类则真正实现了Context中的所有函数,应用程序中所调用的各种Context类的方法,其实现均来自于该类。
Context数量
理解Context的作用,可以通过Context的个数来进行理解。从上图的继承关系可以看出,Context数量 = Activity数量 + Service数量 + Application数量。但是有人会有这个疑问,那么我们的四大组件,还有Broadcast Receiver和Content Provide并不是Context的子类,使用他们的时候用到的Context是从其他地方传过去的,所以是不算进Context的总数中,从上面的种种可以看出Context类在Android系统中地位是多么崇高。
Context的功能
Context实在太多了,启动Activity、弹出Toast、启动Service、创建View等等都用到Context。
TextView textView = new TextView(context);
context.getSharedPreferences(name, mode);
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
context().startActivity(intent);
context().startService(intent);
context().sendBroadcast(intent);
···
Context应用场景
用到Context的地方是很多,但是Context也有一定的规则。参考Context,What Context?
大家注意看到有一些NO上添加了一些数字,其实这些从一定程度上来说是YES,但是为什么说是NO呢?下面一个一个解释:
1.应用程序可以从这里开始一个Activity,但它需要一个新的Task被创建。这可能适合特定的用例,但是一般情况不推荐。
2.这是合法的,但inflation将使用系统默认主题您正在运行的系统,而不是在应用程序中定义的。
3.允许如果Receiver是null,用于获取的当前值的广播,在安卓4.2及以上。
另外上图我选Dialog来说一下。记得我刚开发时犯的错。就是在创建Dialog的时候将getApplication()传进去,因为封装Dialog的方法有tryCatch,导致自己没及时发现,发布到线上后在统计后台中发现报错,这个是我对这Context的时候印象最深刻的。
当时以为Activity的Context与Application的Context是通用的,就直接用了,但XXXActivity和getApplicationContext返回的肯定不是一个对象,一个是当前Activity的实例,一个是项目的Application的实例。既然区别这么明显,那么各自的使用场景肯定不同,乱使用可能会带来一些问题。
那么令有一个问题,我们如果正确获取Context?
获取Context
通常我们获取Context对象有下面的方式:
1.View.getContext(),返回当前View对象的Context对象,通常是当前正在展示的Activity对象。
2.Activity.getApplicationContext,获取当前Activity所在进程的全局Context对象。
3.ContextWrapper.getBaseContext():用来获取一个ContextWrapper进行装饰之前的Context,可以使用这个方法,这个方法在实际开发中使用并不多,也不建议使用。
4.Activity.this 返回当前的Activity实例,如果场景在Activity进行,如UI控件需要使用Activity作为Context对象。
另外在输出getApplication时候,IDE会提示getApplicationContext 、getApplication,那么它们究竟有什么不同呢?
我们通过上面的代码,打印得出两者的内存地址都是相同的,看来它们是同一个对象。其实这个结果也很好理解,因为前面已经说过了,Application本身就是一个Context,所以这里获取getApplicationContext()得到的结果就是Application本身的实例。那么问题来了,既然这两个方法得到的结果都是相同的,那么Android为什么要提供两个功能重复的方法呢?实际上这两个方法在作用域上有比较大的区别。getApplication()方法的语义性非常强,一看就知道是用来获取Application实例的,但是这个方法只有在Activity和Service中才能调用的到。那么也许在绝大多数情况下我们都是在Activity或者Service中使用Application的,但是如果在一些其它的场景,比如BroadcastReceiver中也想获得Application的实例,这时就可以借助getApplicationContext()方法了。
Context引起的内存泄露
public class CustomManager {
private static CustomManager sInstance;
public static CustomManager getInstance(Context context) {
if (sInstance == null) {
sInstance = new CustomManager(context);
}
return sInstance;
}
private Context mContext;
private CustomManager(Context context) {
mContext = context;
}
}
这是很常见的单例写法,内部持有一个Context引用,当我们某Activity在使用该封装类时,为了方便会直接使用Activity.this传进该类中,问题来了,该类的sInstance是一个静态强引用,然而内部引用了一个Activity的Context,换一句话说,项目一直活着,sInstance就会一直劫持该Context,内存无法被回收,因此导致内存泄露。
那么我们解决方法就是:
public class CustomManager {
private static CustomManager sInstance;
public static CustomManager getInstance(Context context) {
if (sInstance == null) {
//Always pass in the Application Context
sInstance = new CustomManager(context.getApplicationContext());
}
return sInstance;
}
private Context mContext;
private CustomManager(Context context) {
mContext = context;
}
}
将封装类的context.getApplicationContext,让ApplicationContext指向这个静态sInstance单例,sInstance与ApplicationContext的生命周期一直,这样就可以解决了内存泄露的问题,Activity也能finish掉后正常让垃圾回收机制回收。
一般Context造成的内存泄漏,几乎都是当Context销毁的时候,却因为被引用导致销毁失败,而Application的Context对象可以理解为随着进程存在的,那么使用Context的正确姿势是:
1:当Application的Context能搞定的情况下,并且生命周期长的对象,优先使用Application的Context。
2:不要让生命周期长于Activity的对象持有到Activity的引用。
3:尽量不要在Activity中使用非静态内部类,因为非静态内部类会隐式持有外部类实例的引用,如果使用静态内部类,将外部实例引用作为弱引用持有。
最后总结下,Context的分析基本就如上述内容,Context伴随着我们每天的开发,我们需要的是在使用Context的时候,注意使用Activity.Context是否合适,会不会造成内存泄露的可能。