一场Toast引发的血案---Toast的显示

最普通不过的Toast我们进场使用,有没有想过这样的问题,可不可以在显示的时候取消Toast的显示,Toast到底是被添加到哪个Window的,WindowManager是如何添加Toast的,在添加Toast的时候总共有几个WindowManager参与,参与Toast显示的WindowManager是如何产生的……

好吧,想到这么多的时候,我感觉头绪有点乱,还是从最简单的开始吧,先看下Toast的显示吧。

最常用的方式如下:

Toast.makeText(context, "test", Toast.LENGTH_SHORT).show();

跟踪下代码:

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;
}

Toast的对象和普通的对象没有什么区别,先新建一个对象,然后给Toast对象设置一个view,再设置Toast的显示时常,然后makeText就直接返回,接下来就需要调用show方法,看下show方法

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
    }
}

show方法是一个跨进程调用,最终会调用NotificationManagerServiceenqueueToast方法,这里有一个TN类型的tn对象,这个对象的作用是接收来自NotificationManagerService的回调,后面会对这个对象进行解释,接下来进入NotificationManagerService,看下它是如何处理Toast

@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
    ......
    synchronized (mToastQueue) {
        ......
        try {
            ToastRecord record;
            int index;
            if (!isSystemToast) {
                index = indexOfToastPackageLocked(pkg);
            } else {
                ......
            }

            // 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.
            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;
            }
            ......
            // 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.
            if (index == 0) {
                showNextToastLocked();
            }
        } finally {
            ......
        }
    }
}

最终函数走进了showNextToast的调用。

 void showNextToastLocked() {
    ToastRecord record = mToastQueue.get(0);
    while (record != null) {
        if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
        try {
            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;
            }
        }
    }
}

showNextToast方法再mToastQueue上进行循环,如果找到待显示的Toast则调用record.callback.show函数进行显示,并执行scheduleTimeoutLocked函数。
record.callback.show函数后面会重点说明,先理解一下scheduleTimeoutLocked函数

scheduleTimeoutLocked

private void scheduleTimeoutLocked(ToastRecord r)
{
    mHandler.removeCallbacksAndMessages(r);
    Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
    long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
    mHandler.sendMessageDelayed(m, delay);
}

返现scheduleTimeoutLocked函数还是很简单的,就是根据显示时的时长去触发一个message。这个地方有一个Toast使用经常会遇到的问题,直接使用Toast.makeText是无法控制Toast的显示时长的,因为系统处理的时候仅仅设置了LONG_DELAYSHORT_DELAY,究竟是不是这个原因,再往下看。

代码段1
public void handleMessage(Message msg)
    {
        switch (msg.what)
        {
            case MESSAGE_TIMEOUT:
                handleTimeout((ToastRecord)msg.obj);
                break;
            ......
        }
    }
代码段2
private void handleTimeout(ToastRecord record)
    {
        if (DBG) Slog.d(TAG, "Timeout pkg=" + record.pkg + " callback=" + record.callback);
        synchronized (mToastQueue) {
            int index = indexOfToastLocked(record.pkg, record.callback);
            if (index >= 0) {
                cancelToastLocked(index);
            }
        }
    }
代码段3
void cancelToastLocked(int index) {
    ToastRecord record = mToastQueue.get(index);
    try {
        record.callback.hide();
    } catch (RemoteException e) {
        Slog.w(TAG, "Object died trying to hide notification " + record.callback
                + " in package " + record.pkg);
        // don't worry about this, we're about to remove it from
        // the list anyway
    }

    ToastRecord lastToast = mToastQueue.remove(index);
    mWindowManagerInternal.removeWindowToken(lastToast.token, true, DEFAULT_DISPLAY);

    keepProcessAliveIfNeededLocked(record.pid);
    if (mToastQueue.size() > 0) {
        // Show the next one. If the callback fails, this will remove
        // it from the list, so don't assume that the list hasn't changed
        // after this point.
        showNextToastLocked();
    }
}

代码段2是MESSAGE_TIMEOUT的具体处理逻辑,可以看到,系统执行cancelToastLocked方法取消了Toast的显示,如果mToastQueue中还存在未显示的Toast,则继续执行显示。看来直接使用Toast.makeText无法控制Toast的显示时长的原因就是这儿了。

record.callback.show

这个方法是Toast显示的核心,还记得record的callback指的是什么吗,不记得的同学往前面看即可。callback指的是toast的内部类的TN类型的对象,因此show方法调用的也是TNshow函数。

public void show(IBinder windowToken) {
        if (localLOGV) Log.v(TAG, "SHOW: " + this);
        mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
    }

show函数很好理解,但是windowToken从哪来的呢?可以看下前面enqueueToast函数的实现,为了实现NotificationServiceManager的跨进程调用,在封装ToastRecord对象时新建了一个binder对象作为windowToken,继续…

public void handleShow(IBinder windowToken) {
        .....
        if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
            return;
        }
        if (mView != mNextView) {
            // 删除之前显示的Toast
            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);
            // 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 {
                mWM.addView(mView, mParams);
                trySendAccessibilityEvent();
            } catch (WindowManager.BadTokenException e) {
                /* ignore */
            }
        }
    }

代码看的比较长,实际上逻辑比较简单,就是将Toast的view添加到Windowmanager。具体的流程:判断当前还有没有正在显示的Toast,如果有则先取消,然后给准备显示的Toast设置显示位置,最后将Toast显示出来。

猜你喜欢

转载自blog.csdn.net/rockstore/article/details/80952254