译文《Context,到底什么是Context?》

本文译自《Context, What Context?》
注:文中提到的“导入布局”,即是指利用LayoutInflater来inflate layout的操作。


Context类对于做Android开发的同学肯定不陌生,但或许许多同学都没有正确地使用Context实例。

Context实例非常常见,在许多的情境下(加载资源、启动一个Activity、取得一个系统级的Service、取得应用独有的文件存储路径还有创建View等)都需要用到一个Context实例,但如果不加区分地使用任意的Context实例,很容易会导致一些没意料到的状况发生。

Context的种类


并不是所有的Context实例都是一样的构造流程。常见的Context子类如下所列:

  • Application——在你的应用进程中单例存在的一个实例。可以通过Activity或Service的getApplication()方法或者其他任意Context子类的getApplicationContext()方法来取得。不论是在哪里以及何时取得的Application实例,它都是进程唯一的。

  • Activity/Service——继承自ContextWrapper类,它们实现了与Context类同样的API,但代理了所有的方法到一个对外不可见的Context实例,也就是它们的base Context。每当系统框架创建一个新的Activity或者Service实例时,它同时也会创建一个ContextImpl实例去执行不同的组件所需要做的不同逻辑。每个Activity或Service,以及它们相应的base context,都是实例唯一的。

  • BroadcastReceiver——这并不是一个Context子类。但每个Receiver都会实现onReceive(Context context, Intent intent)这个回调方法,每次系统发送通知都是调用到这个回调方法,这里就给Receiver传入了一个Context实例。这里传入的Context实例又与其他的Context实例不一样,这里传入的Context实例是不能调用registerReceiver()方法和bindService()方法的。每次发送一个通知的时候,这里传入的Context实例都是不一样的。

  • ContentProvider——这同样也不是一个Context子类。但它内部持有一个Context实例,这个实例可以通过getContext()方法取得。如果ContentProvider与调用者是运行在同一个进程中,那么它的getContext()方法返回的Context实例其实就是这个进程里的始终单例的Application Context。不过如果ContentProvider与调用者是运行在不同的进程中的,如应用A去调用应用B的ContentProvider,那么这时候ContentProvider的getContext()方法返回的则是应用B里的Application Context。

引用的保存


呐,我们先来说说非常常见的一种保存Context实例的引用从而导致内存泄漏的情形:一个实例或一个类,它保存了一个生命周期比自己短的Context实例,这就会导致内存泄漏。举个例子,创建一个需要依赖一个Context实例的单例类来进行一些通用操作如加载资源、调用一个ContentProvider,并把当前Activity或者Service作为它依赖的Context实例设置进去。

错误单例的示范

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参数是啥Context,所以对于我们这个单例来说直接保存这个Context的引用是很危险的(例如这里的Context是一个Activity或者Service的时候)。因为单例里面的对象是静态的,这就会导致它引用的所有资源都不会被系统GC回收掉,假设这里的Context是一个Activity的话,我们这样做就会导致这个Activity相关的View啊还有别的占内存的对象一直不能被系统回收掉,进而导致了内存泄漏。
为了避免这种情况,我们在下面的单例中改为始终是保存Application Context的引用。

正确单例的示范

public class CustomManager {
    private static CustomManager sInstance;
 
    public static CustomManager getInstance(Context context) {
        if (sInstance == null) {
            //不管什么Context,都改为取Application Context
            sInstance = new CustomManager(context.getApplicationContext());
        }
        return sInstance;
    }
 
    private Context mContext;
 
    private CustomManager(Context context) {
        mContext = context;
    }
}

这样我们就不用关心传入的Context到底是什么了,因为我们现在持有的引用是Application Context。就像前文提到的,Application Context是在整个应用程序中进程单例的,所以哪怕我们在代码中对它持有静态引用也不会导致什么内存泄漏。
那,为什么我们不能总是使用Application Context来完成各处需要Context的逻辑呢?这样不就可以永不担心Context相关的内存泄漏了吗?原因其实很简单,就像我在一开头就提到的——一个Context实例并不一定能与另一个Context实例等同。

不同种类的Context的能力区别


直接参考下表即可:

