Andorid中的Toast是一种比较常见的系统提示框。因为常见,所以常常忽略了其细节。经常使用的人都知道,Toast不能在没有Looper的线程显示提示框。那么下面通过源码来探究其原理。
先来看看其makeText方法:
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
return makeText(context, null, text, duration);
}
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration) {
Toast result = new Toast(context, looper);//创建对象
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;
}
好像看不出来什么端倪。再来看看show方法:
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();//NotificationManagerService
String pkg = mContext.getOpPackageName();
TN tn = mTN;//...
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);//IPC调用
} catch (RemoteException e) {
// Empty
}
}
static private INotificationManager getService() {
if (sService != null) {
return sService;
}
sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
return sService;
}
发现有两个令人非常在意的地方。一个是其show方法明显调用的是代理类的方法。通过getService可以看出来,真正的enqueueToast是在NotificationManagerService中实现的。
第二个是入参,tn是我们未知的,那么回头去找一找,发现在刚才的makeText方法中有一个new Toast()的操作,回去看一看:
public Toast(@NonNull Context context, @Nullable Looper looper) {
mContext = context;
mTN = new TN(context.getPackageName(), looper);//tn
mTN.mY = context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.toast_y_offset);
mTN.mGravity = context.getResources().getInteger(
com.android.internal.R.integer.config_toastDefaultGravity);
}
找到了mTN,那么这个mTN是何方神圣呢?看下面:
TN(String packageName, @Nullable Looper looper) {
......
if (looper == null) {
// Use Looper.myLooper() if looper is not specified.
looper = Looper.myLooper();//如果当前线程调用了Looper.prepare(),那么这里是会拿到当前线程的looper的。详情参考Handler有关文章。
if (looper == null) {
throw new RuntimeException(
"Can't toast on a thread that has not called Looper.prepare()");
}
}
mHandler = new Handler(looper, null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case SHOW: {
IBinder token = (IBinder) msg.obj;
handleShow(token);
break;
}
case HIDE: {
handleHide();
mNextView = null;
break;
}
case CANCEL: {
handleHide();
mNextView = null;
try {
getService().cancelToast(mPackageName, TN.this);
} catch (RemoteException e) {
}
break;
}
}
}
};
}
这下找到原因了,原来异常是在这里抛出来的。这个looper是我们在makeText方法中一路传过来的。
return makeText(context, null, text, duration);
那么为什么要这么做呢?为什么这里要特别对looper做要求呢?回到刚才的show方法,是不是把tn当作参数传给了NotificationManagerServer。了解过AIDL和binder机制之后,我有一个大胆的想法,那就是这个tn是一个回调接口,NotificationManagerServer会调用其对应的方法,由于binder运行在binder线程池,所以有这里的线程切换要求。
结合TN这个类来看,果然如此。·
private static class TN extends ITransientNotification.Stub{
......
@Override
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
......
}
看TN的父类形式就是一个典型的BinderServer,这里的show方法和Toast里面的show方法不同,这个方法由
NotificationManagerServer调用,然后会切换到调用Toast.makeText的线程,然后调用handlerShow方法。如下:
public void handleShow(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
// If a cancel/hide is pending - no need to show - at this point
// the window token is already invalid and no need to do any work.
if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
return;
}
if (mView != mNextView) {
// remove the old view if necessary
handleHide();
mView = mNextView;
Context context = mView.getContext().getApplicationContext();
String packageName = mView.getContext().getOpPackageName();
if (context == null) {
context = mView.getContext();
}
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
......
try {
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
}
这里就是常规的addView操作。