Android Toast显示详解

首先,所有 Android 进程的视图显示都需要依赖于一个窗口。而这个窗口对象,被记录在了我们的 WindowManagerService(后面简称 WMS) 核心服务中。WMS 是专门用来管理应用窗口的核心服务。当 Android 进程需要构建一个窗口的时候,必须指定这个窗口的类型。 Toast 的显示也同样要依赖于一个窗口, 而它被指定的类型是:

public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;//系统窗口

可以看出, Toast 是一个系统窗口,这就保证了 Toast 可以在 Activity 所在的窗口之上显示,并可以在其他的应用上层显示。那么,这就有一个疑问:

“如果是系统窗口,那么,普通的应用进程为什么会有权限去生成这么一个窗口呢?”

我们先来看下 Toast 从显示到隐藏的整个流程:

// code Toast.java
public void show() {
        if (mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }
 
        INotificationManager service = getService();//调用系统的notification服务
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;//本地binder
        tn.mNextView = mNextView;
        try {
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
            // Empty
        }
    }

我们通过代码可以看出,当 Toastshow 的时候,将这个请求放在 NotificationManager 所管理的队列中,并且为了保证 NotificationManager 能跟进程交互, 会传递一个 TN 类型的 Binder 对象给 NotificationManager 系统服务。而在 NotificationManager 系统服务中:

//code NotificationManagerService
public void enqueueToast(...) {
    ....
    synchronized (mToastQueue) {
                    ...
                    {
                        // 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++;
                                     if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                                         //上限判断
                                         return;
                                     }
                                 }
                            }
                        }
 
                        Binder token = new Binder();
                        mWindowManagerInternal.addWindowToken(token,
                                WindowManager.LayoutParams.TYPE_TOAST);//生成一个Toast窗口
                        record = new ToastRecord(callingPid, pkg, callback, duration, token);
                        mToastQueue.add(record);
                        index = mToastQueue.size() - 1;
                        keepProcessAliveIfNeededLocked(callingPid);
                    }
                    ....
                     if (index == 0) {
                        showNextToastLocked();//如果当前没有toast,显示当前toast
                    }
                } finally {
                    Binder.restoreCallingIdentity(callingId);
                }
            }
}

们会得到以下的流程(在 NotificationManager系统服务所在的进程中):

  • 判断当前的进程所弹出的 Toast 数量是否已经超过上限 MAX_PACKAGE_NOTIFICATIONS ,如果超过,直接返回
  • 生成一个 TOAST 类型的系统窗口,并且添加到 WMS 管理
  • 将该 Toast 请求记录成为一个 ToastRecord 对象

代码到这里,我们已经看出 Toast 是如何偷天换日的。实际上,这个所需要的这个系统窗口 token ,是由我们的 NotificationManager 系统服务所生成,由于系统服务具有高权限,当然不会有权限问题。不过,我们又会有第二个问题:

既然已经生成了这个窗口的 Token 对象,又是如何传递给 Android进程并通知进程显示界面的呢?

我们知道, Toast 不仅有窗口,也有时序。有了时序,我们就可以让 Toast 按照我们调用的次序显示出来。而这个时序的控制,自然而然也是落在我们的NotificationManager 服务身上。我们通过上面的代码可以看出,当系统并没有 Toast 的时候,将通过调用 showNextToastLocked(); 函数来显示下一个Toast

void showNextToastLocked() {
        ToastRecord record = mToastQueue.get(0);
        while (record != null) {
            ...
            try {
                record.callback.show(record.token);//通知进程显示
                scheduleTimeoutLocked(record);//超时监听消息
                return;
            } catch (RemoteException e) {
                ...
            }
        }
    }

这里,showNextToastLocked 函数将调用 ToastRecord的 callback 成员的 show 方法通知进程显示,那么 callback 是什么呢?

final ITransientNotification callback;//TN的Binder代理对象

我们看到 callback 的声明,可以知道它是一个 ITransientNotification 类型的对象,而这个对象实际上就是我们刚才所说的 TN 类型对象的代理对象:

private static class TN extends ITransientNotification.Stub {    ...}

那么 callback对象的show方法中需要传递的参数 record.token呢?实际上就是我们刚才所说的NotificationManager服务所生成的窗口的 token。 相信大家已经对 AndroidBinder 机制已经熟门熟路了,当我们调用 TN 代理对象的 show 方法的时候,相当于 RPC 调用了 TNshow 方法。来看下 TN 的代码:

// code TN.java
final Handler mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                IBinder token = (IBinder) msg.obj;
                handleShow(token);//处理界面显示
            }
        };
@Override
        public void show(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.obtainMessage(0, windowToken).sendToTarget();
        }

这时候 TN 收到了 show 方法通知,将通过 mHandler 对象去 post 出一条命令为 0 的消息。实际上,就是一条显示窗口的消息。最终,将会调用handleShow(Binder) 方法:

public void handleShow(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                    + " mNextView=" + mNextView);
            if (mView != mNextView) {
                ...
                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
                ....
                mParams.token = windowToken;
                ...
                mWM.addView(mView, mParams);
                ...
            }
        }

而这个显示窗口的方法非常简单,就是将所传递过来的窗口 token 赋值给窗口属性对象 mParams, 然后通过调用 WindowManager.addView 方法,将 Toast 中的mView 对象纳入 WMS 的管理。

上面我们解释了 NotificationManager 服务是如何将窗口 token 传递给 Android 进程,并且 Android 进程是如何显示的。我们刚才也说到,NotificationManager 不仅掌管着 Toast 的生成,也管理着 Toast 的时序控制。

因此,我们需要穿梭一下时空,回到 NotificationManagershowNextToastLocked() 方法。大家可以看到:在调用 callback.show 方法之后又调用了个 scheduleTimeoutLocked 方法:



 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

薛文旺

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值