|Application | Activity | Service | ContentProvider | BroadcastReceiver
---|---|---|---|---|---
构造展示一个Dialog | NO | YES | NO | NO | NO
启动一个Activity | NO1 | YES | NO1 | NO1 | NO1
导入布局文件 | NO2 | YES | NO2 | NO2 | NO2
启动一个Service | YES | YES | YES | YES | YES
绑定到一个Service | YES | YES | YES | YES | NO
发送一个广播 | YES | YES | YES | YES | YES
注册一个BroadcastReceiver | YES | YES | YES | YES | NO3
加载资源数值 | YES | YES | YES | YES | YES
附注:

  1. 一个非Activity的Context可以用于启动一个Activity,但这样启动的Activity需要新创建一个Activity堆叠栈。这个在某些特定情形下或许会适用,但这种设计一般来说都不太好。
  2. 这个其实也是可以的,但是这样导入的布局会用当前系统的默认主题来设置,而不是用你在你的应用程序中设定的主题来设置的。
  3. 在Android 4.2及以上的系统里,如果receiver是null,那这也是可以的。这样做是为了取得一个严格广播的当前值。

用户交互界面


从上表可以看出好些操作不适合使用Application Context来执行,而这些操作无一例外地全都是和用户交互界面直接相关的。适合执行这些与用户交互界面直接相关的操作的Context只有一种,那就是Activity;其他的Context其实和Application Context的功能都差不多。
不过其实这些个与UI相关的操作其实大多数时候都是在Activity中才会有执行的机会。假设使用一个非Activity的Context来调用展示一个Dialog,在调用Dialog实例的show()方法时就会报以下的错误直接崩溃:

Caused by: android.view.WindowManager$BadTokenException: Unable to add window -- token null is not for an application

又或者使用一个非Activity的Context来启动另一个Activity,同样也会报错崩溃:

Caused by: 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来导入布局,应用并不会报错崩溃。详细的流程可以参见我之前写的《布局的导入》。此时,Android框架会默默地返回你需要的布局文件对应的View,其中的各个View的层次关系都是正确正常的,只是你在应用程序中设定的主题和样式(在AndroidManifest.xml中设定的值)不会被应用到此时导入布局文件而产生的View中去,而是应用了系统默认的主题。这是因为在Manifest中定义的主题实际上是仅仅绑定到Activity这种Context上的,所以如果使用非Activity的Context实例来导入布局,那就只会应用系统默认的主题,从而导入了一个可能并不是你所期望的布局样式。

但上述规则是不是有不完善的地方?


有些同学在开发的时候会发现,依照目前的程序设计,我们的程序就是要长时间的持有一个Context实例,而且这个实例还必须是Activity,因为在这长时间的持有过程中,会涉及到UI相关的操作逻辑。那么假设真的有这种情况,我强烈建议你们重新审视你们的程序的设计,因为这种情形完全就是在对抗Android系统框架

经验总结


在大多数情形下,代码是跑在哪类Context内就使用当前可获得的这类Context即可。只要这个Context类引用并不会超脱出它所引用的组件的生命周期,那你完全可以在你的逻辑代码中持有这个引用。但是如果你需要长时持有一个Context引用,这个引用甚至会超脱你的Activity或Service的生命周期,哪怕仅仅是短暂地超脱出生命周期,也务必要把这个Context引用改为Application引用。

译者说两句


这段时间断更了抱歉。
这篇文章虽然是2013年的老博文了,但在我看来还是非常有学习价值的。这是我第一次翻译技术类文章,所以可能表述得不太好,我日后会继续努力提升翻译水平的。
依文中所说,在需要Context的时候,直接取能取到的“最近”的Context实例即可,一般情形下是不会导致内存泄漏的。举个例子,在一个Activity A里有个Fragment a,然后Fragment a里面有Adapter View,那这时候就需要透传Context实例来构造Adapter View里面的Item View了,那这时候,其实大胆地在a里面透传A的引用到Adapter中其实是没有问题的,只要不要把持有的A的引用声明为静态就好
再比如,在后台有个定时任务或者什么的,在特定时机要往SharedPreferences里面写数据啊或者要读取资源文件中的string字符串啥的,这时候就可以在定时任务的代码中长期持有一个Application Context的引用来执行相关的操作,这样也是不会引发内存泄漏的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容