通过源码理解 Android Toast 如何展示
Toast 的使用很简单,只要把 context、message和duration传入,最后 show 就可以展示,但是你知道 show 了之后是如何展示的吗?
Toast.makeText(mContext, "message", Toast.LENGTH_SHORT).show();
接下来通过 show 一步步通过原来来揭示到底是如何展示到屏幕上的吧
看到首先是通过 getService 方法获得一个 INotificationManager 的 service 对象,然后 enqueueToast 进去。
看到 enqueueToast 我们应该就明白了,所有的 Toast 都是由一个队列管理的
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}
再来自习看看 getService 方法,INotificationManager.Stub,一看就是 AIDL 的方法来获取一个service方法,看到返回值是 INotificationManager,我猜应该就是 NotificationManager 相关的 Service
static private INotificationManager getService() {
if (sService != null) {
return sService;
}
sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
return sService;
}
去搜了下,果然是 NotificationManagerService ,接下来看下他是如何执行 enqueueToast 的吧
果然,NotificationManagerService 实现了 INotificationManager.Stub ,就是他了,来看下如何 enqueueToast,下面只列出了关键源码
private final IBinder mService = new INotificationManager.Stub() {
// Toasts
// ============================================================================
@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
....
// 加锁,为了防止多线程Toast异常
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
int index;
// All packages aside from the android package can enqueue one toast at a time
// 所有的Android 应用包,同一时间只能enqueue一个toast进去,所以我看到展示下一个toast,会把上一个toast去掉
if (!isSystemToast) {
index = indexOfToastPackageLocked(pkg);
} else {
index = indexOfToastLocked(pkg, callback);
}
// If the package already has a toast, we update its toast
// in the queue, we don't move it to the end of the queue.
// index >= 0 表示当前应用的 toast 已经存在了,直接给更新当前 Toast ToastRecord 的数据
// 如果不存在,创建一个新的 ToastRecord
// 把 callback 设置到 ToastRecord 里面
if (index >= 0) {
record = mToastQueue.get(index);
record.update(duration);
record.update(callback);
} else {
Binder token = new Binder();
mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
record = new ToastRecord(callingPid, pkg, callback, duration, token);
mToastQueue.add(record);
index = mToastQueue.size() - 1;
}
keepProcessAliveIfNeededLocked(callingPid);
// If it's at index 0, it's the current toast. It doesn't matter if it's
// new or just been updated. Call back and tell it to show itself.
// If the callback fails, this will remove it from the list, so don't
// assume that it's valid after this.
// index 返回 0,才是当前的toast,所以展示当前toast,都走下面的逻辑
if (index == 0) {
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
}
看看 showNextToastLocked 具体如何展示 Toast 的
void showNextToastLocked() {
// 取出第一个 toast
ToastRecord record = mToastQueue.get(0);
while (record != null) {
if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
try {
// 直接回调 callback 里面的 show 方法
// 发现这个 callback 是 Toast 传过来的,TN tn = mTN;
record.callback.show(record.token);
scheduleTimeoutLocked(record);
return;
} catch (RemoteException e) {
Slog.w(TAG, "Object died trying to show notification " + record.callback
+ " in package " + record.pkg);
// remove it from the list and let the process die
int index = mToastQueue.indexOf(record);
if (index >= 0) {
mToastQueue.remove(index);
}
keepProcessAliveIfNeededLocked(record.pid);
if (mToastQueue.size() > 0) {
record = mToastQueue.get(0);
} else {
record = null;
}
}
}
}
兜兜转转,其实最后展示还是在 Toast 里面做的,看看 Toast 里面的 TN 做了啥吧
private static class TN extends ITransientNotification.Stub {
....
TN(String packageName, @Nullable Looper looper) {
....
// 当 looper 为空的时候,直接用的是当前线程的 Looper
// 所以在其他线程要展示一个 Toast 的话,必须要有 Looper 才行,可以再HandlerThread里面展示一个Toast
if (looper == null) {
// Use Looper.myLooper() if looper is not specified.
looper = Looper.myLooper();
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 方法展示 Toast
handleShow(token);
break;
}
case HIDE: {
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
break;
}
case CANCEL: {
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null;
try {
getService().cancelToast(mPackageName, TN.this);
} catch (RemoteException e) {
}
break;
}
}
}
};
}
/**
* schedule handleShow into the right thread
*/
@Override
// show 方法,其实是发送了一个Handler
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
}
来看下真正的 show 的时候做了啥吧,其实就是一个Window的展示过程,Activity,Dialog都是这个过程
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();
}
// 获取 WindowManager
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
// We can resolve the Gravity here by using the Locale for getting
// the layout direction
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);
// Since the notification manager service cancels the token right
// after it notifies us to cancel the toast there is an inherent
// race and we may attempt to add a window after the token has been
// invalidated. Let us hedge against that.
try {
// 在 WindowManager 里面 addView
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
}
WindowManager 也是通过 getSystemService 方法获取的,具体实现是在 WindowManagerImpl 里面,看下 addView方法
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
发现 WindowManagerImpl 也没干啥,最后是通过 WindowManagerGlobal 类来实现的,这里的 addView 代码比较多,只放一些关键代码
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
...
// 创建一个 ViewRootImpl
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
// 把View和ViewRootImpl记录下来
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
// 把当前 View 放到 ViewRootImpl 里面,这里面是View真正 Layout, draw的地方
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
来具体看 ViewRootImpl 的 setView 方法怎么做的吧
为了便于分析流程,我把 ViewRootImpl setView 方法里面的其他过程都删了,只留下了 requestLayout,
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
...
requestLayout();
...
}
}
}
requestLayout都知道就是对View就Layout绘制,具体看下如何做的吧
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
// 执行绘制的地方
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// mChoreographer 是编导者,屏幕的绘制都是由他执行的,基本16ms一次回调,把它加入下一次运行中
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
看下 mTraversalRunnable 是做什么的吧
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
一系列操作后,看到最终执行的就是 performTraversals, 可以看到就是执行了View绘制的三个步骤,measure, layout, draw
private void performTraversals() {
...
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, mWidth, mHeight);
...
performDraw();
}
最终就展示到屏幕上了
结论
-
Toast是一个有序队列,一个应用只允许有一个Toast
-
执行 Toast 的线程必须有 Handler
所以我们一般都在主线程弹一个 Toast,其实只要在任意一个有 Handler 的线程 Toast 都行。不然会报找不到 Looper 的错误