Android Context理解与陷阱
Context?
Context
在安卓开发时是一个非常常见的组件,我们会在许多地方使用它,举一些例子:
- 启动新的
Activity
Service
- 发送广播,接收广播
- 填充
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
操作)。