通过源码理解 Android Toast 如何展示

通过源码理解 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();

}

最终就展示到屏幕上了

结论

  1. Toast是一个有序队列,一个应用只允许有一个Toast

  2. 执行 Toast 的线程必须有 Handler

    所以我们一般都在主线程弹一个 Toast,其实只要在任意一个有 Handler 的线程 Toast 都行。不然会报找不到 Looper 的错误

发布了29 篇原创文章 · 获赞 3 · 访问量 1123

猜你喜欢

转载自blog.csdn.net/qq_16927853/article/details/103050325