Context

  参考 Android 8.0 源码

Context简介

  Context是一个抽象类,它有两个具体的实现子类:ContextImpl和ContextWrapper。

  ContextWrapper类,是一个包装而已,在ContextWrapper构造函数中必须包含一个Context引用,同时ContextWrapper中提供了attachBaseContext()用于给ContextWrapper对象中指定真正的Context对象,调用ContextWrapper的方法都会被转向其所包含的真正的Context对象。ContextWrapper类有好几个子类,主要的有ContextThemeWrapper、Application、Service三个。ContextThemeWrapper类,其内部包含了与主题Theme相关的接口,这里所说的主题就是指在AndroidManifest.xml中通过android:theme为Application元素或者Activity元素指定的主题。当然,只有Activity才需要主题,Service因为是没有界面的后台场景所以不需要主题的。所以Service直接继承自ContextWrapper,而Activity则继承自ContextThemeWrapper类,Application也是不需要主题的,所以也是直接继承自ContextWrapper。

  ContextImpl类则是真正实现了Context中的所有函数,应用程序中所调用的各种Context类的方法,其实均来自于该类。

  Context的两个子类分工明确,其中ContextImpl是Context的具体实现类,ContextWrapper是Context的包装类。Activity、Application、Service虽然都继承自ContextWrapper(Activity继承自ContextWrapper的子类ContextThemeWrapper),但它们最初初始化的过程中都会创建ContextImpl对象,由ContextImpl实现Context中的方法。

public abstract class Context {
    //代码省略;
}
public class ContextWrapper extends Context {
    //代码省略;
}
class ContextImpl extends Context {
    //代码省略;
}
public class ContextThemeWrapper extends ContextWrapper {
    //代码省略;
}
public class Application extends ContextWrapper implements ComponentCallbacks2 {
    //代码省略;
}
public abstract class Service extends ContextWrapper implements ComponentCallbacks2 {
    //代码省略;
}
public class Activity extends ContextThemeWrapper
        implements LayoutInflater.Factory2,
        Window.Callback, KeyEvent.Callback,
        OnCreateContextMenuListener, ComponentCallbacks2,
        Window.OnWindowDismissedCallback, WindowControllerCallback,
        AutofillManager.AutofillClient {
    //代码省略; 
}

Context使用技巧

获取全局Context

  我们在很多地方都是需要使用Context的,在显示Toast的时候,在启动活动的时候,在发送广播的时候,在操作数据库的时候等等。所以我们一般会设置一个全局Context,这样会方便我们获取到Context。

public class MyApplication extends Application {

    private static Context mContext;
    
    @Override
    public void onCreate() {
        super.onCreate();
        mContext = getApplicationContext();
    }

    public static Context getContext(){
        return mContext;
    }
    
}

一个应用程序中有几个Context

  在应用程序中,Context的具体实现子类是Activity,Service,Application。那么Context数量 = Activity数量 + Service数量 + 1。四大组件的另外两个BroadcastReceiver和ContentProvider,并不是Context的子类,它们所持有的Context都是其他地方传过去的,所以不计入Context总数。

Context的作用域

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

Context作用域 Application Activity Service
显示Dialog No Yes No
启动Activity 不推荐 Yes 不推荐
layout inflate 不推荐 Yes 不推荐
启动Service Yes Yes Yes
发送BroadcastReceiver Yes Yes Yes
注册BroadcastReceiver Yes Yes Yes
加载资源 Yes Yes Yes

  Activity持有的Context的作用域最广。因为Activity继承自ContextThemeWrapper,而Application和Service继承自ContextWrapper,很显然ContextThemeWrapper在ContextWrapper的基础上又做了一些操作使得Activity变得更强大。

  在这里讲解一下上图中Application和Service所不推荐的两种使用情况。

  • 不推荐使用Application中的Context去启动一个LaunchMode为standard的Activity:
      因为非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所在的应用进程的ApplicationContext对象,通常我们使用Context对象时,要优先考虑这个全局的进程Context。
  • ContextWrapper.getBaseContext():用来获取一个ContextWrapper进行装饰之前的Context,可以使用这个方法。但是这个方法在实际开发中使用并不多,也不建议使用。
  • Activity.this:返回当前的Activity实例,如果是UI控件需要使用Activity作为Context对象,但是默认的Toast实际上使用ApplicatinContext也可以。

getApplication()和getApplicationContext()的区别

  其实这两个方法获取到的是同一个对象。但是这两个方法在作用域上面有比较大的区别。getApplication()只有在Activity和Service中才能调用到。那么在其他一些场景中,比如BroadcastReceiver中也想获取Application的实例,这时就可以借助getApplicationContext()方法了。

Context引起的内存泄漏

  Context如果使用不当,会引起内存泄漏问题。下面列举两种错误的引用方式:

1 . 错误的单例模式

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 A被销毁掉,但因为它的引用还存在于一个Singleton中,就不可能被GC掉,这样就导致了内存泄漏。

2 . 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会将自身设置到Drawable的callback上去,因为View是实现了Drawable.Callback接口, 这样当Drawable需要刷新的时候,可以调用.这个Callback,然后通知View重新绘制该Drawable. 所以引用的正确顺序应该Drawable->View->Context,因为被static修饰的mDrawable是常驻内存的,MainActivity是它的间接引用,MainActivity被销毁时,也不能被GC掉,所以造成内存泄漏。

  在ImageView中updateDrawable该方法中 有一个d.setCallback(this),ImageView将自身设置到Drawable的callback上去,当Drawable需要刷新的时候,可以调用.这个Callback,然后通知View重新绘制该Drawable.

public void setImageDrawable(@Nullable Drawable drawable) {
    if (mDrawable != drawable) {
        mResource = 0;
        mUri = null;

        final int oldWidth = mDrawableWidth;
        final int oldHeight = mDrawableHeight;

        updateDrawable(drawable);//1

        if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
            requestLayout();
        }
        invalidate();//2
    }
}

  使用context的时候,还有以下几个方面来防止内存泄漏:
1 . 不要让生命周期长的对象引用Activity的context,即保证引用Activity的对象要与Activity本身生命周期是一样的;
2 . 对于生命周期长的对象,可以使用Application的context;
3 . 避免非静态内部类,尽量使用静态类,避免生命周期问题,注意内部类对外部对象引用导致的生命周期的变化;

正确使用Context

  一般Context造成的内存泄漏,几乎都是当Context销毁的时候,却因为被引用导致销毁失败,而Application的Context对象可以理解为随着进程存在的,所以我们可以根据以下方式来使用:
1 . 当Application的Context能搞定的情况下,并且生命周期长的对象,优先使用Application的Context;
2 . 不要让生命周期长于Activity的对象持有Activity引用;
3 . 尽量不要在Activity中使用非静态内部类,因为静态内部类会隐式持有外部类实例的引用,如果使用静态内部类,将外部实例引用作为弱引用持有;

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
----------------------last line for now--------------------