乐趣区

关于android:Android中Toast的显示和隐藏分析

PS:本文系转载文章,浏览原文可读性会更好,文章开端有原文链接

ps:源码是基于 android api 27 来剖析的,demo 是用 kotlin 语言写的。

Toast 作为 Android 零碎中最罕用的类之一,因为它不便的 API 设计和简洁的交互体验,所以咱们会常常用到,也所以深刻学习 Toast 也是很有必要的;在 Android 中,咱们晓得但凡有视图的中央就会有 Window,Toast 显示进去的提醒也属于视图,所以 Toast 依赖于 Window,而且还是零碎窗口,Window 对象是 WindowManagerService 这个类所治理;依据 Type 参数可划分 Window 类型,Window 可分为利用 Window、子 Window 和零碎 Window;Window 是有分层的,其中利用 Window 的层级范畴是 1~99,子 Window 的层级范畴是 1000~1999,零碎 Window 的层级范畴是 2000~2999,这些层级范畴对应着 WindowManager.LayoutParams 的 type 参数;最大的层级其余 Window 的最顶层,所以零碎 Window 在显示问题上具备最高优先级。为什么说 Toast 是零碎 Window 呢?咱们来看 Toast 的指定类型;

WindowManager 的外部类 LayoutParams 中的常量定义;

public static final int FIRST_SYSTEM_WINDOW = 2000;
public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;

能够看出 Toast 的 Type 值为 2005,属于零碎 Window,确保了在 Activity 所在的窗口之上以及其余的利用下层显示。

好了,到了这里,咱们要开始剖析 Toast 的显示和暗藏的源码了,先从 Toast 的 makeText(Context context, CharSequence text, @Duration int duration) 办法开始;

public static Toast makeText(Context context, CharSequence text, @Duration int duration) {

    return makeText(context, null, text, duration);

}

//1、
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 的 makeText(Context context, CharSequence text, @Duration int duration) 办法最终调用了 Toast 中正文 1 中的办法,该正文 1 中的办法是创立一个 Toast 对象并用 transient_notification.xml 布局创立一个视图,从该视图中获取 TextView,用这个 TextView 设置提醒的内容,而后将视图赋值给 Toast 对象的 mNextView,将 duration 赋值给 Toast 对象的 mDuration,最初返回 Toast 对象。

咱们来看 Toast.LENGTH_SHORT 和 Toast.LENGTH_LONG 这 2 个真正的延长时间是多少,看一下 NotivicationManagerService 的 scheduleTimeoutLocked 办法;

@GuardedBy(“mToastQueue”)
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);

}

看一下 NotivicationManagerService 的 LONG_DELAY 和 SHORT_DELAY 申明;

static final int LONG_DELAY = PhoneWindowManager.TOAST_WINDOW_TIMEOUT;
static final int SHORT_DELAY = 2000; // 2 seconds

再看一下 PhoneWindowManager 的 TOAST_WINDOW_TIMEOUT 申明;

public static final int TOAST_WINDOW_TIMEOUT = 3500; // 3.5 seconds

所以 Toast.LENGTH_SHORT 的延时工夫为 2s,Toast.LENGTH_LONG 的延时工夫为 3.5s。

咱们来看一下 Toast 的 show 办法;

public void show() {

    if (mNextView == null) {throw new RuntimeException("setView must have been called");
    }

    //2、INotificationManager service = getService();
    String pkg = mContext.getOpPackageName();
    TN tn = mTN;
    tn.mNextView = mNextView;

    try {
        
        //3、service.enqueueToast(pkg, tn, mDuration);
    } catch (RemoteException e) {// Empty}

}

正文 2 中拿到的 INotificationManager 对象,它具体的实现类是 NotificationManagerService 的 IBinder 类型的 mService 属性;正文 3 中将申请放在 NotificationManagerService 中 IBinder 类型的 mService 属性的 enqueueToast 办法,并传递一个实现了 ITransientNotification.Stub 类的 Toast 外部类 TN,TN 是一个 Binder 对象,能进行跨过程通信。

咱们看一下 NotificationManagerService 中 IBinder 类型的 mService 属性的 enqueueToast 办法;

public class NotificationManagerService extends SystemService {

......
private final IBinder mService = new INotificationManager.Stub() {
    // Toasts
    // ============================================================================

    @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) {......} else {
                    // Limit the number of toasts that any given package except the android
                    // package can enqueue.  Prevents DOS attacks and deals with leaks.
                    if (!isSystemToast) {
                        int count = 0;
                        final int N = mToastQueue.size();
                        for (int i = 0; i < N; i++) {final ToastRecord r = mToastQueue.get(i);
                            if (r.pkg.equals(pkg)) {
                                count++;

                                //4、if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                                    Slog.e(TAG, "Package has already posted" + count
                                            + "toasts. Not showing more. Package=" + pkg);
                                    return;
                                }
                            }
                        }
                    }

                    Binder token = new Binder();
                    mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);

                    //5、record = new ToastRecord(callingPid, pkg, callback, duration, token);

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

}

