阅读 484

Android上下文Context的使用说明书

Context概述以及继承关系

Context意为上下文,是一个应用程序环境的入口。在开发中我们经常会用到Context比如:

  • 1、使用Context调用方法,启动Activity、访问资源、调用系统服务等
  • 2、将Context作为参数传入方法中,弹出Toast、创建dialog等

Activity、Service、Application都间接的继承自Context,那么一个应用程序进程中有多少个Context呢? 我们看一下Context的关系树就知道了。

image.png

由此可见

Context的数量=Activity的数量+Service的数量+1(application的数量)

这个类图看着是不是很眼熟呢?不错,这就是装饰者模式的类图,Context的设计就是基于装饰者模式的。(以后会单独解析装饰者模式)

再来看下源码中的类关系

image.png

  • Context:是一个抽象类,主要提供一些通用的抽象方法
  • ContextImpl:Context的具体实现类,在Android的源码中大量使用接口和抽象类,我们在刚开始阅读源码的时候,经常会找到顶层接口之后,就不知道下一步找谁了,遇到这种情况,一般就是要找接口的实现类,实现类中如何实现才是寻找答案的关键,通常这些实现类都是xxxxImpl.java以impl结尾的,这也算是阅读源码的一个小技巧吧。
  • ContextWrapper:Context的包装类,内部持有一个ContextImpl的实例对象mBase,对Context的操作,最终都会引申到mBase中。
  • ContextThemeWrapper:该类内部包含主题相关的方法。而Service不需要主题,所以Service继承自ContextWrapper,这也是Activity和Service的Context的主要区别点。

不同Context使用上的区别

20190531205240514.png

为什么会有这些区别以及随意使用context会造成的问题

1:启动Activity:

如果我们用ApplicationContext去启动一个LaunchMode为standard的Activity的时候会报错getApplication().startActivity(new Intent(this, NewActivity.class));, 这是因为非Activity类型的Context并没有所谓的任务栈,所以待启动的Activity就找不到栈了。解决这个问题的方法就是为待启动的Activity指定FLAG_ACTIVITY_NEW_TASK标记位,这样启动的时候就为它创建一个新的任务栈

contextImpl的startActivity()方法中options == null image.png

这是因为在源码中有这样一段话(基于 SDK 29 源码)

image.png

在Android7.0-8.0 的中加入了options != null的判断,options是Bundle类型,正常启动Activity如果不传Bundle的时候,就不会抛出异常提醒。后来在Android9.0上又改成options == null了

下图是sdk基于26时的源码 image.png

2.创建dialog

使用application的context创建Dialog dialog = new Dialog(getApplicationContext()); 会报这样的错误

image.png 这又是为什么呢? 首先看一下Activity的attach()方法:

 //activity
 
 final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {
        attachBaseContext(context);
        ...
        //创建PhoneWindow
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        ...
        //设置windowMananger 其中第二个参数为token
        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        ...
    }

复制代码

activity被创建后,会创建PhoneWindow和并设置windowManager绑定token。 这里的 IBinder 对象 mToken 很重要。它是一个 Binder 对象,可以在 app 进程,system_server 进程之间进行传递。和我们通常所说的 Token 一样,这里也可以把它看做是一种特殊的令牌,用来标识 Window ,在对 Window 进行视图操作的时候就可以做一些校验工作。

所以,Activity 对应的 Window/WMS 都是持有这个 mToken 的。结合之前 Application 创建 Dialog 的报错信息,我们可以大胆猜测 Application Context 创建 Dialog 的过程中,并没有实例化类似的 token。

Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {

        //获取context中的windowManager
        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        //创建window 并设置windowmanager
        final Window w = new PhoneWindow(mContext);
        w.setWindowManager(mWindowManager, null, null);
    }

复制代码

再来看看dialog的构造函数,从传入的context中拿到windowManager并创建新的window。我们知道dialog本质上也是一个window,显示的时候也是要通过WindowManager的addView()方式显示的。 看一下dialog的show()方法

//dialog

public void show() {
        //通过addView方法-->windowManagerImpl的addView-->windowManagerGlobal的addView-->viewRootImpl的setView...
        mWindowManager.addView(mDecor, l);
       
        sendShowMessage();
    }
    
复制代码

如果传入的 Context 是 Activity,返回的是在 Activity.attach() 方法中创建的 mWindowManager 对象,这个时候 mToken 也已经绑定。

如果传入的 Context 是 Application,最终调用的是父类 ContextImpl 的方法。并没有绑定token

所以,Android 不允许 Activity 以外的 Context 来创建和显示普通的 Dialog (Toast这样的除外)。

3.生命周期导致的内存泄漏

在项目过程中,我们存在很多工具类,这些工具类不限于网络请求及一些工具方法,这些工具方法大多数是需要访问资源的,这时就需要Context的参与

public class SingleInstanceTest {

    private SingleInstanceTest(final Context context) {
        this.mContext = context;
    }
    
    private Context mContext;
    
    public static volatile SingleInstanceTest singleInstanceTest;

    public static SingleInstanceTest getInstance() {
        if (singleInstanceTest == null) {
            synchronized (SingleInstanceTest.class) {
                if (singleInstanceTest == null) {
                    singleInstanceTest = new SingleInstanceTest(context);
                }
            }
        }
        return singleInstanceTest;
    }
}
复制代码

一个简单的单例模式,内部持有一个context的引用,这么写是没有问题的,问题在于,这个Context哪来的我们不能确定,很大的可能性,你在某个Activity里面为了方便,直接传了个this;这样问题就来了,我们的这个类中的singleInstanceTest是一个static且强引用的,在其内部引用了一个Activity作为Context,也就是说,我们的这个Activity只要我们的项目活着,就没有办法进行内存回收。而我们的Activity的生命周期肯定没这么长,所以造成了内存泄漏。

如何解决呢?

   public static SingleInstanceTest getInstance() {
        if (singleInstanceTest == null) {
            synchronized (SingleInstanceTest.class) {
                if (singleInstanceTest == null) {
                    //使用ApplicationContext 因为ApplicationContext的生命周期跟app的周期一样长
                    singleInstanceTest = new SingleInstanceTest(context.getApplicationContext());
                }
            }
        }
        return singleInstanceTest;
    }
复制代码

这样,我们就解决了内存泄漏的问题,因为我们引用的是一个ApplicationContext,它的生命周期和我们的单例对象一致。

文章分类
Android
文章标签