0%

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private final String mBasePackageName; 
private final String mOpPackageName; //软件包名
private final Resources mResources;
private final ResourcesManager mResourcesManager; //用于管理资源文件
private final Display mDisplay; //为View填充等提供屏幕尺寸、像素密度等信息
private final DisplayAdjustments mDisplayAdjustments = new DisplayAdjustments();
private Resources.Theme mTheme = null; //主题信息
private File mCacheDir;
@GuardedBy("mSync")
private File mCodeCacheDir;
...
@GuardedBy("mSync")
private File[] mExternalObbDirs;
@GuardedBy("mSync")
private File[] mExternalFilesDirs;
@GuardedBy("mSync")
private File[] mExternalCacheDirs;
@GuardedBy("mSync")
private File[] mExternalMediaDirs; //各种文件路径

这些域的存在为功能提供了必要的信息,例如在LayoutInflater填充View时需要一个context作为参数,我们查看这个context如何被使用:

1
final XmlResourceParser childParser = context.getResources().getLayout(layout);

我们传入的ResourceId最终会被通过contextgetResource()方法获取的Resource对象的getLayout()方法定位到对应的xml文件提供给Inflater进行解析。

1
2
3
4
5
6
7
8
9
// Apply a theme wrapper, if allowed and one is specified.
if (!ignoreThemeAttr) {
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
}

在这里调用了contextobtainStyledAttributes()方法:

1
2
3
4
public final TypedArray obtainStyledAttributes(
AttributeSet set, @StyleableRes int[] attrs) {
return getTheme().obtainStyledAttributes(set, attrs, 0, 0);
}

最终使用了context中存放的主题信息为填充的view设置属性。

现在我们知道,我们存放在res文件夹下的内容(布局文件、字符串文件、图片、主题……)都需要通过一个context去向系统获取。

那么为什么在启动activity、启动service、发送广播时都需要使用context呢?因为这些操作与系统是紧密相关的,我们知道启动这些东西都需要使用一个叫intent的东西(关于intent的内容会在另外的文章讲),以startActivity()方法为例,我们一路向上追溯,可以发现启动activity最终是由AcitivityManagerNative.getDefault()的本地方法startActivity()执行的:

1
2
3
4
5
6
7
8
9
10
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess();
int result = ActivityManagerNative.getDefault().startActivity(
whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()), token,
target != null ? target.mEmbeddedID : null, requestCode, 0, null, options);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
}

这个时候我们发现,传入的context已经变成了上面代码中的who,利用这个 context获取了包名与方法的第四个参数who.getContentResolver()。它的作用是提供信息来解析intentMIME type,帮助系统决定intent的目标。

可以看到context在这里同样起到了一个提供必要信息的作用。

Context的作用

在这里再重复一遍上面说过的话,配合之前的例子,是不是可以更好地理解了呢?

我们可以把它看作是一个连接我们代码与安卓系统的“桥梁”,我们开发的应用是与运行在设备上的操作系统紧密相关的,只能通过操作系统,我们才能去启动一个新的Activity,向其他应用发送广播,启动一个新的Service或是访问我们存放在apk中的资源文件。

Context就是系统为我们提供上述功能的一个接口,我们需要使用它去完成与系统的信息交换。

Context的使用

Context分类

Context并不是都是相同的,根据获取方式的不同,我们得到的Context的各类也有所不同。

Activity/Service

我们知道Acitivity类继承自ContextThemeWrapperContextThemeWrapper继承自ContextWrapper,最后ContextWrapper继承自Context。顾名思义,ContextWrapperContextThemeWrapper只是将Context进行了再次的包装,加入了更多的信息,同时对一些方法做了转发。

所以我们在ActivityService中需要Context时就可以直接使用this,因为它们本身就是Context

当系统创建一个新的Activity/Service实例时,它也会创建一个新的ContextImpl实例来封装所有的信息。

对于每一个Activity/Service实例,它们的基础Context都是独立的。

Application

Application同样继承于ContextWrapper,但是Application本身是以单例模式运行在应用进程中的,它可以被任何Activity/ServicegetApplication()或是被任何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
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/Serivce,那么几乎可以肯定的是,这个Activity/Service将不会在结束以后被垃圾回收。如果是一个Activity,那么这意味着与它相关联的View或是其他庞大的类都将留在内存中而不会被回收。

为了避免这样的问题,我们可以改正这个单例:

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;
}
}

我们只修改了一处,第7行中我们使用context.getApplicationContext()这个方法来获取Application这个单例,而不是直接保存context本身,这样就可以保证不会出现某context因为被这个单例引用而不能回收的情况。而Application本身是单例这个特性保证了生命周期的一致,不会造成内存的浪费。

为什么不总是使用application作为context

既然它是一个单例,那么我们为什么不直接在任何地方都只使用它呢?

这是因为各种context的能力有所不同:

(图片出处见文末)

对几个注解的地方作说明:

  1. 一个application可以启动一个activity,但是需要新建一个task,在特殊情况下可以这么做,但是这不是一个好的行为因为这会导致一个不寻常的返回栈。
  2. 虽然这是合法的,但是会导致填充出来的view使用系统默认的主题而不是我们设置的主题。
  3. 如果接收器是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操作)。

Reference

https://possiblemobile.com/2013/06/context/

https://web.archive.org/web/20170621005334/http://levinotik.tumblr.com/post/15783237959/demystifying-context-in-android

http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/5.1.1_r1/android/app/ContextImpl.java?av=f

欢迎关注我的其它发布渠道