Android Context详解

本人博客已经迁移到nasdaqgodzilla.github.io


2019年12月11日16:41:43


Activity mActivity =new Activity()

作为 Android 开发者,不知道你有没有思考过这个问题,Activity 可以 new 吗?Android 的应用程序开发采用 JAVA 语言,Activity 本质上也是一个对象,那上面的写法有什么问题呢?估计很多人说不清道不明。Android 程序不像 Java 程序一样,随便创建一个类,写个 main() 方法就能运行,Android 应用模型是基于组件的应用设计模式,组件的运行要有一个完整的 Android 工程环境,在这个环境下,Activity、Service 等系统组件才能够正常工作,而这些组件并不能采用普通的 Java 对象创建方式,new 一下就能创建实例了,而是要有它们各自的上下文环境,也就是我们这里讨论的 Context。可以这样讲,Context 是维持 Android 程序中各组件能够正常工作的一个核心功能类。

Context 到底是什么?

Context 的中文翻译为:语境; 上下文; 背景; 环境,在开发中我们经常说称之为 “上下文”,那么这个“上下文” 到底是指什么意思呢?在语文中,我们可以理解为语境,在程序中,我们可以理解为当前对象在程序中所处的一个环境,一个与系统交互的过程。比如微信聊天,此时的 “环境” 是指聊天的界面以及相关的数据请求与传输,Context 在加载资源、启动 Activity、获取系统服务、创建 View 等操作都要参与。

那 Context 到底是什么呢?一个 Activity 就是一个 Context,一个 Service 也是一个 Context。Android 程序员把 “场景” 抽象为 Context 类,他们认为用户和操作系统的每一次交互都是一个场景,比如打电话、发短信,这些都是一个有界面的场景,还有一些没有界面的场景,比如后台运行的服务(Service)。一个应用程序可以认为是一个工作环境,用户在这个环境中会切换到不同的场景,这就像一个前台秘书,她可能需要接待客人,可能要打印文件,还可能要接听客户电话,而这些就称之为不同的场景,前台秘书可以称之为一个应用程序。

如何生动形象的理解 Context?

上面的概念中采用了通俗的理解方式,将 Context 理解为 “上下文” 或者“场景”,如果你仍然觉得很抽象,不好理解。在这里我给出一个可能不是很恰当的比喻,希望有助于大家的理解:一个 Android 应用程序,可以理解为一部电影或者一部电视剧,Activity,Service,Broadcast Receiver,Content Provider 这四大组件就好比是这部戏里的四个主角:胡歌,霍建华,诗诗,Baby。他们是由剧组(系统)一开始就定好了的,整部戏就是由这四位主演领衔担纲的,所以这四位主角并不是大街上随随便便拉个人(new 一个对象)都能演的。有了演员当然也得有摄像机拍摄啊,他们必须通过镜头(Context)才能将戏传递给观众,这也就正对应说四大组件(四位主角)必须工作在 Context 环境下(摄像机镜头)。那 Button,TextView,LinearLayout 这些控件呢,就好比是这部戏里的配角或者说群众演员,他们显然没有这么重用,随便一个路人甲路人乙都能演(可以 new 一个对象),但是他们也必须要面对镜头(工作在 Context 环境下),所以Button mButton=new Button(Context)是可以的。虽然不很恰当,但还是很容易理解的,希望有帮助。

源码中的 Context

public abstract class Context {
    
    public static final int MODE_PRIVATE = 0x0000;

    public static final int MODE_WORLD_WRITEABLE = 0x0002;

    public static final int MODE_APPEND = 0x8000;

    public static final int MODE_MULTI_PROCESS = 0x0004;

    .
    .
    .
    }


源码中的注释是这么来解释 Context 的:Context 提供了关于应用环境全局信息的接口。它是一个抽象类,它的执行被 Android 系统所提供。它允许获取以应用为特征的资源和类型,是一个统领一些资源(应用程序环境变量等)的上下文。就是说,它描述一个应用程序环境的信息(即上下文);是一个抽象类,Android 提供了该抽象类的具体实现类;通过它我们可以获取应用程序的资源和类(包括应用级别操作,如启动 Activity,发广播,接受 Intent 等)。既然上面 Context 是一个抽象类,那么肯定有他的实现类咯,我们在 Context 的源码中通过 IDE 可以查看到他的子类最终可以得到如下关系图:

Context 类本身是一个纯 abstract 类,它有两个具体的实现子类:ContextImpl 和 ContextWrapper。其中 ContextWrapper 类,如其名所言,这只是一个包装而已,ContextWrapper 构造函数中必须包含一个真正的 Context 引用,同时 ContextWrapper 中提供了 attachBaseContext()用于给 ContextWrapper 对象中指定真正的 Context 对象,调用 ContextWrapper 的方法都会被转向其所包含的真正的 Context 对象。ContextThemeWrapper 类,如其名所言,其内部包含了与主题(Theme)相关的接口,这里所说的主题就是指在 AndroidManifest.xml 中通过 android:theme 为 Application 元素或者 Activity 元素指定的主题。当然,只有 Activity 才需要主题,Service 是不需要主题的,因为 Service 是没有界面的后台场景,所以 Service 直接继承于 ContextWrapper,Application 同理。而 ContextImpl 类则真正实现了 Context 中的所以函数,应用程序中所调用的各种 Context 类的方法,其实现均来自于该类。一句话总结:**Context 的两个子类分工明确,其中 ContextImpl 是 Context 的具体实现类,ContextWrapper 是 Context 的包装类。**Activity,Application,Service 虽都继承自 ContextWrapper(Activity 继承自 ContextWrapper 的子类 ContextThemeWrapper),但它们初始化的过程中都会创建 ContextImpl 对象,由 ContextImpl 实现 Context 中的方法。

