目录
- 原文在此
- 前言
- Context类型
- Application
- Activity/Service
- BroadcastReceiver
- ContentProvider
- 保存引用
- 非良好单例
- 改进
- Context功能
- Context能什么取决于该Context创建于哪里
- 用户界面UI
- 规则
- 经验
原文在此
前言
Context可能是Android应用程序中最常用的元素…同时也最容易被误用
Context对象非常普遍和常用,它可能会发生出人预料的情形. 加载资源、启动Activity、获得系统服务、获取内部文件路径、创建View等等都需要Context的支持才能完成. 接下来我会为你提供一些和Context工作原理相关的说明,希望能帮助你更有效的开发
Context类型
Context实例的创建方式不是唯一的. 根据Android组件的不同,创建出的Context实例是有细微区别的
Application
Context在APP中以单例模式存在. 可以通过不同的方式得到Context实例,如在Activity或Service中的getApplication()
、或者从其他继承了Context的任何子类调用getApplicationContext()
获取. 无论从何处以何种方式得到,Context在一个APP的所有进程中是只有唯一实例
Activity/Service
继承自ContextWrapper并实现了相同的API,代理了Context实例的所有内部隐藏的回调方法. 当Framework层创建新的Activity或Service实例时,同样会创建ContentImpl实例来完成其他组件内部的重型任务. 任何Activity和Service以及他们之间的通信都基于在每个进程中只有唯一实例的Context对象
BroadcastReceiver
本身没有Context实例,但是Framework层会在在每次广播事件发生时的onReceive()方法中传递Context实例进来. 该实例是受限制的,有两个主要功能(registerReceiver()
、bindService()
)被阉割了. 这两个功能不能从现有的BroadcastReceiver.onReceive()
调用. 每次接受者处理广播,Context会传递进来一个新的实例
ContentProvider
本身没有Context实例,不过可以通过getContext()
获取. 如果ContentProvider运行在本应用程序内,则会获取Application单例. 如果该ContentProvider与调用者不在同一进程,则会创建新的实例代表provider所运行的包
保存引用
首要解决的问题是Context的生命周期可能会比其所在的对象的生命周期要长. 例如,创建一个自定义单例,其需要Context对象加载资源或者获取ContentProvider,并保存引用的Activity和Service的单例
非良好单例
这里的问题在于,我们不知道这个Context是从哪里来的,并且如果保存一个最终指向的是Activity或者Servece的引用是并不安全的. 这是一个问题,是因为一个单例在类的内部维持一个唯一的静态引用,这意味着我们的对象,以及所有其他它所引用的对象,将永远不能被垃圾回收. 假如这个Context是一个Activity,我们将保存与这个Activity相关的所有的view以及其他大的对象,从而造成内存泄漏
改进
为了解决这个问题,我们修改单例永远只是保存Application Context
现在这个例子中,我们的Context来自哪里都没有关系,因为我们这里保存引用是安全的. Application Context 本身就是一个单例,所以我们再创建另外一个static引用,不会造成任何内存泄漏. 另外一个很好的例子是,在后台线程或者一个等待的Handler中保存Context的引用,也可以使用这样的方法
为什么我们不能总是引用Application Context呢?正如前面说的,引用Application Context永远不用担心内存泄漏的问题. 问题的答案,就像我在开始的介绍中说的,是因为不同context并不是等价的
Context功能
Context能什么取决于该Context创建于哪里
- | Application | Activity | Service | ContentProvider | BroadcastReceiver |
显示Dialog | NO | YES | NO | NO | NO |
启动Activity | NO1 | YES | NO1 | NO1 | NO1 |
Layout Inflation | NO2 | YES | NO2 | NO2 | NO2 |
启动Service | YES | YES | YES | YES | YES |
绑定到Service | YES | YES | YES | YES | NO |
发送Broadcast | YES | YES | YES | YES | YES |
注册BroadcastReceiver | YES | YES | YES | YES | NO3 |
加载Resource | YES | YES | YES | YES | YES |
说明
- NO1 表示Application context的确可以开始一个Activity,但是它需要创建一个新的task. 这可能会满足一些特定的需求,但是在你的应用中会创建一个不标准的回退栈(back stack),这通常是不推荐的或者不是是好的实践
- NO2 表示这是非法的,但是这个填充(inflation)的确可以完成,但是是使用所运行的系统默认的主题(theme),而不是你app定义的主题
- NO3 在Android4.2以上,如果Receiver是null的话(这是用来获取一个sticky broadcast的当前 值的),这是允许的
用户界面UI
从前面的表格中可以看到,application context有很多功能并不是合适去做,而这些功能都与UI相关. 实际上,只有Activity能够处理所有与UI相关的任务. 其他类别的Context实例功能都差不多
幸运的是,在应用中这三种操作基本上都不需要在Activity范围之外进行,这很可能是android框架故意这么设计的. 尝试显示一个使用Aplication Context创建的Dialog,或者使用Application Context开始一个Activity,系统会抛出一个异常,让你的Application崩溃,非常强的告诉你某些地方出了问题
一个并不明显的问题是填充布局(inflating layout). 如果你已经读过了我(原文作者)的上一篇文章Layout inflation,你就已经知道它可能是一个非常神秘过程,伴随一些隐藏的行为. 使用正确的context关系到其中的一个行为。当你使用Application context来inflate一个布局的时候,框架并不会报错,并返回一个使用系统默认的主题创建一个完美的view给你,而没有考虑你的Applicaiton自定义的theme和style. 这是因为Acitivity是唯一的绑定了在manifast文件种定义主题的Context. 其他的Context实例将会使用系统默认的主题来inflater你的view. 导致显示的结果并不是你所希望的
规则
可能有些读者已经得出两个规则互相矛盾的结论. 可能有些情况下,在某些Application的设计中,我们可能既必须长期保存一个的引用,并且为了完成与UI相关的工作又必须保存一个Activity. 如果出现这种情况,我将会强烈建议你重新考虑你的设计,它将是一个很好的“反框架”教材
经验
通常,使用组件内能够直接获取的Context. 只要该引用没有超过所在组件的生命周期,便可以安全的保存该引用. 一旦超过了Activity或者Service的生命周期范围,就应该将Context引用转换为为Application Context