前言
我们在做开发的时候经常要传递 Context 类型的参数,一般称它为语境、上下文。
Google 文档对它的解释是:
Interface to global information about an application environment. This is an abstract class whose implementation is provided by the Android system. It allows access to application-specific resources and classes, as well as up-calls for application-level operations such as launching activities, broadcasting and receiving intents, etc.
Context 提供了关于应用环境全局信息的接口。这是一个由 Android 系统提供实现的抽象类。它允许访问特定于应用程序的资源和类,以及如启动活动、广播和接收 intent 等应用程序级操作的上调。
1. 什么是 Context?
Android 应用模型是基于组件的应用设计模式,组件的运行要有一个完整的 Android 工程环境,在这个环境下,Activity、Service 等系统组件才能够正常工作,而这些组件并不能采用普通的 Java 对象创建方式,new 一下就能创建实例了,而是要让它们有各自的上下文环境,也就是我们这里讨论的 Context。可以这样讲,Context 是维持 Android 程序中各组件能够正常工作的一个核心功能类。
2. Context 类继承结构
3. 一个应用程序中有多少个 Context?
从上面 Context 的类关系图中,我们可以得出答案。
在应用程序中,Context 的具体实现子类就是:Activity、Service、Application。所以:
Context 数量 = Activity 数量 + Service 数量 + Application 数量(1个)
我们常说四大组件,这里怎么只有 Activity,Service 持有 Context,而 Broadcast Receiver,Content Provider 却没有呢?
这是因为 Broadcast Receiver,Content Provider 并不是 Context
的子类,它们所持有的 Context 都是从其他地方传过去的,所以并不计入 Context 总数。
4. Context 的作用域
虽然 Context 能做事情太多了,但是并不是随便一个 Context 实例都可以为所欲为,它还是有一些规则限制的。
在绝大多数场景下,Activity、Service 和 Application 这三种类型的 Context 都是可以通用的。不过有几种场景比较特殊:
比如启动 Activity,还有弹出 Dialog。出于安全原因的考虑,Android 是不允许 Activity 或 Dialog 凭空出现的,一个 Activity 的启动必须要建立在另一个 Activity 的基础之上,也就是以此形成返回栈。
而 Dialog 则必须在一个 Activity 上面弹出(除非是
System Alert
类型的 Dialog),因此在这种场景下,我们只能使用 Activity 类型的Context,否则将会出错。这是因为 Dialog 是在 Window 上显示的,而 Activity 有挂载 Window,Service 和 Application 没有。
从上图我们可以发现 Activity 所持有的 Context 的作用域最广。
因为 Activity 继承自 ContextThemeWrapper,而 Application 和
Service 继承自 ContextWrapper,很显然 ContextThemeWrapper
在 ContextWrapper 的基础上又做了一些操作使得 Activity 变得更强大。
这里解释一下上图中 Application 和 Service 所不推荐的两种使用情况:
- 如果我们用 ApplicationContext 去启动一个 LaunchMode 为 standard 的 Activity 的时候会报错:
android.util.AndroidRuntimeException:
Calling startActivity from outside of an Activity context requires the
FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?
这是因为非 Activity 类型的 Context 并没有所谓的任务栈,所以待启动的 Activity 就找不到栈了。
解决这个问题的方法就是:为待启动的 Activity 指定
FLAG_ACTIVITY_NEW_TASK
标记位,这样启动的时候就为它创建一个新的任务栈,而此时 Activity 是以 singleTask 模式启动的。所以这种用 Application 启动 Activity 的方式不推荐使用,Service 同理。
- 在 Application 和 Service 中去 layout inflate 也是合法的,但是会使用系统默认的主题样式,如果你自定义了某些样式可能不会被使用。所以这种方式也不推荐使用。
总之:凡是跟 UI 相关的,都应该使用 Activity 做为
Context 来处理,其他的一些操作,Service、Activity、Application 等实例都可以,当然,注意 Context 引用的持有,防止内存泄漏。
5. Context 引起的内存泄露
Context 并不能随便乱用,用的不好有可能会引起内存泄露的问题,下面介绍两种错误的引用方式。
错误的单例模式:
public class Singleton {
private static Singleton ourInstance;
private Context mContext;
private Singleton(Context context) {
mContext = context;
}
public static Singleton getInstance(Context context) {
if (ourInstance == null) {
ourInstance = new Singleton(context);
}
return ourInstance;
}
}
这是一个非线程安全的单例模式,ourInstance 作为静态对象,其生命周期要长于普通的对象,其中包含一个 Context,假如 Activity A 去 getInstance 获得 instance 对象,传入 this,常驻内存的 Singleton 保存了你传入的 Activity A 对象,并一直持有,即使 Activity 被销毁掉,但因为它的引用还存在于一个 Singleton 中,就不可能被 GC 掉,这样就导致了内存泄漏。
6. 如何正确使用 Context?
一般 Context 容易造成的内存泄漏,几乎都是当 Context 销毁的时候,却因为被引用导致销毁失败,而 Application 的 Context 对象可以理解为随着进程存在的,所以我们总结出使用 Context 的正确方法:
当 Application 的 Context 能搞定的情况下,并且生命周期长的对象,优先使用 Application 的 Context。
不要让生命周期长于 Activity 的对象持有到 Activity 的引用。
尽量不要在 Activity 中使用非静态内部类,因为非静态内部类会隐式持有外部类实例的引用,如果使用静态内部类,将外部实例引用作为弱引用持有。
7. 如何获取 Context?
通常我们想要获取 Context 对象,主要有以下四种方法:
View.getContext()
返回当前 View 对象的 Context 对象,通常是当前正在展示的 Activity 对象;Activity.getApplicationContext()
获取当前 Activity 所在的应用进程的 Context 对象,通常我们使用 Context 对象时,要优先考虑这个全局的进程 Context;ContextWrapper.getBaseContext()
用来获取一个
ContextWrapper 进行装饰之前的 Context,可以使用这个方法,这个方法在实际开发中使用并不多,也不建议使用;Activity.this
返回当前的 Activity 实例,如果是 UI 控件需要使用 Activity 作为 Context 对象,但是默认的 Toast 实际上使用
ApplicationContext 也可以。
8. getApplication() 和 getApplicationContext() 的区别?
获取当前 Application 对象用 getApplicationContext()
,那么
getApplication()
呢,这两个方法有什么区别?
通过上面的代码,打印得出两者的内存地址都是相同的,所以它们是同一个对象。其实这个结果也很好理解,前面已经说过了,Application 本身就是一个 Context,所以这里获取的
getApplicationContext()
得到的结果就是 Application 本身的实例。
那么 Android 为什么要提供两个功能重复的方法呢?
实际上这两个方法在作用域上有比较大的区别。
getApplication()
方法的语义性非常强,一看就知道是用来获取Application 实例的,但是这个方法只有在 Activity 和 Service 中才能调用的到。但是如果在一些其它的场景,比如 Broadcast Receiver 中也想获得 Application 的实例,就只能借助
getApplicationContext()
方法了。
9. getBaseContext()
getBaseContext()
返回由构造函数指定或 setBaseContext()
设置的上下文。如果你想从一个 Context 访问到另一个 Context 就用该方法。
public class ContextWrapper extends Context {
Context mBase;
public ContextWrapper(Context base) {
mBase = base;
}
......
/**
* @return the base context as set by the constructor or setBaseContext
*/
public Context getBaseContext() {
return mBase;
}
......
10. getContext()
getContext()
返回的是当前运行 view 的 context,通常是当前活动的 activity 的 context。使用 getContext()
获取的是当前对象所在的 Context。
View 类里面的 getContext()
方法。
/**
* Returns the context the view is running in, through which it can
* access the current theme, resources, etc.
*
* @return The view's Context.
*/
@ViewDebug.CapturedViewProperty
public final Context getContext() {
return mContext;
}