一、Context 简介
Google 正式推出 Kotlin 前,主流 Android 应用都是使用 Java 语言来编写的。那么大家有没有思考过,一个 Android 程序和一个Java 程序,他们的区别在哪里?划分界限又是什么呢?其实简单点分析,Android 程序不像 Java 程序一样,随便创建一个类,写个 main() 方法就能运行了,而是要有一个完整的 Android 工程环境。在这个环境下,Activity 、Service 、 ContentProvider 、BroadcastReceiver 等系统组件并不是像普通的 Java 程序 new 一下就能创建实例了,而是要有它们各自的上下文环境,也就是我们这里讨论的 Context 。可以这样讲,Context 是维持 Android 程序中各组件能够正常工作的一个核心类。
Android 官方注释:
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.
译文:Context 是有关应用程序环境的全局信息的接口。这是一个抽象类,其实现由 Android 系统提供。 它允许访问特定于应用程序的资源和类,以及对应用程序级别操作的上调,例如启动 Activity,发送广播和接收 Intent 等。
既然 Context 是一个抽象类,那么具体使用中肯定要有它的实现类,通过查看 Android 官方文档我们得知 Context 的继承结构如下图所示:
通过关系图得知,Context 有三个具体的实现子类:MockContext 、ContextWrapper 和 ContextImpl 。 MockContext 类,正如官方注释“A mock Context class. All methods are non-functional and throw UnsupportedOperationException. You can use this to inject other dependencies, mocks, or monitors into the classes you are testing. ”,是一个虚假的 Context, 所有的方法都没有具体实现,主要用于单元测试中,这里不做过多探讨。ContextWrapper 类,如其名所言,这是一个包装类,ContextWrapper 构造函数中必须包含一个真正的 Context 引用,同时 ContextWrapper 中提供了attachBaseContext() 用于给 ContextWrapper 对象指定真正的 Context 对象,调用 ContextWrapper 的方法都会被转向其所包含的真正的 Context 对象。ContextThemeWrapper类,如其名所言,其内部包含了与主题(Theme)相关的接口,这里所说的主题是指在 AndroidManifest.xml 中通过 android:theme 为 Application 元素或者 Activity 元素指定的主题。当然只有 Activity 才需要主题,Application 是不需要主题的,因为 Application 是没有界面的,所以 Application 直接继承于 ContextWrapper,Service 同理。ContextImpl 类则真正实现了 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.
*/
public abstract class Context {
/**
* Returns an AssetManager instance for the application's package.
* <p>
* <strong>Note:</strong> Implementations of this method should return
* an AssetManager instance that is consistent with the Resources instance
* returned by {@link #getResources()}. For example, they should share the
* same {@link Configuration} object.
*
* @return an AssetManager instance for the application's package
* @see #getResources()
*/
public abstract AssetManager getAssets();
/**
* Returns a Resources instance for the application's package.
* <p>
* <strong>Note:</strong> Implementations of this method should return
* a Resources instance that is consistent with the AssetManager instance
* returned by {@link #getAssets()}. For example, they should share the
* same {@link Configuration} object.
*
* @return a Resources instance for the application's package
* @see #getAssets()
*/
public abstract Resources getResources();
/** Return PackageManager instance to find global package information. */
public abstract PackageManager getPackageManager();
...
}
MockContext 类部分源码:
/**
* A mock {@link android.content.Context} class. All methods are non-functional and throw
* {@link java.lang.UnsupportedOperationException}. You can use this to inject other dependencies,
* mocks, or monitors into the classes you are testing.
*/
public class MockContext extends Context {
@Override
public AssetManager getAssets() {
throw new UnsupportedOperationException();
}
@Override
public Resources getResources() {
throw new UnsupportedOperationException();
}
@Override
public PackageManager getPackageManager() {
throw new UnsupportedOperationException();
}
...
}
ContextWrapper 类部分源码:
/**
* Proxying implementation of Context that simply delegates all of its calls to
* another Context. Can be subclassed to modify behavior without changing
* the original Context.
*/
public class ContextWrapper extends Context {
Context mBase;
public ContextWrapper(Context base) {
mBase = base;
}
/**
* Set the base context for this ContextWrapper. All calls will then be
* delegated to the base context. Throws
* IllegalStateException if a base context has already been set.
*
* @param base The new base context for this wrapper.
*/
protected void attachBaseContext(Context base) {
if (mBase != null) {
throw new IllegalStateException("Base context already set");
}
mBase = base;
}
/**
* @return the base context as set by the constructor or setBaseContext
*/
public Context getBaseContext() {
return mBase;
}
@Override
public AssetManager getAssets() {
return mBase.getAssets();
}
@Override
public Resources getResources() {
return mBase.getResources();
}
@Override
public PackageManager getPackageManager() {
return mBase.getPackageManager();
}
...
}
ContextImpl 类部分源码:
/**
* Common implementation of Context API, which provides the base
* context object for Activity and other application components.
*/
class ContextImpl extends Context {
@Override
public AssetManager getAssets() {
return getResources().getAssets();
}
@Override
public Resources getResources() {
return mResources;
}
@Override
public PackageManager getPackageManager() {
if (mPackageManager != null) {
return mPackageManager;
}
IPackageManager pm = ActivityThread.getPackageManager();
if (pm != null) {
// Doesn't matter if we make more than one instance.
return (mPackageManager = new ApplicationPackageManager(this, pm));
}
return null;
}
...
}
小结:MockContext 类除了单元测试,没什么实际作用。ContextWrapper 类和 ContextImpl 类分工明确,其中 ContextImpl 是 Context 的具体实现类,ContextWrapper 是 Context 的包装类。Activity、Application、Service 虽都继承自 ContextWrapper,但它们初始化的过程中都会创建 ContextImpl 对象,由ContextImpl 实现 Context 中的方法。
二、Context 的应用场景
前面简单的介绍了 Context,Context 在 Android 中都有那些应用场景呢?实在太多了,比如启动 Activity 、启动 Service 、发送广播、弹出 Toast 、操作数据库等都需要用到 Context 。由于 Context 的抽象方法都是由 ContextImpl 类具体实现的,因此在绝大多数场景下 Activity、Service 和 Application 这三种类型的 Context 都是可以通用的。不过有几种场景比较特殊,比如启动 Activity 和弹出 Dialog 。出于安全原因的考虑,Android 不允许 Activity 和 Dialog 凭空出现,一个 Activity 的启动必须要建立在另一个Activity 的基础之上或者新建一个任务栈,以形成返回栈。而 Dialog 则必须在一个 Activity 上面弹出(除非是 SystemAlert 类型的 Dialog),因此在这种场景下,我们只能使用 Activity 类型的 Context,否则将会出错。
Application | Activity | Service | |
Start Activity | 不推荐 | YES | 不推荐 |
Start Service | YES | YES | YES |
Show Dialog | NO | YES | NO |
Register BroadcastReceiver | YES | YES | YES |
Send Broadcast | YES | YES | YES |
Layout Inflation | 不推荐 | YES | 不推荐 |
Load Resource Values | YES | YES | YES |
从上面图表可以发现 Activity 类型的 Context 作用域最广,因为 Activity 继承自 ContextThemeWrapper,而 Application 和 Service 继承自 ContextWrapper,很显然 ContextThemeWrapper 在 ContextWrapper 的基础上又做了一些扩展,使得 Activity 变得更强大。这里就不贴源码分析了,有兴趣的同学可以自己查看源码。上面图表中的 YES 和 NO 分别代表支持和不支持使用该类型的 Context,详细说下上图中Application 和 Service 不推荐的情况。
1)如果我们用 Application 类型的 Context 去启动一个 LaunchMode 为 standard 的 Activity 的时候会报"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 并没有所谓的任务栈,所以待启动的 Activity 找不到栈了。解决这个问题的方法就是为待启动的 Activity 指定 FLAG_ACTIVITY_NEW_TASK 标记位,这样启动的时候就为它创建一个新的任务栈,而此时新的 Activity 是以 singleTask 模式启动的。所有这种用 Application 启动 Activity 的方式不推荐使用,Service 原理。
2)在 Application 和 Service 中去 Layout Inflation 也是合法的,但是会使用系统默认的主题样式,如果你自定义了某些样式可能不会被使用,所以这种方式也不推荐使用。
小结:凡是跟 UI 相关的,都应该使用 Activity 类型的 Context 来处理;其他的一些操作,Application、Activity、Service等类型的 Context 都可以。
三、Context 的获取
1. this 关键字
Application 、Activity 、Service 类都是继承自 Context 类,在当前类获取 Context,只需要使用 this 关键字即可,但是有的时候在子线程里面需要使用类名.this 获取 Context 。
2. getApplication()
Android 官方文档对该方法的注释为"getApplication() is available to Activity and Services only. Although in current Android Activity and Service implementations, getApplication() and getApplicationContext() return the same object, there is no guarantee that this will always be the case (for example, in a specific vendor implementation). So if you want the Application class you registered in the Manifest, you should never call getApplicationContext() and cast it to your application, because it may not be the application instance (which you obviously experienced with the test framework)."大概意思就是 getApplication() 仅可见于Activity 和 Service 类。虽然在当前 Activity 和 Service 实现中,getApplication() 和 getApplicationContext() 返回相同的对象,但无法保证始终如此(例如,在特定的供应商实现中)。因此,如果您想要获取在 Manifest 中注册的 Application 类,则不应该调用 getApplicationContext() 并将其强制转换为您的应用程序,因为它可能不是应用程序实例。
3. getApplicationContext()
Android 官方文档注释为"Return the context of the single, global Application object of the current process. This generally should only be used if you need a Context whose lifecycle is separate from the current context, that is tied to the lifetime of the process rather than the current component."即返回当前进程的 Application 对象的 Context,通常只应在需要生命周期与当前上下文分离的上下文时使用,该上下文与进程的生命周期而不是当前组件相关联。通常该方法和 getApplication() 返回相同的对象,但该方法的作用域比 getApplication() 大很多,getApplication() 只能用在 Activity 和 Service 中,getApplicationContext() 还可以在 ContentProvider 、BroadcastReceiver 等组件中使用。
LogUtils.d("DHZ", "getApplication() is " + getApplication());
LogUtils.d("DHZ", "getApplicationContext() is " + getApplicationContext());
2019-05-31 18:52:48.146 20020-20020/com.medical.screen D/DHZ: getApplication() is com.medical.screen.TvApp@c2ee82b
2019-05-31 18:52:48.146 20020-20020/com.medical.screen D/DHZ: getApplicationContext() is com.medical.screen.TvApp@c2ee82b
通过日志信息验证了两者返回相同对象的结论。
4. ContextWrapper.getBaseContext()
Android 官方文档注释为"Return the base context as set by the constructor or setBaseContext",即返回 ContextWrapper 构造函数或 setBaseContext() 中赋予的 Context ,一般不建议使用。
LogUtils.d("DHZ", "getBaseContext() is " + getBaseContext());
2019-05-31 18:57:43.279 20278-20278/com.medical.screen D/DHZ: getBaseContext() is android.app.ContextImpl@6f82488
通过日志信息我们看到这个 Context 类型为 ContextImpl ,验证了我们简介中“ Activity、Application、Service 虽都继承自 ContextWrapper,但它们初始化的过程中都会创建 ContextImpl 对象,由ContextImpl 实现 Context 中的方法”的结论。
5. View.getContext()
获取当前 View 的 Context,通常为当前 View 所在的 Activity 的 Context 。
6. Fragment.getActivity()
获取当前 Fragment 关联的 Activity,如果直接关联其他的 Context,会返回 null 。
7. Activity.getParent()
如果当前 Activity 是其他 Activity 的嵌入式子 View ,获取其他Activity ,否则返回 null 。
四、常见的错误使用 Context 举例
获取 Context 的方法很多,如果使用不当,会引起严重的错误甚至导致内存泄露。
1. Application 的设计
实际开发中,基本上每一个应用都会自定义一个 Application,让它继承自 Application 类,然后在自定义的 Application 类中去封装一些通用的操作。其实这并不是 Google 所推荐的一种做法,因为这样我们只是把 Application 当成了一个通用工具类来使用了,而使用一个简单的单例类也可以实现同样的功能。如果自定义 Application 和单例模式混合在一起可能造成如下常见的错误。
public class TvApp extends Application {
private static TvApp sTvApp;
public static TvApp getInstance() {
if (sTvApp == null) {
sTvApp = new TvApp();
}
return sTvApp;
}
}
咋一看好像没什么问题,就像单例模式一样,这里提供了一个 getInstance() ,用于获取 TvApp 的实例 sTvApp,有了这个实例之后,就可以调用 TvApp 中的各种工具方法了。但这种写法真的对吗?肯定不对。因为我们知道 Application 是属于系统组件,系统组件的实例是要由系统去创建的,如果这里我们自己去 new 一个 TvApp 的实例,它就只是一个普通的 Java 对象而已,而不具备任何 Context 的能力。如果真的想要提供一个获取 TvApp 实例的方法,比较标准的写法又是什么样的呢?其实这里我们只需谨记一点,Application 全局只有一个,它本身就已经是单例了,无需再用单例模式去为它做多重实例保护了,代码如下所示:
public class TvApp extends Application {
private static TvApp sTvApp;
public static TvApp getInstance() {
return sTvApp;
}
@Override
public void onCreate() {
super.onCreate();
sTvApp = this;
}
}
getInstance() 可以照常提供,但是里面不要做任何逻辑判断,直接返回 sTvApp 对象就可以了,而 sTvApp 对象又是什么呢?在onCreate() 中我们将 sTvApp 对象赋值为 this,this 就是当前 Application 的实例,那么 sTvApp 也就是当前Application的实例了。
2. Context引起的内存泄露
我们依然用单例模式进行举例,代码如下所示:
public class Singleton {
private static Singleton sSingleton;
private Context mContext;
public Singleton(Context context) {
mContext = context;
}
public static synchronized Singleton getInstance(Context context) {
if (sSingleton == null) {
sSingleton = new Singleton(context);
}
return sSingleton;
}
}
这是常见的单例实现代码,sSingleton 作为静态对象,其生命周期要长于普通的对象,假如在 Activity A 中通过 getInstance() 获得 sSingleton 对象,传入 this,常驻内存的 sSingleton 引用了你传入的 Activity A 对象,并一直持有,当 Activity A 销毁时,因为它的引用还存在于 sSingleton 中,就不可能被 GC 掉,这样就导致了内存泄漏。一般 Context 造成的内存泄漏,几乎都是当 Context 销毁的时候,却仍然被引用导致销毁失败,解决办法是什么呢?这里的 this 改用 getApplicationContext() 即可。 Application 类型的的 Context 对象可以理解为随着进程存在的,也就不会存在内存泄露的问题。所以总结出以下几点使用 Context 的正确姿势:
1)当 Application 类型的 Context 能实现相关功能的情况下,优先使用 Application 类型的 Context;
2)不要让生命周期长于 Activity 的对象持有该 Activity 的引用。
五、结语
Context 在 Android 开发中的地位举足轻重,正确的使用 Context 才能写出更好的程序。以上只是介绍了 Context 的冰山一角,更多关于 Context 的使用请查看 Android 官方文档。写作此篇文章时也参考了一些技术大佬的博客,在这里一并感谢。
参考:https://developer.android.com/reference/android/content/Context
https://blog.youkuaiyun.com/guolin_blog/article/details/47028975
https://blog.youkuaiyun.com/lmj623565791/article/details/40481055
https://blog.youkuaiyun.com/yanbober/article/details/45967639