#原文地址 : Context,What Context?

#引言
Context可能是安卓程序中使用频率最高的元素,同时,也可能是被误用最多的元素.

Context对象非常普遍,并会被频繁地传递,这会使你遇到一些未曾预料的问题.加载资源,启动一个新的Activity,获取一个系统服务,获取内部文件的路径以及新建View都需要使用Context来完成(而这些,也只是Context全部作用的冰山一角).关于Context如何工作,我有一些见解和建议,希望能使你在应用中更有效地使用它.


#Context的类型
Context的实例并非完全相同.不同的组件,你能使用的Context也不同:

##Application
Application 在你的应用中是单例的.在Activity或Service中,你可以通过方法getApplication()获取它.而在其他Context的子类中,这个方法叫做getApplicationContext().但无论你从哪里获取到它,你获得的都是同一个实例.

##Activity/Service
Activity/Service 继承自ContextWrapper. ContextWrapper实现了一些API.其所有方法都只是简单封装了其内部的一个Context实例的方法.任何时候,框架层创建一个新的Activity或Service实例,都会同时创建一个ContextImpl实例来承担所有繁重的代码并将其封装.无论是Activity,还是Service,抑或是他们相应的base context实例,都是一个独立的实例.

##BroadcastReceiver
BroadcastReceiver本身并没有Context对象,但每次有广播事件时,系统会通过onReceive()方法传递一个Context给它.这是一个ReceiverRestrictedContext的实例,它有两个功能无法使用:registerReceiver()和bindService().这两个功能在现存的BroadcastReceiver.onReceive()中是被禁止的.广播接收器每次处理广播,传进来的Context对象都是新建的.

##ContentProvider
ContentProvider 同样不是一个Context对象,但在其创建时框架层会传入一个Context对象,可以通过方法getContext()获得.如果ContentProvider与调用者运行在同一个地方(比如同一个应用进程),这个方法返回的便是同一个Application单例.然而,如果两者运行在不同进程,此方法便会新建一个实例,来表示提供者所运行的程序包.


#持有引用
我们需要注意的第一个问题,是在一个对象或类中持有一个Context的引用时,他们的生命周期可能长于这个Context.例如,当你新建一个单例,可能需要一个Context对象来加载资源或使用ContentProvider,由此便会在此单例中持有一个当前Activity或Service的引用.

##Bad Singleton

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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或Service的引用是不安全的.原因在于这个单例是由一个被封装起来的静态成员变量来管理的.这就意味着,这个对象和它关联的所有对象都不会被垃圾回收器回收.如果这个Context是一个Activity,那我们实际上将其所有view和其他潜在的与其关联的大对象保存在了内存中,由此便引发了内存泄露.

为了避免这个问题,我们修改单例,让其引用一个application context:

##Better Singleton

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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从哪来已经不重要了,因为我们引用的application context是绝对安全的.Application context本来就是一个单例,我们在静态引用它的过程中不必担心任何泄漏.同样,当你在一个正在运行的子线程或正在等待的Handler中引用Context时,这么做也是安全的.

那么为什么我们不能在所有的地方都选择引用Application Context呢?为什么不能将中间的引用去掉,那我们就永远不用再担心内存泄露了?答案我在前面已经提到过了,那就是Context之间并不相同.


#Context的功能
Context从何而来决定了你用它进行哪些操作是安全的.下面这个表格标明了一些会使用到Context的地方以及每种Context在这里是否可用.

Application Activity Service ContentProvider BroadcastReceiver
Show a Dialog NO YES NO NO NO
Start an Activity NO^1 YES NO^2 NO^3 NO^4
Layout Inflation NO^5 YES NO^6 NO^7 NO^8
Start a Service YES YES YES YES YES
Bind to a Service YES YES YES YES NO
Send a Broadcast YES YES YES YES YES
Register BroadcastReceiver YES YES YES YES NO[^9]
Load Resource Values YES YES YES YES YES

[^9]:在Android4.2及以上的系统上,当receiver为null时,此操作是被允许的.这可以被用来获取一个sticky broadcast当前的值.


#UI
从上面的表格可以看出,有些功能不适合使用application context来操作,这些功能都和UI有关(译者注:Toast可以用Application context).实际上,唯一一个可以处理所有UI任务的是Activity,其他Context都或多或少有不能使用的地方.

幸运的是,这三个UI操作的使用范围都局限于Activity,Android框架似乎故意这样设计.如果你尝试用application去显示一个对话框或启动一个Activity,系统会直接抛出一个异常,来告诉你有地方出错了.

而实例化布局时,问题就没那么明显了.如果你读过我上一篇关于布局实例化的文章,你就会知道这是一个有些神秘的过程.它有着一些隐秘的操作,而使用正确的Context关联着其中一个操作.当你使用application context去实例化布局时,框架层可不会有什么抱怨并返回一个完美的view层级给你,你的app里的theme和style也不会被使用.这是因为Activity是唯一一种你在清单文件定义过theme的context类型.其他类型的context会使用系统默认的theme来实例化view,这可能导致view显示异常.


#结论

一定会有人觉得很矛盾.当前的系统设计使我们必须保存一个长期的引用.另外,我们还必须保存一个Activity的引用,来完成一些更新UI的任务.在这种情况下,我强烈建议你重新思考你的设计来规避这种问题,这说不定会成为你与框架层斗智斗勇的典型案例.

大多数情况下,直接使用当前所在组件的Context是可行的.只要一个对象没有超过这个组件的生命周期,你就能安全地持有这个Context.而一旦这个对象的生命周期超过了所在的Activity或Service,即便只是暂时地,你也应该选择使用application context.