Android学习笔记14-从源码分析Toast的创建过程

Toast.show()

显示一个Toast只需要调用它的show()方法,看一下源码

/**
109     * Show the view for the specified duration.
110     */
111    public void show() {
112        if (mNextView == null) {
113            throw new RuntimeException("setView must have been called");
114        }
115
116        INotificationManager service = getService(); //获得NotificationManagerService
117        String pkg = mContext.getOpPackageName(); // 包名
118        TN tn = mTN;
119        tn.mNextView = mNextView;
120
121        try {
122            service.enqueueToast(pkg, tn, mDuration); // 调用NMS
123        } catch (RemoteException e) {
124            // Empty
125        }
126    }
复制代码

可以看到显示由 getService()获得了 NMS,NMS主要是Android系统用来管理 通知服务的,而且Toast也属于系统通知的一种。 NMS调用了enqueueToast(pkg,tn,mDuration),这三个参数分别是 :

  • pkg : 应用包名
  • tn : 是Toast的一个静态内部类Tn,用于被回调,内部含有两个主要方法用来显示,隐藏Toast , 并且这两个方法是等着被回调,不会主动调用
private static class TN extends ITransientNotification.Stub {
   @Override
       public void hide() {
           if (localLOGV) Log.v(TAG, "HIDE: " + this);
           mHandler.obtainMessage(HIDE).sendToTarget();
       }

       public void cancel() {
           if (localLOGV) Log.v(TAG, "CANCEL: " + this);
           mHandler.obtainMessage(CANCEL).sendToTarget();
       }
}
复制代码

我们发现,在show()hide()方法中,都是调用了Handler来处理,这是因为 NMS是运行在系统的进程中,Toast和NMS之间是一个IPC过程,NMS只能通过远程调用的方式来显示和隐藏Toast, 而 TN这个类是一个Binder类,它里面的show()'hide()'方法会在Toast和NMS进行IPC时回调。
这时,TN是运行在Binder的线程池中的,而我们的Toast需要在当前UI线程中显示,所以需要通过Handler,配合着Looper来完成切换线程

  • mDuration : 这个就是我们创建Toast时传入的 显示时长。

接下来我们会一层一层的深入分析,贴一张图来记录进度:


INotificationManager.enqueueToast(pkg,tn,mDurtion)

接下来分析enqueueToast(pkg,tn,mDuration)里面是做了什么事情呢?我们继续点开看看

NotificationManagerService.java #enqueueToast()

1087            synchronized (mToastQueue) { 
1089                ...
1090                try {
						//将Toast请求封装为ToastRecord 见 1117行
1091                    ToastRecord record;
1092                    int index = indexOfToastLocked(pkg, callback);
1093                    //如果Toast已经在列表中,则更新它的信息
1095                    if (index >= 0) {
1096                        record = mToastQueue.get(index);
1097                        record.update(duration);
1098                    } else {
1099                        // 限制Toast的个数, MAX_PACKAGE_NOTIFICATIONS = 50
1101                        if (!isSystemToast) {
1102                            int count = 0;
1103                            final int N = mToastQueue.size();
1104                            for (int i=0; i<N; i++) {
1105                                 final ToastRecord r = mToastQueue.get(i);
1106                                 if (r.pkg.equals(pkg)) {
1107                                     count++;
1108                                     if (count >= MAX_PACKAGE_NOTIFICATIONS) {
1109                                         Slog.e(TAG, "Package has already posted " + count
1110                                                + " toasts. Not showing more. Package=" + pkg);
1111                                         return;
1112                                     }
1113                                 }
1114                            }
1115                        }
1116
1117                        record = new ToastRecord(callingPid, pkg, callback, duration);
1118                        mToastQueue.add(record);
							...
1121                    }
1122                    // 如果index==0,代表就是当前的Toast
1126                    if (index == 0) {
1127                        showNextToastLocked();
1128                    }
1129                } finally {
1130                    Binder.restoreCallingIdentity(callingId);
1131                }
复制代码

我只截取了重要的一部分代码,有点长,我们来慢慢看:

  1. enqueueToast()方法首先把Toast的请求封装到ToastRecord中。
record = new ToastRecord(callingPid, pkg, callback, duration);
复制代码
  1. ToastRecord添加到一个存储到到 mToastQueue中,这是一个ArrayList的存储结构,对于非系统应用,最多能存下50个Toast,
mToastQueue.add(record);
复制代码
1106 if (r.pkg.equals(pkg)) {
1107    count++;
1108    if (count >= MAX_PACKAGE_NOTIFICATIONS) {  //MAX_PACKAGE_NOTIFICATIONS = 50
1109        Slog.e(TAG, "Package has already posted " + count
1110         + " toasts. Not showing more. Package=" + pkg);
1111        return;
1112        }
1113}
复制代码
  • 接下来NMS通过showNestToastLocked()来显示当前的Toast ,index = 0,就代表队列中只剩下一个Toast,就是当前的Toast
1119       index = mToastQueue.size() - 1;
		   ...
1126       if (index == 0) {
1127          showNextToastLocked();
1128       }
复制代码

enqueueToast()分析完了,记录一下


INotificationManager.showNextToastLocked()

先贴上源码

    void showNextToastLocked() {
        ToastRecord record = mToastQueue.get(0);
        while (record != null) {
            ...
            try {
                record.callback.show(record.token); //回调 callback中的show方法
                scheduleTimeoutLocked(record); //发送延时消息,取决于Toast的时长
                return;
            } catch (RemoteException e) {
               ...//省略部分代码
            }
        }
    }
复制代码

这里 record.callback就是 我们前面提到的TN,在这里回调它的show()方法Toast,并通过scheduleTimeoutLocked(record)根据指定的Toast显示时长发送一个延时消息。

当前记录:

下面来看一下延时消息是如何实现的


scheduleTimeoutLocked(record)

    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);
    }