一个应用程序有几个 Context?

其实这个问题本身并没有什么意义,关键还是在于对 Context 的理解,从上面的关系图我们已经可以得出答案了,在应用程序中 Context 的具体实现子类就是:Activity,Service,Application。那么Context数量=Activity数量+Service数量+1。当然如果你足够细心,可能会有疑问:我们常说四大组件,这里怎么只有 Activity,Service 持有 Context,那 Broadcast Receiver,Content Provider 呢?**Broadcast Receiver,Content Provider 并不是 Context 的子类,他们所持有的 Context 都是其他地方传过去的,所以并不计入 Context 总数。**上面的关系图也从另外一个侧面告诉我们 Context 类在整个 Android 系统中的地位是多么的崇高,因为很显然 Activity,Service,Application 都是其子类,其地位和作用不言而喻。

Context 能干什么?

Context 到底可以实现哪些功能呢?这个就实在是太多了,弹出 Toast、启动 Activity、启动 Service、发送广播、操作数据库等等都需要用到 Context。

TextView tv = new TextView(getContext());

ListAdapter adapter = new SimpleCursorAdapter(getApplicationContext(), ...);

AudioManager am = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);getApplicationContext().getSharedPreferences(name, mode);

getApplicationContext().getContentResolver().query(uri, ...);

getContext().getResources().getDisplayMetrics().widthPixels * 5 / 8;

getContext().startActivity(intent);

getContext().startService(intent);

getContext().sendBroadcast(intent);


Context 作用域

虽然 Context 神通广大,但并不是随便拿到一个 Context 实例就可以为所欲为,它的使用还是有一些规则限制的。由于 Context 的具体实例是由 ContextImpl 类去实现的,因此在绝大多数场景下,Activity、Service 和 Application 这三种类型的 Context 都是可以通用的。不过有几种场景比较特殊,比如启动 Activity,还有弹出 Dialog。出于安全原因的考虑,Android 是不允许 Activity 或 Dialog 凭空出现的,一个 Activity 的启动必须要建立在另一个 Activity 的基础之上,也就是以此形成的返回栈。而 Dialog 则必须在一个 Activity 上面弹出(除非是 System Alert 类型的 Dialog),因此在这种场景下,我们只能使用 Activity 类型的 Context,否则将会出错。

从上图我们可以发现 Activity 所持有的 Context 的作用域最广,无所不能。因为 Activity 继承自 ContextThemeWrapper,而 Application 和 Service 继承自 ContextWrapper,很显然 ContextThemeWrapper 在 ContextWrapper 的基础上又做了一些操作使得 Activity 变得更强大,这里我就不再贴源码给大家分析了,有兴趣的童鞋可以自己查查源码。上图中的 YES 和 NO 我也不再做过多的解释了,这里我说一下上图中 Application 和 Service 所不推荐的两种使用情况。

  1. 如果我们用 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。

  2. 在 Application 和 Service 中去 layout inflate 也是合法的,但是会使用系统默认的主题样式,如果你自定义了某些样式可能不会被使用。所以这种方式也不推荐使用。

一句话总结:凡是跟 UI 相关的,都应该使用 Activity 做为 Context 来处理;其他的一些操作,Service,Activity,Application 等实例都可以,当然了,注意 Context 引用的持有,防止内存泄漏。

如何获取 Context?

通常我们想要获取 Context 对象,主要有以下四种方法

  1. View.getContext, 返回当前 View 对象的 Context 对象,通常是当前正在展示的 Activity 对象。
  2. Activity.getApplicationContext, 获取当前 Activity 所在的 (应用) 进程的 Context 对象,通常我们使用 Context 对象时,要优先考虑这个全局的进程 Context。
  3. ContextWrapper.getBaseContext(): 用来获取一个 ContextWrapper 进行装饰之前的 Context,可以使用这个方法,这个方法在实际开发中使用并不多,也不建议使用。
  4. Activity.this 返回当前的 Activity 实例,如果是 UI 控件需要使用 Activity 作为 Context 对象,但是默认的 Toast 实际上使用 ApplicationContext 也可以。

getApplication() 和 getApplicationContext()

上面说到获取当前 Application 对象用 getApplicationContext,不知道你有没有联想到 getApplication(),这两个方法有什么区别?相信这个问题会难倒不少开发者。

程序是不会骗人的,我们通过上面的代码,打印得出两者的内存地址都是相同的,看来它们是同一个对象。其实这个结果也很好理解,因为前面已经说过了,Application 本身就是一个 Context,所以这里获取 getApplicationContext() 得到的结果就是 Application 本身的实例。那么问题来了,既然这两个方法得到的结果都是相同的,那么 Android 为什么要提供两个功能重复的方法呢?实际上这两个方法在作用域上有比较大的区别。getApplication() 方法的语义性非常强,一看就知道是用来获取 Application 实例的,但是这个方法只有在 Activity 和 Service 中才能调用的到。那么也许在绝大多数情况下我们都是在 Activity 或者 Service 中使用 Application 的,但是如果在一些其它的场景,比如 BroadcastReceiver 中也想获得 Application 的实例,这时就可以借助 getApplicationContext() 方法了。

public class MyReceiver extends BroadcastReceiver{
  @Override
  public void onReceive(Contextcontext,Intentintent){
    Application myApp= (Application)context.getApplicationContext();
  }
}


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 的正确姿势:

  1. 当 Application 的 Context 能搞定的情况下,并且生命周期长的对象,优先使用 Application 的 Context。
  2. 不要让生命周期长于 Activity 的对象持有到 Activity 的引用。
  3. 尽量不要在 Activity 中使用非静态内部类,因为非静态内部类会隐式持有外部类实例的引用,如果使用静态内部类,将外部实例引用作为弱引用持有。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值