Android Context理解与陷阱
Context?
Context在安卓开发时是一个非常常见的组件,我们会在许多地方使用它,举一些例子:
- 启动新的
ActivityService - 发送广播,接收广播
- 填充
View - 获取资源
相信每一个开发者在看见它时都有过这样一些疑问:
-
Context是什么 -
Context的作用 -
Context从哪里来
同时,我们也经历过需要一个Context但不知道如何去正确获取/传递的情况,事实上不正确地保存一个Context的引用可能会导致部分内存不能被正确GC从而造成事实上的内存泄漏。
本文将着重对上面这些内容进行讲解。
Context的定义
字面上解释,Context意为“环境”,这个解释比较符合它的作用。
官方文档中对Context的解释是:
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.
关于应用环境的全局信息的接口。它是一个抽象类,具体由安卓系统来实现。它允许我们去访问特定的应用的资源和类,同时也可以经由它去向上请求应用级别的操作例如启动
Activity、发送广播、接收intents等等。
我们可以把它看作是一个连接我们代码与安卓系统的“桥梁”,我们开发的应用是与运行在设备上的操作系统紧密相关的,只能通过操作系统,我们才能去启动一个新的Activity,向其他应用发送广播,启动一个新的Service或是访问我们存放在apk中的资源文件。
Context就是系统为我们提供上述功能的一个接口,我们需要使用它去完成与系统的信息交换。
Context从哪里来
Context作为一个依赖于系统的类,SDK中只给了我们一个抽象类,具体的实现由系统完成,下文举例使用的ContextImpl就是AOSP中安卓源码对于Context的一个实现。
Context的作用
Context中封装的信息
我们可以看看Context里面包含了哪些东西(部分)。
1 | private final String mBasePackageName; |
这些域的存在为功能提供了必要的信息,例如在LayoutInflater填充View时需要一个context作为参数,我们查看这个context如何被使用:
1 | final XmlResourceParser childParser = context.getResources().getLayout(layout); |
我们传入的ResourceId最终会被通过context的getResource()方法获取的Resource对象的getLayout()方法定位到对应的xml文件提供给Inflater进行解析。
1 | // Apply a theme wrapper, if allowed and one is specified. |
在这里调用了context的obtainStyledAttributes()方法:
1 | public final TypedArray obtainStyledAttributes( |
最终使用了context中存放的主题信息为填充的view设置属性。
现在我们知道,我们存放在res文件夹下的内容(布局文件、字符串文件、图片、主题……)都需要通过一个context去向系统获取。
那么为什么在启动activity、启动service、发送广播时都需要使用context呢?因为这些操作与系统是紧密相关的,我们知道启动这些东西都需要使用一个叫intent的东西(关于intent的内容会在另外的文章讲),以startActivity()方法为例,我们一路向上追溯,可以发现启动activity最终是由AcitivityManagerNative.getDefault()的本地方法startActivity()执行的:
1 | try { |
这个时候我们发现,传入的context已经变成了上面代码中的who,利用这个 context获取了包名与方法的第四个参数who.getContentResolver()。它的作用是提供信息来解析intent的MIME type,帮助系统决定intent的目标。
可以看到context在这里同样起到了一个提供必要信息的作用。
Context的作用
在这里再重复一遍上面说过的话,配合之前的例子,是不是可以更好地理解了呢?
我们可以把它看作是一个连接我们代码与安卓系统的“桥梁”,我们开发的应用是与运行在设备上的操作系统紧密相关的,只能通过操作系统,我们才能去启动一个新的
Activity,向其他应用发送广播,启动一个新的Service或是访问我们存放在apk中的资源文件。
Context就是系统为我们提供上述功能的一个接口,我们需要使用它去完成与系统的信息交换。
Context的使用
Context分类
Context并不是都是相同的,根据获取方式的不同,我们得到的Context的各类也有所不同。
Activity/Service
我们知道Acitivity类继承自ContextThemeWrapper,ContextThemeWrapper继承自ContextWrapper,最后ContextWrapper继承自Context。顾名思义,ContextWrapper与ContextThemeWrapper只是将Context进行了再次的包装,加入了更多的信息,同时对一些方法做了转发。
所以我们在Activity或Service中需要Context时就可以直接使用this,因为它们本身就是Context。
当系统创建一个新的Activity/Service实例时,它也会创建一个新的ContextImpl实例来封装所有的信息。
对于每一个Activity/Service实例,它们的基础Context都是独立的。
Application
Application同样继承于ContextWrapper,但是Application本身是以单例模式运行在应用进程中的,它可以被任何Activity/Service用getApplication()或是被任何Context使用getApplicationContext()方法获取。
不管使用什么方法去获取Application,获取的总是同一个Application实例。
BroadcastReciver
BroadcastReciver本身并不是一个Context或在内部保存了一个Context,但是系统会在每次调用其onRecive()方法时向它传递一个Context对象,这个Context对象是一个ReceiverRestrictedContext(接收器限定Context),与普通Context不同在它的registerReceiver()与bindSerivce()方法是被禁止使用的,这意味着我们不能在onRecive()方法中调用该Context的这两个方法。
每次调用onReceive()方法传递的Context都是全新的。
ContentProvider
它本身同样不是一个Context,但它在创建时会被赋予一个Context并可以通过getContext()方法获取。
如果这个内容提供器运行在调用它的应用中,将会返回该应用的Application单例,如果它是由其他应用提供的,返回的Context将会是一个新创建的表示其他应用环境的Context。
使用Context时的陷阱
现在我们知道Context的几种分类,其实上面的分类也就是我们获取它的方式。着重标出的内容说明了它们被提供的来源,也暗指了它们的生命周期。
我们常常会在类中保存对Context的引用,但是我们要考虑生命周期的问题:如果被引用的这个Context是一个Acitivity,如果存放这个引用的类的生命周期大于Activity的生命周期,那么Activity在停止使用之后还被这个类引用着,就会引致无法被GC,造成事实上的内存泄露。
举一个例子,如果使用下面的一个单例来保存Context的引用来加载资源:
1 | public class CustomManager { |
这段程序的问题在于不知道传入的Context会是什么类型的,可能在初始化的时候传入的是一个Activity/Serivce,那么几乎可以肯定的是,这个Activity/Service将不会在结束以后被垃圾回收。如果是一个Activity,那么这意味着与它相关联的View或是其他庞大的类都将留在内存中而不会被回收。
为了避免这样的问题,我们可以改正这个单例:
1 | public class CustomManager { |
我们只修改了一处,第7行中我们使用context.getApplicationContext()这个方法来获取Application这个单例,而不是直接保存context本身,这样就可以保证不会出现某context因为被这个单例引用而不能回收的情况。而Application本身是单例这个特性保证了生命周期的一致,不会造成内存的浪费。
为什么不总是使用application作为context
既然它是一个单例,那么我们为什么不直接在任何地方都只使用它呢?
这是因为各种context的能力有所不同:

(图片出处见文末)
对几个注解的地方作说明:
- 一个
application可以启动一个activity,但是需要新建一个task,在特殊情况下可以这么做,但是这不是一个好的行为因为这会导致一个不寻常的返回栈。 - 虽然这是合法的,但是会导致填充出来的
view使用系统默认的主题而不是我们设置的主题。 - 如果接收器是
null的话是被允许的,通常在4.2及以上的版本中用来获取一个粘性广播的当前值。
我们可以发现与UI有关的操作除activity之外都不能完成,在其他地方这些context能做的事情都差不多。
但是我们回过头来想,这三个与UI相关的操作一般都不会在一个activity之外进行,这个特性很大程度上就是系统为我们设计成这样的,如果我们试图去用一个Application去显示一个dialog就会导致异常的抛出和应用的崩溃。
对上面的第二点再进一步解释,虽然我们可以使用application作为context去填充一个view,但是这样填充出的view使用的将会是系统默认的主题,这是因为只有acitivity中才会存有我们定义在manifest中的主题信息,其他的context将会使用默认的主题去填充view。
如何使用正确的Context
既然我们不能将Activity作为context保存在另外一个比该Activity生命周期长的类中,那么如果我们需要在这个类中完成与UI有关的操作(比如显示一个dialog)该怎么办?
如果真的遇到了这样的情况:我们不得不保存一个activity在一个比该Activity生命周期长的类中以进行UI操作,就说明我们的设计是有问题的,系统的设计决定了我们不应该去进行这样的操作。
所以我们可以得出结论:
我们应该在Activity/Service的生命周期范围内直接使用该Activity/Service作为context,在它们的范围之外的类,应该使用Application单例这个context(并且不应该出现UI操作)。