复制代码

在上面代码中,LONG_DELAYSHORT_DELAY分别是 3.5s和 2s. 在经过这么长的延时后,发送message
来看一下对应此Message的处理:

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

好,接下来又要进 handleTimeout()方法中看一下,码住:


handleTimeout(ToastRecord record)

private void handleTimeout(ToastRecord record)
    {
     synchronized (mToastQueue) {
            int index = indexOfToastLocked(record.pkg, record.callback);
            if (index >= 0) {
                cancelToastLocked(index);
            }
        }
    }
复制代码

在经过一定的延时时间后,就该去除当前这个Toast了,跟 index 判断 当前这个Toast是否还在队列中,如果还在,NMS就会通过cancelToastLocked()方法来隐藏Toast,并将其从队列中移除。 如果队列中还有其他的Toast,继续调用showNextToastLocked();将其显示.

cancelToastLocked(int index)

void cancelToastLocked(int index) {
        ToastRecord record = mToastQueue.get(index);
        try {
            record.callback.hide();
        } catch (RemoteException e) {
            ...
        }

        ToastRecord lastToast = mToastQueue.remove(index); //从队列中移除
        mWindowManagerInternal.removeWindowToken(lastToast.token, true, DEFAULT_DISPLAY); //移除window

        keepProcessAliveIfNeededLocked(record.pid);
        if (mToastQueue.size() > 0) { //如果还有其他的Toast,继续显示
            // 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();
        }
    }
复制代码

到这里,一个Toast的显示到隐藏就结束了。刚刚我们说过,Toast的显示和隐藏都是回调 TN中的方法 :

现在来看一下TN中具体显示Toast的方法 : 可以看一下注释

public void handleShow(IBinder windowToken) {
            ...
            //如果此时handler又发送 隐藏 或者 取消的消息,则返回,也就是不显示了。
            if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
                return;
            }
            if (mView != mNextView) {
                // 如果正在显示的Toast不是当前的Toast.(是之前显示的还没隐藏掉),那就隐藏它
                handleHide();
                mView = mNextView;
                Context context = mView.getContext().getApplicationContext();
                String packageName = mView.getContext().getOpPackageName();
                if (context == null) {
                    context = mView.getContext();
                }
                //获得WindowMangaer
                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
                ... //省略了显示Toast的一些布局参数的设置代码
                try {
                    mWM.addView(mView, mParams); //将Toast添加到Window中
                    trySendAccessibilityEvent();
                } catch (WindowManager.BadTokenException e) {
                   ...
                }
            }
        }
复制代码

handleShow()主要做的就是将Toast添加到Window中。相反,handleHide()会把Toast的View从Window中移除:

public void handleHide() { 
            if (mView != null) {
                
                if (mView.getParent() != null) {
                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeViewImmediate(mView);
                }

                mView = null;
            }
        }
复制代码

mView.getParent()用来判断此View是否已经被添加到Window,如果!= null,说明 有Window包含这个Toast的View,那就移除它。

Toast的流程图:


ps: 这是我第一次写关于源码分析的博客,还有很多可能写的不清楚的地方,大家可以指出来互相学习O(∩_∩)O。我觉得通过看源码能够让你对系统的理解层次清晰,不会及停留在表面。看源码的时候注意不要被各个类之间的调用关系搞混,可以随手画出来记录一下。。。

Reference: 《Android艺术开发探索》-

(完~)

转载于:https://juejin.im/post/5cd8599f6fb9a031fe3be4fc

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值