背景引入
最近,菜鸟开发者小李在开发一个 Android 应用时,遇到了关于 Context 的问题。他尝试在一个工具类中使用 Context 来获取资源,却总是报错。这让他感到十分困惑,于是决定向公司里的技术大牛,退休的保洁大爷请教。
这天,小李在公司茶水间找到了正在休息的保洁大爷,一脸苦恼地走过去。
小李(挠挠头,满脸疑惑) :大爷,我最近在开发中遇到了 Context 的问题,实在搞不明白,您能给我讲讲吗?
保洁大爷(放下手中的茶杯,微笑着) :哈哈,当然可以,你先说说遇到什么问题了?
基础概念之问
1. 什么是 Context?
保洁大爷(耐心解释) :Context 呢,简单来说,它是 Android 应用程序环境的一个抽象概念。官方定义是,它是一个提供访问应用资源和服务的接口。你可以把它想象成一个应用程序的 “管家”,管理着应用的各种资源、组件和服务。比如说,你要获取应用中的字符串资源、启动一个新的 Activity、获取系统的一些服务,像网络服务、传感器服务等,都需要通过 Context 来完成。
小李(恍然大悟) :原来是这样,感觉它就像一个桥梁,连接着应用和系统资源。那为什么我们需要这个 Context 呢?没有它不行吗?
2. 为什么需要 Context?
保洁大爷(笑着说) :这可不行,Context 在 Android 开发中可是不可或缺的。你想想,我们开发的应用要和系统交互,要使用系统提供的各种资源和服务。比如说,你想在应用中显示一个字符串,这个字符串是保存在资源文件里的,你怎么获取它呢?就需要通过 Context 来获取资源管理器,再从资源管理器中获取这个字符串。又比如,你要启动一个新的 Activity,这个操作也得通过 Context 来完成。还有,像获取系统服务,比如获取网络连接状态、震动服务等,都离不开 Context。所以说,没有 Context,我们的应用就没办法正常使用系统提供的这些资源和服务,就像一个人没有了手脚,什么都干不了。
小李(频频点头) :明白了,确实很重要。那我在代码中看到有个 ContextImpl,这又是什么呢?
3.ContextImpl 是什么?
保洁大爷(耐心解答) :ContextImpl 是 Context 的具体实现类。我们知道 Context 是一个抽象类,它定义了很多抽象方法,这些方法具体是怎么实现的呢,就是由 ContextImpl 来完成的。比如说,我们在 Activity 或者 Service 中调用 Context 的方法,像获取资源、启动 Activity 等,实际上最终调用的是 ContextImpl 中的实现。Activity、Service 和 Application 这些类,虽然它们都继承自 ContextWrapper,但在它们初始化的时候,都会创建一个 ContextImpl 对象,然后把这个对象赋值给 ContextWrapper 中的 mBase 成员变量,这样我们在调用 Activity、Service 或 Application 的 Context 方法时,就会通过这个 mBase 变量,调用到 ContextImpl 中的具体实现方法。简单来说,ContextImpl 就是幕后的执行者,真正干活的角色。
小李(若有所思) :原来是这样,感觉对 Context 的理解又深了一些。那大爷,Context 都有哪些类型呢?
深入探究 Context
1. Context 有哪些类型?
保洁大爷(喝了口茶,缓缓说道) :Context 主要有三种类型,分别是 Activity、Service 和 Application。从类的继承关系来看,它们都间接继承自 Context。Activity 继承自 ContextThemeWrapper,而 ContextThemeWrapper 又继承自 ContextWrapper,ContextWrapper 内部持有一个 ContextImpl 对象,真正实现 Context 功能的是 ContextImpl。Service 和 Application 则是直接继承自 ContextWrapper 。
这么说可能有点抽象,我给你画个简单的类图。(保洁大爷拿起笔,在纸上画了起来)
从这个类图可以看出,Activity 因为有界面,所以需要处理主题相关的内容,因此继承自 ContextThemeWrapper。而 Service 是在后台运行,不需要界面和主题,Application 是代表整个应用的上下文,也不需要主题,所以它们直接继承自 ContextWrapper。
小李(看着类图,恍然大悟) :原来是这样,通过这个类图,一下子就清楚它们之间的关系了。那这三种类型的 Context 在使用上有什么区别呢?
2. 不同类型 Context 的区别和使用场景?
保洁大爷(耐心解释) :这三种 Context 虽然都继承自 Context,拥有 Context 的基本功能,但在使用场景上还是有一些区别的。
Activity 类型的 Context,它与一个具体的 Activity 实例相关联,生命周期和 Activity 一致。由于它关联着具体的界面,所以在涉及到与 UI 相关的操作时,比如显示 Dialog、启动 Activity(在同一个任务栈中)、LayoutInflater 加载布局等,都需要使用 Activity 类型的 Context 。因为 Dialog 必须依附于一个 Activity 才能显示,而 LayoutInflater 加载布局时,如果使用 Activity 的 Context,可以正确加载 Activity 对应的主题样式。
Service 类型的 Context,它的生命周期和 Service 一致。主要用于启动和管理 Service 自身,以及执行一些与 Service 相关的后台任务,比如在 Service 中获取系统服务、访问应用资源等 。不过,Service 的 Context 不能直接用于显示 Dialog,因为它没有对应的界面。如果在 Service 中启动 Activity,需要为 Intent 添加 FLAG_ACTIVITY_NEW_TASK 标志,否则会报错,因为 Service 没有自己的任务栈。
Application 类型的 Context,它的生命周期是整个应用程序的生命周期,从应用启动到结束。它主要用于全局状态的管理,比如保存一些全局的变量、获取应用级别的资源等 。在一些需要长期保存 Context 引用的场景,比如在单例模式的工具类中,为了避免内存泄漏,通常会使用 Application 的 Context,因为它的生命周期和应用一致,不会因为某个 Activity 或 Service 的销毁而导致 Context 失效。
小李(认真思考后) :明白了,也就是说与 UI 相关的用 Activity 的 Context,后台服务相关的用 Service 的 Context,全局管理相关的用 Application 的 Context。那这些 Context 在创建的时候有什么不同呢?
3. Context 的创建过程是怎样的?
保洁大爷(清了清嗓子,开始讲解) :我们先来看看 Activity 的 Context 创建过程。当我们启动一个 Activity 时,ActivityThread 会调用 performLaunchActivity 方法 。在这个方法中,首先会通过 createBaseContextForActivity 方法创建一个 ContextImpl 对象 。这个方法会传入 ActivityThread、Activity 的包信息、Activity 的 token 等参数来初始化 ContextImpl。然后,会调用 ContextImpl 的 setOuterContext 方法,将 Activity 实例赋值给 ContextImpl 的成员变量 mOuterContext,这样 ContextImpl 就可以访问 Activity 的变量和方法。接着,会调用 Activity 的 attach 方法,在这个方法中,会调用 attachBaseContext 方法,将创建好的 ContextImpl 对象赋值给 Activity 的 mBase 变量 ,这样 Activity 就和 ContextImpl 关联起来了。简单来说,就是先创建 ContextImpl,然后将 Activity 和 ContextImpl 相互关联。
Service 的 Context 创建过程和 Activity 类似。当我们启动一个 Service 时,ActivityThread 会调用 handleCreateService 方法 。在这个方法中,会创建一个 ContextImpl 对象,同样会调用 setOuterContext 方法将 Service 实例赋值给 ContextImpl 的 mOuterContext 。然后,会调用 Service 的 attach 方法,将 ContextImpl 对象赋值给 Service 的 mBase 变量 ,完成 Service 和 ContextImpl 的关联。
Application 的 Context 创建过程稍有不同。在应用启动时,ActivityThread 会调用 makeApplication 方法 。在这个方法中,会创建一个 ContextImpl 对象,然后通过 Instrumentation 的 newApplication 方法创建 Application 实例 。接着,会调用 ContextImpl 的 setOuterContext 方法,将 Application 实例赋值给 ContextImpl 的 mOuterContext 。最后,会调用 Application 的 attach 方法,将 ContextImpl 对象赋值给 Application 的 mBase 变量 ,完成 Application 和 ContextImpl 的关联。
为了让你更清楚,我给你画个简单的时序图。(保洁大爷又拿起笔,在纸上画了起来)
小李(看着时序图,不住点头) :哇,大爷,您这么一讲,我对 Context 的创建过程就完全明白了,真是太感谢您了!
实战与陷阱
1. 如何正确获取 Context?
小李(拿出笔记本,认真地问) :大爷,那在实际开发中,我们都有哪些常见的获取 Context 的方式呢?
保洁大爷(思考片刻,回答道) :常见的获取 Context 的方式有几种。在 Activity 中,你可以直接使用this关键字来获取当前 Activity 的 Context,因为 Activity 本身就是一个 Context。比如,你要在 Activity 中弹出一个 Toast,就可以这样写:
Toast.makeText(this, "这是一个Toast", Toast.LENGTH_SHORT).show();
在 Service 中,你可以使用getApplicationContext()方法来获取 Application 的 Context 。Application 的 Context 生命周期和应用一样长,它不依赖于某个具体的 Activity 或 Service。比如:
Context context = getApplicationContext();
在 Application 类中,同样可以使用getApplicationContext()方法来获取自身的 Context 。另外,如果你自定义了一个 Application 类,还可以在里面提供一个静态方法来获取 Context,方便在其他地方使用。比如:
public class MyApplication extends Application {
private static Context context;
@Override
public void onCreate() {
super.onCreate();
context = getApplicationContext();
}
public static Context getContext() {
return context;
}
}
然后在其他地方就可以通过MyApplication.getContext()来获取 Context 。还有一种情况,在 Fragment 中,你可以通过getActivity()方法获取所在 Activity 的 Context ,不过要注意,在 Fragment 还没有依附到 Activity 时,getActivity()可能会返回null,所以最好先进行判空处理 。比如:
if (getActivity()!= null) {
Context context = getActivity();
}
小李(边记录边点头) :明白了,这些获取方式都很实用。那在获取 Context 的时候,有什么注意事项吗?
保洁大爷(强调道) :当然有。首先,要根据具体的使用场景选择合适的 Context 获取方式。比如,如果你要进行一些与 UI 相关的操作,像显示 Dialog、启动 Activity(在同一个任务栈中),就必须使用 Activity 类型的 Context ,因为这些操作需要依赖 Activity 的界面和任务栈。如果使用了 Application 的 Context,可能会导致异常。另外,在使用 Context 时,要注意避免内存泄漏。像前面提到的单例模式中,如果不正确地持有 Activity 的 Context,就可能导致 Activity 无法被回收,造成内存泄漏。所以,在单例中如果需要 Context,尽量使用 Application 的 Context 。还有,在一些生命周期较短的对象中,比如临时的 Runnable 或线程中,不要持有 Activity 的 Context ,如果确实需要,可以使用弱引用(WeakReference)来持有 Context,这样在 Activity 销毁时,Context 可以被正常回收 。
小李(恍然大悟) :原来是这样,看来获取和使用 Context 还真有不少学问呢!那大爷,Context 会不会引发内存泄漏问题呢?
2. Context 引发的内存泄漏问题及解决办法?
保洁大爷(表情严肃,认真地说) :会的,Context 引发的内存泄漏是 Android 开发中比较常见的问题。其中一种常见的场景就是单例持有 Activity 的 Context 。你看,单例的生命周期和应用的生命周期一样长,如果在单例中持有了 Activity 的 Context,当 Activity 销毁时,由于单例还持有它的引用,Activity 就无法被回收,从而导致内存泄漏 。比如说,有一个单例类AppManager,它的构造函数需要传入一个 Context:
public class AppManager {
private static AppManager instance;
private Context mContext;
private AppManager(Context context) {
this.mContext = context;
}
public static AppManager getInstance(Context context) {
if (instance == null) {
synchronized (AppManager.class) {
if (instance == null) {
instance = new AppManager(context);
}
}
}
return instance;
}
}
如果在 Activity 中这样使用:
AppManager appManager = AppManager.getInstance(this);
当这个 Activity 退出时,Activity 应该被回收,但是单例中又持有它的引用,导致 Activity 回收失败,造成内存泄漏 。
小李(皱着眉头,担心地问) :那这可怎么办呢?有什么解决办法吗?
保洁大爷(笑着说) :解决办法有几种。一种是在单例中使用 Application 的 Context ,而不是 Activity 的 Context 。因为 Application 的 Context 生命周期和单例一样长,不会导致内存泄漏 。我们可以修改AppManager的代码,在构造函数中使用context.getApplicationContext()来获取 Application 的 Context:
public class AppManager {
private static AppManager instance;
private Context mContext;
private AppManager(Context context) {
this.mContext = context.getApplicationContext();
}
public static AppManager getInstance(Context context) {
if (instance == null) {
synchronized (AppManager.class) {
if (instance == null) {
instance = new AppManager(context);
}
}
}
return instance;
}
}
这样不管外面传入什么 Context,最终都会使用 Application 的 Context ,从而避免了内存泄漏 。
另一种办法是使用弱引用(WeakReference)来持有 Context 。弱引用不会阻止对象被垃圾回收器回收,如果对象只被弱引用持有,在系统内存不足时,对象会被回收 。我们可以修改AppManager的代码,使用弱引用来持有 Context:
public class AppManager {
private static AppManager instance;
private WeakReference<Context> mContextWeakReference;
private AppManager(Context context) {
this.mContextWeakReference = new WeakReference<>(context);
}
public static AppManager getInstance(Context context) {
if (instance == null) {
synchronized (AppManager.class) {
if (instance == null) {
instance = new AppManager(context);
}
}
}
return instance;
}
public Context getContext() {
return mContextWeakReference.get();
}
}
在使用getContext()方法获取 Context 时,要先判断返回的是否为null,因为弱引用持有的对象可能已经被回收了:
Context context = appManager.getContext();
if (context!= null) {
// 使用context
}
这样也可以在一定程度上避免内存泄漏 。
小李(如释重负,开心地说) :太感谢大爷了,您这么一讲,我对 Context 引发的内存泄漏问题和解决办法就清楚多了!
总结与展望
保洁大爷(语重心长地说) :今天我们聊了很多关于 Android 中 Context 的知识,从它的基本概念、类型、创建过程,到在实际开发中的使用和可能遇到的内存泄漏问题。Context 在 Android 开发中就像地基一样,是非常重要的基础知识,只有把它理解透彻了,你在开发中才能少走弯路,避免很多不必要的问题。你还有什么疑问吗?
小李(感激地说) :大爷,我没有疑问了,今天真是收获满满,您讲得太清楚了,把我之前好多疑惑都解开了。
保洁大爷(笑着鼓励道) :那就好,不过 Android 开发的知识很广泛,Context 只是其中一部分。你要保持这种求知的态度,继续深入学习,遇到问题多思考,多查阅资料,相信你的技术会越来越好。
小李(坚定地点点头) :好的,大爷,我一定会的!再次感谢您的耐心解答,以后要是还有问题,我还能来请教您吗?
保洁大爷(爽快地答应) :当然可以,随时欢迎,有什么问题都可以来找我交流。