Toast 源码分析
思考
在分析源码之前有两个问题
子线程直接使用 Toast 时会抛出异常 “Can’t create handler inside thread that has not called Looper.prepare()”, 在 Toast 类里搜 “Looper” 并搜不到。是哪里抛出的这个异常呢?
在子线程中这样使用 Toast:
Looper.prepare();
Toast.makeText(getApplicationContext(), "test toast", Toast.LENGTH_SHORT).show();
Looper.loop();
像上面这种调用是在主线程显示 Toast 吗
Toast 源码分析
先看 Toast 的 makeText 和 show 方法:
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
// 创建 TN(Toast 的内置对象),用于展示 Toast 的主要类
Toast result = new Toast(context);
// 加载 Toast 的展示内容
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;
}
public void show() {
// 校验显示内容
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService(); // NotificationManagerService 的成员变量 mService 是 INotificationManager.Stub 的实现类
String pkg = mContext.getOpPackageName();
TN tn = mTN;
// 展示内容属性赋值给 tn 的属性
tn.mNextView = mNextView;
try {
// 调用 NotificationManagerService 中 mService 的 enqueueToast 方法
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
下面来看 NotificationManagerService 中 mService 的 enqueueToast 方法:
// callback 就是 TN 实例
@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
// ...
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
int index = indexOfToastLocked(pkg, callback);
// If it's already in the queue, we update it in place, we don't
// move it to the end of the queue.
if (index >= 0) {
record = mToastQueue.get(index);
record.update(duration);
} else {
// ...
Binder token = new Binder();
mWindowManagerInternal.addWindowToken(token,
WindowManager.LayoutParams.TYPE_TOAST);
// 创建 ToastRecord 对象
record = new ToastRecord(callingPid, pkg, callback, duration, token);
// mToastQueue 添加记录
mToastQueue.add(record);
index = mToastQueue.size() - 1;
keepProcessAliveIfNeededLocked(callingPid);
}
if (index == 0) {
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
上面代码可以看到有往 mToastQueue 中添加记录,那么什么时候执行呢,全局搜 “mToastQueue.get(” 看到在 showNextToastLocked 方法中取出 mToastQueue 的第 0 个元素,调用 record.callback.show(record.token), record.callback 就是 TN 对象,也就是这里调了 TN 的 show 方法。下面就来看一下 TN 的 show 方法做了什么:
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
// 交给 mHandler 处理
mHandler.obtainMessage(0, windowToken).sendToTarget();
}
// TN 的 mHandler
final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// 所以 TN 的 show 方法处理的是这里
IBinder token = (IBinder) msg.obj;
handleShow(token);
}
};
下面来看核心展示 Toast 的方法,也就是 TN 的 handleShow() 方法
public void handleShow(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+ " mNextView=" + mNextView);
if (mView != mNextView) {
handleHide();
mView = mNextView;
// ...
// 展示使用的是 WindowManager
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
// 下面配置的是位置参数
final Configuration config = mView.getContext().getResources().getConfiguration();
final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
mParams.gravity = gravity;
if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
mParams.horizontalWeight = 1.0f;
}
if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
mParams.verticalWeight = 1.0f;
}
mParams.x = mX;
mParams.y = mY;
mParams.verticalMargin = mVerticalMargin;
mParams.horizontalMargin = mHorizontalMargin;
mParams.packageName = packageName;
mParams.hideTimeoutMilliseconds = mDuration ==
Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
mParams.token = windowToken;
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeView(mView);
}
if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
// 展示 Toast, 由此也可以看出,Toast 是被添加在 Window 下的
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
}
}
总结
Toast 是先调用 makeText 创建要展示的 view, 调用 show 方法时内部跨进程通知 NotificationManagerService, 再通知到 TN 执行 show 方法,最终通过 handler 添加到 Window 下。
再看下最开始的思考,
makeText 会 new Toast, Toast 构造方法里创建 TN 对象,TN 有一个成员变量 mHandler, 在创建 Handler 时需要该线程有 Looper 对象,主线程的 ActivityThread 的 main() 方法有创建 Looper 对象,但子线程并没有 Looper 对象,所以会抛异常。
如果在子线程调 Looper.prepare(), 会是在主线程展示的 Toast 吗,看上面的代码很明显不是,因为 Toast, TN 都是在子线程创建的,所以 mHandler 也是在子线程创建的,处理消息时的 Looper 也是子线程创建的,所以展示也是在子线程的。