Context使用场景

Context,直译上下文,是Android应用程序区别与JAVA程序的重要标志,在实际开发中被广泛应用,但由于Context有多个种类,使用时如果不注意,可能出现程序crash。

Context类型

阅读源码可得Context的类图,如下图所示(这里只画出了我们关注的类图部分):

从Context的类图中可以看出,Android只为Application, Activity和Service这2个组件提供了Context,所以一个APP中Context的数量应该为:

// 单进程
contextNum = activityNum + serviceNum + 1 (applicationNum)
// 多进程
contextNum = activityNum + serviceNum + processNum

Application的实例数量取决于进程数,APP有几个进程,对应就有几个Application实例。所以如果一个APP中有多个进程,那么在Application的onCreate的初始化部分应该j进程名进行限制,避免初始化组件部分被多次执行。

if (getPackageName().equals(AndroidPlatformUtil.getProcessName(this))) {
    // 主进程初始化部分
}

Context的常见使用场景

  • 页面跳转
  • 根据服务获取系统参数;
  • 在Fragment中获取其Activity对象;
  • 单例模式中的应用
  • 创建View;
  • 弹出Toast;
  • 显示对话框

页面跳转

页面跳转一般我们都是从Activity/Fragment中进行跳转,这种情况下使用Context基本不会出现问题,如果需要在Service或Application进行跳转(不推荐这种方式),则需要新建一个任务栈;

根据服务获取系统参数

获取系统服务时,推荐使用Application类型的Context,避免Activity为空获取系统服务时出现空指针异常。

在Fragment中获取其Activity对象

这种场景使用比较普通,但存在的问题也多,最常见的莫过于getActivity()为null,出现这个问题的根本原因是:Fragment和Activity失去了关联,当Activity被重新创建之后,原来的Fragment并没有被销毁,就导致了与重新创建前的Activity失去了关联,此时在原来的Fragment中再使用getActivity()时就会返回为空。这种情况多出现在Fragment的异步操作中使用getActivity()的场景。所以在使用getActivity()时,最好对其进行非空判断。

在单例模式中使用

在Android开发中,经常会使用单例模式,但是有些单例需要Context,然后就有了以下形式的单例实现:

public class Singleton {

    private Context context;
    private static Singleton instance = null;

    private Singleton(Context context) {
        this.context = context;
    }

    public static Singleton getInstance(Context context) {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton(context);
                }
            }
        }

        return instance;
    }
}

乍一看似乎没什么问题,但是回归到单例模式的本质,就会发现出现了内存泄漏。单例模式的特点就是在程序运行期间有且只有一个实例,也就是说,一旦创建,就始终在内存中(如果需要销毁,就没必要使用单例模式了),但是Activity类型的Context的生命周期仅限于页面的生命周期,所以在这种单例模式中,如果传入的Context是Activity类型的,则会导致页面关闭时Context无法释放,最终导致内存泄漏。

解决方案
  1. 传入Application类型的Context,因为Application类型的Context的生命周期就是APP的运行时间;
  2. 创建单例对象时将将传入的context转换成Application类型的Context;
public static Singleton getInstance(Context context) {
    if (instance == null) {
        synchronized (Singleton.class) {
            if (instance == null) {
                instance = new Singleton(context.getApplicationContext());
            }
        }
    }

    return instance;
}

弹出Toast

/**
 * Make a standard toast that just contains a text view.
 *
 * @param context  The context to use.  Usually your {@link android.app.Application}
 *                 or {@link android.app.Activity} object.
 * @param text     The text to show.  Can be formatted text.
 * @param duration How long to display the message.  Either {@link #LENGTH_SHORT} or
 *                 {@link #LENGTH_LONG}
 *
 */
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
    return makeText(context, null, text, duration);
}

从设置toast内容的方法说明中可以看出,makeText中的Context既可以是Activity类型的,也可以是Application类型,所以为了避免Activity类型的Context为null出现闪退,推荐使用Application类型的Context。当然,在实际开发中我们也不会直接使用makeText来弹出toast信息, 毕竟参数太多,最后还要show,相对比较麻烦,所以都会进行简单的封装。

public class ToastUtil {

    private static Context mContext;

    // 在Application中初始化
    pulbic static init(Context context) {
        mContext = context.getApplicationContext();
    }

    public static show(String msg) {
        Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show();
    }
}

当然,也可对Toast的样式进行自定义:

public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
    Toast result = new Toast(context);

    LayoutInflater inflate = (LayoutInflater)
            context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
    TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
    tv.setText(text);

    result.mNextView = v;
    result.mDuration = duration;

    return result;
}

从源码可知,Toast的本质就是一个TextView, 通过创建TextView, 设置样式,然后通过Toast.setView(textView)即可修改默认的显示样式,有兴趣的尝试一下。

显示对话框

创建对话框时Context必须使用Activity类型的,当Activity被销毁后再弹出对话框时(通常是Activity被系统杀掉了),APP就会crash,并报以下错误:

android.view.WindowManager$BadTokenException

Unable to add window -- token android.os.BinderProxy@25c2334 is not valid; is your activity running?

所以在显示对话框时,需要先判断当前的Activity是否还处于运行中,从而避免APP出现不可控制的闪退。

总结

Context是Android开发中的一个重要角色,了解不同类型的Context的使用场景,可从根本上避免很多意外的Crash。

,