正文 4 示意判断以后的过程所弹出的 Toast 数量是否曾经超过下限 MAX_PACKAGE_NOTIFICATIONS,这里的 MAX_PACKAGE_NOTIFICATIONS 值是 50,如果超过,间接返回;正文 5 示意把 TN 封装在 ToastRecord 中;正文 6 示意把 ToastRecord 保留在 ArrayList 类型的 mToastQueue 属性中;正文 7 示意如果没有其余的 Toast 了,那么就显示以后的 Toast。

咱们来看 NotificationManagerService 的 showNextToastLocked 办法;

void showNextToastLocked() {

    ToastRecord record = mToastQueue.get(0);
    while (record != null) {if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + "callback=" + record.callback);
        try {
            //8、record.callback.show(record.token);
            
            //9、scheduleTimeoutLocked(record);
            return;
        } catch (RemoteException e) {......}
    }

}

正文 8 示意显示 Toast,record.callback 是 ITransientNotification 类型的对象,ITransientNotification.Stub 实现了 ITransientNotification,Toast 的外部类 TN 又继承了 ITransientNotification.Stub,所以咱们看 Toast 的外部类 TN 的 show 办法;

mHandler = new Handler(looper, null) {

    @Override
    public void handleMessage(Message msg) {switch (msg.what) {
            case SHOW: {IBinder token = (IBinder) msg.obj;
                handleShow(token);
                break;
            }
            ......
        }
    }
};

/**
 * schedule handleShow into the right thread
 */
@Override
public void show(IBinder windowToken) {if (localLOGV) Log.v(TAG, "SHOW:" + this);
    mHandler.obtainMessage(SHOW, windowToken).sendToTarget();}

咱们看到 TN 的 show 办法调用了 TN 的外部类 Handler,Handler 依据 msg.what 又调用了 TN 的 handleShow 办法,咱们看 handleShow 办法;

public void handleShow(IBinder windowToken) {

    ......
    if (mView != mNextView) {
        ......
        mParams.token = windowToken;
        ......
        try {mWM.addView(mView, mParams);
            trySendAccessibilityEvent();} catch (WindowManager.BadTokenException e) {/* ignore */}
    }

}

这个办法将所传递过去的窗口 token 赋值给 Toast 的窗口属性对象 mParams,而后通过调用 WindowManager.addView 办法,将 Toast 中的 mView 对象纳入 WMS 的治理,WindowManager.addView 办法最终实现 Toast 的显示。

咱们回看 NotificationManagerService 的 showNextToastLocked 办法中正文 9 的代码,也就是 NotificationManagerService 的 scheduleTimeoutLocked 办法;

@GuardedBy(“mToastQueue”)
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);

}

在该办法中的 Handler 的实现类是 NotificationManagerService 外部类 WorkerHandler,将 Message 的 what 置为 MESSAGE_TIMEOUT 就延时发送一条音讯,咱们且看 WorkerHandler 对应的解决;

private final class WorkerHandler extends Handler {

    ......
    @Override
    public void handleMessage(Message msg) {switch (msg.what) {
            case MESSAGE_TIMEOUT:
                handleTimeout((ToastRecord) msg.obj);
                break;
            ......
        }
    }

}

WorkerHandler 依据 msg.what == MESSAGE_TIMEOUT 又调用了 NotificationManagerService 的 handleTimeout 办法;

private void handleTimeout(ToastRecord record) {

    ......
    synchronized (mToastQueue) {int index = indexOfToastLocked(record.pkg, record.callback);
        if (index >= 0) {cancelToastLocked(index);
        }
    }

}

handleTimeout 办法通过搜寻后调用 NotificationManagerService 的 cancelToastLocked 办法勾销掉 Toast 的显示,往下看 cancelToastLocked 办法;

