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无法释放,最终导致内存泄漏。
解决方案
- 传入Application类型的Context,因为Application类型的Context的生命周期就是APP的运行时间;
- 创建单例对象时将将传入的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。