Context,即上下文,是Android中常用的类之一。你知道它到底是代表了什么吗?我们都知道一个APP中Context的总个数=Activity个数+Service个数+1,可你知道为什么吗?
是的,很多人都仅仅停留在了“会用”这一阶段,没有做到“知其然,知其所以然”。
本篇文章将较为详细的为大家介绍一下Context,帮助大家更加深入的理解Android程序员的这一“老朋友”。
无处不在的 Context
在 Android 开发中,我们随处看见Context
,几乎所有对象都要求拥有一个 Context 变量,大多数常用方法也都需要一个Context。从启动 Activity,访问资源,再到弹出一个Toast,创建一个Dialog,Context 实可谓无处不在。
Context 是什么?
这么多地方都需要Context,它究竟是个什么东西呢?
Context 意为上下文,是一个应用程序环境信息的接口。
根据翻译,Context 被翻译为上下文。这个解释有点抽象,笔者初次接触时也为此迷惑了很久。
其实,这里的“上下文”,应该更接近与文章中的上下文。比如我们学生时代常见的那个问题:
请联系上下文,谈谈你对XXX的认识。
换言之,这里的“上下文”,其实是语境的意思,即:environment
的snapshot
为了更好的理解它,让我们举个例子:
…
林冲大叫一声,“啊也!”.
…
问:这句话林冲的“啊也”表达了林冲怎样的心理?
你看,如果一篇文章,只摘录一段,没前没后,你读不懂,即使自以为读懂了也难免断章取义。这就是因为有语境
,有语言环境存在,一段话说了什么,需要通过“上下文”来推断。
类似的,app的一屏之于app,子程序之于程序,甚至进程之于操作系统,都是这个道理。程序执行了部分到达子程序,子程序要获得结果,要用到程序之前的一些结果(包括但不限于外部变量值,外部对象等等);app点击一个按钮进入一个新的界面,也要保存你是在哪个屏幕跳过来的等等信息,以便你点击返回的时候能正确跳回,如果不存肯定就无法正确跳回了。
你现在明白Context的这个上下文到底是什么意思,为什么这么多地方都需要它了吗?
接下来,让我们从源码的角度继续了解Context.
源码解析 Context
我们可以看到,Context 其实是一个抽象类,其内部定义了很多方法以及静态常量。它的具体实现类是ContextImpl。
除ContextImpl之外,和Context相关的类还有ContextWrapper,ContextThemeWrapper以及我们熟悉的Application,Service,Activity等。它们之间的关系如下图所示。
从类图中我们可以看出,ContextImpl和ContextWrapper继承自Context。ContextWrapper中有一个Context类型的mBase对象,具体指向了ContextImpl,由此拥有了ContextImpl中实现的很多功能。而外界不止需要这些功能,还需要拓展其他的功能,因此这里其实使用了装饰模式
。
装饰模式
设计模式之装饰模式
什么是装饰模式?
装饰模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。
这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。
简单来说,装饰模式就是动态地给一个对象添加一些额外的职责。
你可能要问了,我们不是可以通过继承达到相同的目的吗?
其实,就增加功能来说,装饰器模式相比生成子类要更为灵活一些。我们为了扩展一个类如果使用继承方式实现,随着扩展功能的增多,子类会很膨胀。
怎么样?听懂了吗?没有的话让我们举个例子,帮你更好的理解吧~
装饰模式的特点
-
何时使用:在不想增加很多子类的情况下扩展类。
-
如何解决:将具体功能职责划分,同时继承装饰者模式。
-
应用实例:
1、孙悟空有 72 变,当他变成"庙宇"后,他的根本还是一只猴子,但是他又有了庙宇的功能。
2、不论一幅画有没有画框都可以挂在墙上,但是通常都是有画框的,并且实际上是画框被挂在墙上。在挂在墙上之前,画可以被蒙上玻璃,装到框子里;这时画、玻璃和画框形成了一个物体。 -
优点:装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。
-
缺点:多层装饰比较复杂。
-
使用场景: 1、扩展一个类的功能。 2、动态增加功能,动态撤销。
-
注意事项:可代替继承。
Context 中的装饰模式
具体到我们的所看到的类图,Context即为抽象组件类,而ContextImpl则是组件的具体实现类,ContextWrapper则是抽象装饰类,将ContextImpl进行了一层包装,ContextWrapper中几乎所有方法都是调用mBase
实现的。下面的ContextThemeWrapper、Application、Service才是装饰者的具体实现类,它们分别又对ContextWrapper进行了自己所需的功能拓展,Activity又对ContextThemeWrapper做了相同的事情,ContextThemeWrapper相比于其父类,增加了一些与主题相关的能力,如:setTheme()/getTheme()等。
Context 为什么要使用装饰模式?
Context及其关联类使用装饰模式主要是因为以下优点:
- 使用者可以方便的使用Context
- 如果ContextImpl发生变化,其他类基本不需要任何修改(开闭原则)
- ContextImpl 的实现不会暴露给使用者,使用者也不必关心ContextImpl的实现(即ContextImpl对使用者来说是黑盒的)
- 通过组合而非继承的方式,拓展ContextImpl的功能,可以在运行时选择不同的装饰类,从而实现不同的功能
如果你实在是好奇ContextImpl具体的实现,你可能会去Android Studio中翻源码,但是你会失望的,因为你永远也找不到它。
ContextImpl 在哪?
其实,这个文件是保护文件(即注解了是内部保护文件),所以在Android Studio等IDE中都是不显示的。如果想看它的代码,可以使用魔法,去官网中查看Android全部源码。
Context 作用域
虽然Context几乎无所不在,但并不是随便拿到一个Context实例就可以为所欲为,它的使用还是有一些规则限制的。我们已经知道,Context是由ContextImpl类去实现的,因此在绝大多数场景下,Activity、Service和Application这三种类型的Context都是可以通用的。
不过有几种场景比较特殊,比如启动Activity,还有弹出Dialog。出于安全原因的考虑,Android是不允许Activity或Dialog凭空出现的。(试想一下,如果允许,那岂不是随处可弹窗可跳转,垃圾、病毒软件将会横行,真就是“整个晋西北都乱成一锅粥了”。)
所以,一个Activity的启动必须要建立在另一个Activity的基础之上,也就是以此形成的返回栈。而Dialog则必须在一个Activity上面弹出(除非是System Alert类型的Dialog),因此在这种场景下,我们只能使用Activity类型的Context,否则将会出错。
具体Context所对应的能力如下图所示:
这里对不推荐的情况作出解释:
- 如果我们用ApplicationContext去启动一个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同Application。 - 在Application和Service中去layout inflate也是合法的,但是会使用系统默认的主题样式,如果你自定义了某些样式可能不会被使用。所以这种方式也不推荐使用。
一句话总结:凡是跟UI相关的,都应该使用Activity作为Context来处理;其他的一些操作,Service,Activity,Application等实例都可以,当然了,注意Context引用的持有,防止内存泄漏。
如何获取Context?
通常我们想要获取Context对象,主要有以下四种方法
- View.getContext:返回当前View对象的Context对象,通常是当前正在展示的Activity对象。
- Activity.getApplicationContext:获取当前Activity所在的(应用)进程的Context对象,通常我们使用Context对象时,要优先考虑这个全局的进程Context。
- ContextWrapper.getBaseContext():用来获取一个ContextWrapper进行装饰之前的Context(即变量mBase),可以使用这个方法,这个方法在实际开发中使用并不多,也不建议使用。
- Activity.this:返回当前的Activity实例,如果是UI控件需要使用Activity作为Context对象,但是默认的Toast实际上使用ApplicationContext也可以。
Application和ApplicationContext
获取当前Application对象用getApplicationContext,不知道你有没有联想到getApplication(),这两个方法有什么区别?相信这个问题会难倒不少开发者。
程序是不会骗人的,我们通过上面的代码,打印得出两者的内存地址都是相同的,看来它们是同一个对象。
其实这个结果也很好理解,因为前面已经说过了,Application本身就是一个Context,所以这里获取getApplicationContext()得到的结果就是Application本身的实例。
那么问题来了,既然这两个方法得到的结果都是相同的,那么Android为什么要提供两个功能重复的方法呢?
实际上这两个方法在作用域上有比较大的区别。
getApplication()方法的语义性非常强,一看就知道是用来获取Application实例的,而getApplicationContext() 是返回应用的上下文也就是把Application作为Context。
这里可以参考StackOverflow的一个高赞回答:
翻译如下:
尽管在当前的Android活动和服务实现中,getApplication()和getApplicationContext()返回相同的对象,但不能保证总是这样(例如,在特定的供应商实现中)。
因此,如果您想要在清单中注册的应用程序类,则永远不要调用getApplicationContext()并将其强制转换为应用程序,因为它可能不是应用程序实例(您显然在测试框架中遇到过这种情况)。
为什么getApplicationContext()首先存在?
getApplication()仅在活动类和服务类中可用,而getApplicationContext()在上下文类中声明。
这实际上意味着一件事:当在广播接收器中编写代码时,它不是一个上下文,而是在其onReceive方法中给定了一个上下文,您只能调用getApplicationContext()。这也意味着你不能保证在BroadcastReceiver中访问你的应用程序。
当查看Android代码时,您会看到,当附加时,活动会接收基本上下文和应用程序,这些是不同的参数。getApplicationContext()将其调用委托给baseContext。getApplicationContext()。
还有一件事:文档中说,在大多数情况下,您不需要对应用程序进行子类化:
通常不需要对应用程序进行子类化。在大多数情况下,静态单例可以以更模块化的方式提供相同的功能。如果你的单身汉需要一个全局上下文(例如注册广播接收器),那么可以给检索它的函数一个内部使用上下文的上下文。getApplicationContext()首次构造单例时。
简单来说,getApplication()方法的语义性非常强,一看就知道是用来获取Application实例的,但是这个方法只有在Activity和Service中才能调用的到。那么也许在绝大多数情况下我们都是在Activity或者Service中使用Application的,但是如果在一些其它的场景,比如BroadcastReceiver中也想获得Application的实例,这时就可以借助getApplicationContext()方法了。getApplicationContext() 是返回应用的上下文,生命周期是整个应用,应用摧毁它才摧毁。这里要区别一下Activity的Context,Activity.this的context 返回当前Activity的上下文,及把Activity用作Context,生命周期属于Activity ,Activity 摧毁他就摧毁。
Context引起的内存泄露
Context并不能随便乱用,用的不好有可能会引起内存泄露的问题,下面就示例两种错误的引用方式。
错误的单例模式
public class Singleton {
private static Singleton instance;
private Context mContext;
private Singleton(Context context) {
this.mContext = context;
}
public static Singleton getInstance(Context context) {
if (instance == null) {
instance = new Singleton(context);
}
return instance;
}
}
这是一个非线程安全的单例模式,instance作为静态对象,其生命周期要长于普通的对象,其中也包含Activity,假如Activity A去getInstance获得instance对象,传入this,常驻内存的Singleton保存了你传入的Activity A对象,并一直持有,即使Activity被销毁掉,但因为它的引用还存在于一个Singleton中,就不可能被GC掉,这样就导致了内存泄漏。
View持有Activity引用
public class MainActivity extends Activity {
private static Drawable mDrawable;
@Override
protected void onCreate(Bundle saveInstanceState) {
super.onCreate(saveInstanceState);
setContentView(R.layout.activity_main);
ImageView iv = new ImageView(this);
mDrawable = getResources().getDrawable(R.drawable.ic_launcher);
iv.setImageDrawable(mDrawable);
}
}
有一个静态的Drawable对象当ImageView设置这个Drawable时,ImageView保存了mDrawable的引用,而ImageView传入的this是MainActivity的mContext,因为被static修饰的mDrawable是常驻内存的,MainActivity是它的间接引用,MainActivity被销毁时,也不能被GC掉,所以造成内存泄漏。
正确使用Context
一般Context造成的内存泄漏,几乎都是当Context销毁的时候,却因为被引用导致销毁失败,而Application的Context对象可以理解为随着进程存在的。所以我们总结出使用Context的正确姿势:
- 当Application的Context能搞定的情况下,并且生命周期长的对象,优先使用Application的Context。
- 不要让生命周期长于Activity的对象持有到Activity的引用。
- 尽量不要在Activity中使用非静态内部类,因为非静态内部类会隐式持有外部类实例的引用,如果使用静态内部类,将外部实例引用作为弱引用持有。
总结
现在,让我们回过头来再看文章开始时提出的问题。
Context是什么?
答:上下文,语境
为什么有这个公式?
答:因为Application、Service、Activity都是Context的一种,一个APP的总Context个数便是三者之和。公式中的“1”其实代表的是Application。
除此之外,我们还对Context及其关联类,不同Context的区别,以及如何获取Context都有了了解,还简单了解了装饰模式。怎么样?是不是收获满满呢?
参考文章:
编程中什么是「Context(上下文)」? - 知乎 (zhihu.com)
《Android 进阶解密》
《Android 源码设计模式》
Context都没弄明白,还怎么做Android开发? - 简书 (jianshu.com)
(3条消息) Android Context完全解析,你所不知道的Context的各种细节_郭霖的专栏-CSDN博客_android context
android - getApplication() vs. getApplicationContext() - Stack Overflow