@GuardedBy("mToastQueue")
void cancelToastLocked(int index) {ToastRecord record = mToastQueue.get(index);
    try {
    
        //10、record.callback.hide();} catch (RemoteException e) {......}

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

咱们先看正文 11 中的代码,WindowManagerInternal 的 实现类是 WindowManagerService 的外部类 LocalService,咱们看 LocalService 的 removeWindowToken 办法;

@Override
public void removeWindowToken(IBinder binder, boolean removeWindows, int displayId) {synchronized(mWindowMap) {
        ......
        WindowManagerService.this.removeWindowToken(binder, displayId);
    }
 }

从 LocalService 的 removeWindowToken 办法能够看出,Toast 生成的窗口 Token 从 WMS 服务中删除。

咱们回看 NotificationManagerService 的 cancelToastLocked 办法中正文 10 的代码;后面说过 record.callback 是 TN 的具体对象,咱们看看 TN 的 hide 办法;

    @Override
    public void hide() {if (localLOGV) Log.v(TAG, "HIDE:" + this);
        mHandler.obtainMessage(HIDE).sendToTarget();}
    mHandler = new Handler(looper, null) {
    @Override
    public void handleMessage(Message msg) {switch (msg.what) {
            ......
            case HIDE: {handleHide();
                // Don't do this in handleHide() because it is also invoked by
                // handleShow()
                mNextView = null;
                break;
            }
            ......
        }
    }
};

TN 的 hide 办法发送一条音讯给 Handler,Handler 又调用了 TN 的 handleHide 办法;

public void handleHide() {

    ......
    if (mView != null) {
        ......
        if (mView.getParent() != null) {
            ......
            mWM.removeViewImmediate(mView);
        }

        mView = null;
    }

}

TN 的 handleHide 办法通过 WindowManagerService 删除了 Toast 的 View,胜利实现对 Toast 的暗藏。

这里咱们有一个疑难,如果在主线程中调用 Toast 的 show 办法,而后在延时 4s 会看到 Toast 提醒吗?

咱们写个 demo 验证一下;

(1)新建一个 kotlin 语言类型的 Activity,名叫 MainActivity:

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
}

fun onClick(v: View) {Toast.makeText(this@MainActivity, "被点击了", Toast.LENGTH_SHORT).show()
    SystemClock.sleep(4 * 1000)
}

}

(2)新建一个 MainActivity 对应的布局文件 activity_main.xml:

<?xml version=”1.0″ encoding=”utf-8″?>
<LinearLayout

xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.xe.postdemo.MainActivity">
<Button
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="Toast 的 show 办法被调用后,主线程加了个 4s 的延时"
    android:onClick="onClick"
    android:textAllCaps="false"/>

</LinearLayout>

程序运行后,界面如下所示(留神:我是在 Android API 29 的手机上运行的):

图片

点击“Toast 的 show 办法被调用后,主线程加了个 4s 的延时”按钮后,发现没有 Toast 提醒,是什么起因呢?是 Activity 超时无响应吗?不是这个起因,Activity 超时无响应是产生延时大于等于 5s 的条件下;是因为 Toast 的 show 办法是跨过程通信,Toast 的 show 办法发动零碎 NotificationManagerService 外部的 INotificationManager.Stub 申请后,App 这边的主线程就进行 4s 的延时;其中 INotificationManager.Stub 申请是属于零碎过程,App 这边的主线程属于非零碎过程,当零碎过程回调 TN 的 show 办法时,发现 App 这边的主线程进行延时,零碎过程回调 TN 的 show 办法就会处于音讯阻塞状态,等到 4s 延时完结后就会执行 TN 的 show 办法,然而 4s 延时完结之前因为 Toast.LENGTH_SHORT 那么长时间后,也就是 2s 后就进行了 NotificationManagerService 的 cancelToastLocked 办法调用,cancelToastLocked 办法执行了 WindowManagerService 对 窗口 token 的删除;当 4s 延时完结后,TN 最终会执行 handleShow 办法,然而这时候发现 handleShow 办法的 IBinder 类型的 windowToken 不存在了,所以就显示不了 Toast 的提醒内容了。

如果在 Android API 25 及其以下 API 版本的手机,有的手机会呈现报错,我先列出 API 25 中 TN 的 handleShow 办法;

public void handleShow(IBinder windowToken) {

    if (localLOGV) Log.v(TAG, "HANDLE SHOW:" + this + "mView=" + mView
            + "mNextView=" + mNextView);
    if (mView != mNextView) {
        ......
        //
        mWM.addView(mView, mParams);
        trySendAccessibilityEvent();}

}

是因为 mWM.addView(mView, mParams) 语句没有进行异样捕捉引起的。

退出移动版