文章已授权『郭霖』公众号发布
前言
很高兴遇见你~ 欢迎阅读我的文章。
在文章Android全面解析之由浅及深Handler消息机制中讨论到,Handler可以:
避免我们自己去手动写 死循环和输入阻塞 来不断获取用户的输入以及避免线程直接结束,而是采用事务驱动型设计,使用Handler消息机制,让AMS可以控制整个程序的运行逻辑。
这是关于android程序在设计上更加重要的一部分,不太了解的读者可以前往阅读了解一下。而当我们知道android程序的程序是通过main方法跑起来的,然后通过handler机制来控制程序的运行,那么四大组件和普通的Java类到底有什么区别?为什么同样是Java类,而ActivityThread、Activity等等这些类就显得那么特殊呢?我们的代码、写的布局是通过什么路径使用系统资源把界面展示在屏幕上的?这一切就涉及到我们今天的主角:Context。
什么是Context
回想一下最初学习Android开发的时候,第一用到context是什么时候?如果你跟我一样是通过郭霖的《第一行代码》来入门android,那么一般是Toast。Toast的常规用法是:
Toast.makeText(this, "我是toast", Toast.LENGTH_SHORT).show()
当初也不知道什么是Context,只知道他需要一个context类型,把activity对象传进去即可。从此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 {
...}
关于应用程序环境的全局信息的接口。 这是一个抽象类,它的实现是由Android系统提供。 它允许访问特定应用的资源和类,以及向上调用应用程序级的操作,如启动活动,广播和接收Intent等
。
可以看到Context最重要的作用就是获取全局消息、访问系统资源、调用应用程序级的操作。可能对于这些作用没什么印象,想一下,如果没有context,我们如何做到以下操作:
- 弹出一个toast
- 启动一个activity
- 获取程序布局文件、drawable文件等
- 访问数据库
这些平时看似简单的操作,一旦失去了context将无法执行。这些行为都有一个共同点:需要与系统交汇。四大组件为什么配为组件,而我们的写的就只能叫做一个普通的Java类,正是因为context的这些功能让四大组件有了不一样的能力。简单来说,context是:
应用程序和系统之间的桥梁,应用程序访问系统各种资源的接口。
我们一般使用context最多的是两种情景:直接调用context的方法和调用接口时需要context参数。这些行为都意味着我们需要访问系统相关的资源。
那context是从哪里来的?AMS!AMS是系统级进程,拥有访问系统级操作的权利,应用程序的启动受AMS的调控,在程序启动的过程中,AMS会把一个“凭证”通过跨进程通信给到我们的应用程序,我们的程序会把这个“凭证”封装成context,并提供一系列的接口,这样我们的程序也就可以很方便地访问系统资源了。这样的好处是:
系统可以对应用程序级的操作进行调控,限制各种情景下的权限,同时也可以防止恶意攻击。
如Application类的context和Activity的context权利是不一样的,生命周期也不一样。对于想要操作系统攻击用户的程序也进行了阻止,没有获得允许的Java类没有任何权利,而Activity开放给用户也只有部分有限的权利。而我们开发者获取context的路径,也只有从activity、application等组件获取。
因而,什么是Context?Context是应用程序与系统之间沟通的桥梁,是应用程序访问系统资源的接口,同时也是系统给应用程序的一张“权限凭证”。有了context,一个Java类才可以被称之为组件。
Context家族
上一部分我们了解什么是context以及context的重要性,这一部分就来了解一下context在源码中的子类继承情况。先看一个图:
最顶层是Context抽象类
,他定义了一系列与系统交汇的接口。ContextWrapper
继承自Context,但是并没有真正实现Context中的接口,而是把接口的实现都托管给ContextImpl
,ContextImpl是Context接口的真正实现者,从AMS拿来的“凭证”也是封装到了ContextImpl中,然后赋值给ContextWrapper,这里运用到了一种模式:装饰者模式。Application
和Service
都继承自ContextWrapper,那么他们也就拥有Context的接口方法且本身即是context,方便开发者的使用。Activity
比较特殊,因为它是有界面的,所以他需要一个主题:Theme,ContextThemeWrapper
在ContextWrapper的基础上增加与主题相关的操作。
这样的设计有这样的优点:
- Activity等可以更加方便地使用context,可以把自身当成context来使用,遇到需要context的接口直接把自身传进去即可。
- 运用装饰者模式,向外屏蔽ContextImpl的内部逻辑,同时当需要更改ContextImpl的逻辑实现,ContextWrapper的逻辑几乎不需要更改。
- 更方便地扩展不同情景下的逻辑。如service和activity,情景不同,需要的接口方法也不同,但是与系统交互的接口是相同的,使用装饰者模式可以拓展出很多的功能,同时只需要把ContextImpl对象赋值进去即可。
context的分类
前面讲到Context的家族体系时,了解到他的最终实现类有:Application、Activity、Service,ContextImpl被前三者持有,是Context接口的真正实现。那么这里讨论一下这三者有什么不同,和使用时需要注意的问题。
Application
Application是全局Context,整个应用程序只有一个,他可以访问到应用程序的包信息等资源信息。获取Application的方法一般有两个:
context.getApplicationContext()
activity.getApplication()
通过context和activity都可以获取到Application,那这两个方法有什么区别?没有区别。我们可以打印来看一下:
override fun onCreate(savedInstanceState: Bundle?) {
...
Log.d("一只修仙的猿", "application:$application")
Log.d("一只修仙的猿", "applicationContext:$applicationContext")
}
可以看到确实是同个对象。但为什么要提供两个一样作用的方法?getApplication()
方法更加直观,但是只能在activity中调用。getApplicationContext()
适用范围更广,任意一个context对象皆可以调用此方法。
Application类的Context的特点是生命周期长,在整个应用程序运行的期间他都会存在。同时我们可以自定义Application,并在里面做一些全局的初始化操作,或者写一个静态的context供给全局获取,不需要在方法中传入context。如:
class MyApplication : Application(){
// 全局context
companion object{
lateinit var context: Context
}
override fun onCreate() {
super.onCreate()
// 做全局初始化操作
RetrofitManager.init(this)
context = this
}
}
这样我们就可以在应用启动的时候对一些组件进行初始化,同时可以通过MyApplication.context来获取Application对象。
但是!!!请不要把Application当成工具类使用。由于Application获取的便利性,有开发者会在Application中编写一些工具方法,全局获取使用,这样是不行的。自定义Application的目的是在程序启动的时候做全局初始化工作,而不能拿来取代工具类,这严重违背谷歌设计Application的原则,也违背Java代码规范的单一职责原则。
四大组件
Activity继承自ContextThemeWrapper,是一个拥有主题的context对象。Activity常用于与UI有关的操作,如添加window等。常规使用可以直接用activity.this。
Service继承自ContextWrapper,也可以和Activity一样直接使用service.this来使用context。和activity不同的是,Service没有界面,所以也不需要主题。
ContextProvider使用的是Application的context,Broadcast使用的是activity的context,这两点在后面会进行源码分析。
BaseContext
嗯?baseContext是什么?把这个拿出来单独讲,细心的读者可能会发现activity中有一个方法:getBaseContext
。这个是ContextWrapper中的mBase对象,也就是ContextImpl,也是context接口的真正逻辑实现。
context的使用问题
使用context最重要的问题之一是注意内存泄露。不同的context的生命周期不同,Application是在应用存在的期间会一直存在,而Activity是会随着界面的销毁而销毁,如果当我们的代码长时间持有了activity的context,如静态引用或者单例类,那么会导致activity无法被释放。如下面的代码:
object MyClass {
lateinit var mContext : Context
fun showToast(context : Context){
mContext = context
}
}
单例类在应用持续的时间都会一直存在,这样context也就会被一直被持有,activity无法被回收,导致内存泄露。
那,我们就都换成Application不就可以了,如下:
object MyClass {
lateinit var mContext : Context
fun showToast(context : Context){
mContext = context.applicationContext
}
}
答案是:不可以。什么时候可以使用Application?**不涉及UI以及启动Activity操作。**Activity的context是拥有主题属性的,如果使用Application来操作UI,那么会丢失自定义的主题,采用系统默认的主题。同时,有些UI操作只有Activity可以执行,如弹出dialog,这涉及到window的token问题,我在这篇文章token验证进行了详细的解答,有兴趣的读者可以去阅读一下。这也是官方对于context不同权限的设计,没有界面的context,就不应该有操作界面的权利。使用Application启动的Activity必须指定task以及标记为singleTask,因为Application是没有任务栈的,需要重新开一个新的任务栈。因此,我们需要根据不同context的不同职责来执行不同的任务。
Context的创建过程
经过上面的讨论,读者对于context在心中有了一定的理解。但始终觉得少点什么:activity是什么时候被创建的,他的contextImpl是如何被赋值的?Application呢?为什么说ContextProvider的context是Application,Broadcast的context是Activity?contextImpl又是如何被创建的?解决这些疑惑,就必须阅读源码了。阅读源码的好处非常多,上面我的讲述,都是基于我阅读源码之后的理解,而“一千个观众有一千个哈姆雷特”,阅读源码可以形成自己对整个机制自己的思考和理解,同时可以让自己对context那些知识真正落实到代码上,增强自己对知识的自信心。当别人和你意见不同的时候,你可以拍拍胸脯说:我看过源码,这个地方就是这样。是不是非常自信且傲娇?
然而阅读源码不是越多越好,而是把握整体的流程之后阅读关键源码,不要深入源码堆中无法自拔。例如我觉得activity的contextImpl是在Activity创建的过程中被赋值的,那么我就会去找activity的启动流程源码,然后只看和context有关的部分。提高效率的同时,还可以切中我们学习的点。下面的源码阅读我们给出整体流程,然后重点理解关键代码,其他的源码读者可自行下载源码去跟踪阅读一下。
Application
Application应用级别的context,是在应用被创建的时候被创建的,是第一个被创建的context,也是最后一个被销毁的context。因而追踪Application的创建需要从应用程序的启动流程看起。应用启动的源码流程如下(简化版):
应用程序从ActivityThread的main方法开始执行,从Handler消息机制中我们知道main方法主要是开启线程的Looper以及handler,然后由AMS向主线程发送message控制应用的启动过程。因而我们可以把目标锁定在图中的最后一个方法:handleBindApplication
,Application最有可能在这里被创建:
ActivityThread.class (api29)
private void handleBindApplication(AppBindData data) {
...
// 创建LoadedApk对象
data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
.