Toast的View界面是如何绘制出来的--Toast的Window(view)创建过程

前面我们已经讲述了Activity的Window创建过程、Dialog的Window创建过程, 本文将继续探索Window相关的知识:Toast的创建过程 及 其 View界面的展示。

#####代码示例

Toast的一般使用非常简单, 一行代码就可以搞定:

	Toast.makeText(this, "Toast测试", Toast.LENGTH_SHORT).show();

通过makeText创建一个Toast, 然后调用show方法,去真正的显示出来这个Toast, 这一点和前文的Dialog很相似。

我们去看看这个makeText源码:

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

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

代码也很简单, 就是 new 一个Toast, 然后填充一个默认视图,最后把这个新建的Toast对象返回。从这里也可以看出, 其实我们可以自己new一个Toast,然后填充我们的自定义布局, 就可以定义我们自己想要的Toast了。这里Toast为我们提供了setView(View v)方法实现我们的自定义布局。

#####Toast的Window创建过程

Toast和前文讲到的Dialog有所不同,Toast的工作过程要更复杂一些。Toast也是基于Window的, 这是毋庸置疑的,但是由于Toast有具有定时取消的特点,即我们通过设置Toast.LENGTH_SHORT或者Toast.LENGTH~LONG,Toast具有固定显示实现, 分别是2.5s和3.5s,显示完后就要消失, 所以这里系统采用了handler的延时机制去完成。

在Toast内部, 有两个不同的IPC过程, 第一个是Toast访问NotificationManagerService, 第二个是NotificationManagerService回调给Toast里的TN接口(这里的TN就是一个类,它的类名就是TN), 这里说是一个回调其实是为了方便我们理解, 它的本质其实是NotificationManagerService通过IPC来访问我们的Toast,同时把数据传过来。关于Binder通信机制可以自行搜索一下, 或者参考我的博客**Android的IPC机制–实现AIDL的最简单例子(上)(下)**, AIDL本质就是一个Binder通信。

除了这两个IPC过程, 最后Window的添加也是一个IPC过程, 所以Toast的IPC过程总共有三个,其中创建过程两个, 添加过程一个。

Toast是一个系统级Window(这个后面会说明), show和cancel两个方法用于显示和隐藏Toast。
刚刚看过了makeText()方法,里面很简单,接着我们看看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
    }
}

注意这一行代码:

INotificationManager service = getService();

这里就是去获取一个NotificationManagerService的本地代理 service对象, 然后通过service.enqueueToast(pkg, tn, mDuration) 向NotificationManagerService发送了消息:我要创建一个Toast啦。 这里就是我们刚刚所提到的第一个IPC过程,其中enqueueToast的三个参数别是包名,TN对象以及 toast的时长。 我们去看一下这个getService()方法:

static private INotificationManager getService() {
    if (sService != null) {
        return sService;
    }
    sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
    return sService;
}

如果了解AIDL的通信, 是不是觉得这里很眼熟, 这其实就是一个AIDL啊,还不了解AIDL的同学,推荐看完本文后,再移步看一下我的另两篇博客**Android的IPC机制–实现AIDL的最简单例子(上)(下)**。跨进程通信不是本篇的重点, 这里只是提一下。

回到刚刚的位置, 我们通过service.enqueueToast()方法向NotificationManagerService发送消息后, 将会执行NotificationManagerService的enqueueToas方法, 继续进去查看源码:

private final IBinder mService = new INotificationManager.Stub() {
   
    @Override
    public void enqueueToast(String pkg, ITransientNotification callback, int duration)
    {
       ...
        synchronized (mToastQueue) {
           ...
            try {
                ToastRecord record;
                int index = indexOfToastLocked(pkg, callback);
				// 1. 如果这个toast已经存在于queue了,则只是更新它,但是并不会把它移动到队尾
                if (index >= 0) {
                    record = mToastQueue.get(index);
                    record.update(duration);
                } else {
                    if (!isSystemToast) {
                        int count = 0;
                        final int N = mToastQueue.size();
                        for (int i=0; i<N; i++) {
                             final ToastRecord r = mToastQueue.get(i);
							// 2. 判断如果是同一个包, 则最多只能存在50个toast,
							//否则不再允许添加进队列
                             if (r.pkg.equals(pkg)) {
                                 count++;
                                 if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                                     return;
                                 }
                             }
                        }
                    }
                    record = new ToastRecord(callingPid, pkg, callback, duration);
					//把传过来的toast加入进队列, 等待显示
                    mToastQueue.add(record);
                   ...
                }
                // 3. 如果是第一个toast, 则直接显示
                if (index == 0) {
                    showNextToastLocked();
                }
	...

上面这一段源码,主要有三个重要代码:

    1. 判断这个Toast是否已存在,如果这个toast已经存在于queue了,则只是更新它,但是并不会把它移动到队列的队尾, 这里的mToastQueue其实是一个ArrayList;
    1. 判断如果是同一个包, 则最多只能存在50个toast,否则不再允许添加进队列, 这一点非常重要,试想一下, 如果我们通过循环去大量弹出toast, 那么这个toast队列里面的toast就会无穷无尽, 这个时候去打开其他APP, 它们都没机会弹出toast了。正常情况下, 一个应用的toast也打不到50个,完全够用了;
    1. 如果是第一个toast, 则直接调用showNextToastLocked方法弹出toast。

要注意这个enqueueToast的三个参数, 我们传递过来的分别是包名、 TN对象、 显示时间, 这里把TN对象赋值给了callback, TN实际上实现了ITransientNotification接口的。

#####Toast的Window显示过程

接着,我们去看看showNextToastLocked是如何显示toast的:

void showNextToastLocked() {
    ToastRecord record = mToastQueue.get(0);
    while (record != null) {
       
        try {
			//关键代码 1:回调show方法
            record.callback.show();
			// 关键代码2:执行toast的超时后的移除处理
            scheduleTimeoutLocked(record);
            return;
        } catch (RemoteException e) {
			//这里处理由于某些意外导致 跨进程通信失败,即这个调用Toast的显示失败
			//这个时候就从队列把这个Toast移除掉, 执行下一个Toast的显示
            int index = mToastQueue.indexOf(record);
            if (index >= 0) {
                mToastQueue.remove(index);
            }
            keepProcessAliveLocked(record.pid);
            if (mToastQueue.size() > 0) {
                record = mToastQueue.get(0);
            } else {
                record = null;
            }
        }
    }
}

这里的逻辑一样很简单, 主要看try代码块就行了, 二tyr代码块就两行代码,都是关键代码:

  • 关键代码1: 回调是record.callback.show(): 这个callback对象其实就是我们的TN对象, 所以这里就回调给我们的toast中的TN了, 这里就是第二次跨进程通信;
  • 关键代码2 调用scheduleTimeoutLocked:从这个方法名上也可看出一二, 这是用来处理超时的方法, 主要是当显示超过我们的设定的Toast的显示时间后(即SHORT或者LONG), 就移除掉这个toast,然后继续调用下一个Toast。具体细节感兴趣的可以去跟一下

我们继续按照主路径走, 去看我们的关键代码, 这里回调给了TN的show方法, TN是Toast的一个内部类:

 private static class TN extends ITransientNotification.Stub {
    final Runnable mShow = new Runnable() {
        @Override
        public void run() {
            handleShow();
        }
    };

	...

    @Override
    public void show() {
        if (localLOGV) Log.v(TAG, "SHOW: " + this);
        mHandler.post(mShow);
    }

	...
}

我们看到 TN extends ITransientNotification.Stub, 了解AIDL的同学一看就知道这是一个跨进程通信的类, 它继承了ITransientNotification.Stub, 也就实现了ITransientNotification接口。 我们继续跟进show方法, 里面调用了 mHandler.post(mShow), 具体的执行在mShow里面,接着看, mShow里面又调用了handleShow(), 看一下这个方法:

public void handleShow() {
      
        if (mView != mNextView) {
            // 移除掉前一个Toast的view 
            handleHide();
			//把我们的Toast的View传进来
            mView = mNextView;
           ...
			//关键代码, 获取WindowManagerb本地代理对象
            mWM = (WindowManager)context.getSystemService
           ...
            //关键代码, 向WindowManager添加Window, 把我们的view传过去
            mWM.addView(mView, mParams);
        }
    }

这个方法逻辑也比较清楚, 就是把上一个toast的view已移除掉,然后把要显示的Toast添加进入WindowManger, 这同样是一个IPC过程, Window的添加过程可以参考**从Window的添加过程理解Window和WindowManager**, 文中配了一张图片, 很清晰的展示了Window的添加过程。 这就是我们前面提到的到的Toast的 第三次IPC过程

然后, Toast就真正的显示在我们的界面上啦~~

到这里, 整个Toast的view添加, Window添加,以及Toast的超时后自动消失的 整个流程就讲完了~~

#####Toast的一些不为人知的细节

######1、为什么我们通常只能在UI主线程使用Toast?

上面一段代码中, 我们看到,当NotificationManagerService回调给TN, 通知它可以显示Toast的时候, 回调给了TN的show方法, 我们再次看一些这个show方法:

	@Override
    public void show() {
        if (localLOGV) Log.v(TAG, "SHOW: " + this);
        mHandler.post(mShow);
    }

注意到没有, 这里使用mHandler, 去提交的一个任务, 看一下这个mHandler的定义:

final Handler mHandler = new Handler();   

就是一个普通的Handler, 而我们知道, 只有在主线程, 才能直接这么使用handler, 否则都会报错,因为主线程在初始化时, 就已经调用了Looper.prepare()方法。如果需要在子线程使用Handler的话, 必须调用Looper.prepare()方法。所以, 一般情况下, 我们在主线程使用Toast, 但是如果想要在子线程使用Toast, 也是可以的, 就在子线程的run方法最后一行, 调用Looper.prepare()即可。为什么是在最后一行调用呢, 因为这个Looper.prepare()内部是一个死循环, 在这个方法后面的代码就无法执行了。

######2、为什么说Toast是一个系统级的Window?

Window分为系统级Window, 应用级Window, 子Window, 其中子Window必须附属在其他两类Window上,这个在**从Window的添加过程理解Window和WindowManager** 一文中有所介绍, 那为什么说Toast是一个系统级Window呢, 我们只需要去看看它的层级即可。

我们再去看看Toast的Window的添加过程:

 //关键代码, 向WindowManager添加Window, 把我们的view传过去
 mWM.addView(mView, mParams);

层级是在WindowManager.LayoutParams.type中设置的, 我们从这里的mParams去看看, 找一下这个mParams在哪里赋值了, 找了一圈, 除了刚刚的handleShow方法, mparams在TN的构造方法里也有一些初始化:

TN() {
     
        final WindowManager.LayoutParams params = mParams;
        params.height = WindowManager.LayoutParams.WRAP_CONTENT;
        params.width = WindowManager.LayoutParams.WRAP_CONTENT;
        params.format = PixelFormat.TRANSLUCENT;
        params.windowAnimations = com.android.internal.R.style.Animation_Toast;
		//关键代码,给Window层级赋值
        params.type = WindowManager.LayoutParams.TYPE_TOAST;
        params.setTitle("Toast");
        params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
    }

我们看到, type的值为WindowManager.LayoutParams.TYPE_TOAST, 去看看这个值:

    public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;

可以看到它的值是FIRST_SYSTEM_WINDOW+5, 这个FIRST_SYSTEM_WINDOW表示系统Window的第一个层级, 比它大的都是系统Window,FIRST_SYSTEM_WINDOW的值为2000。系统Window的层级为 2000~2999.

到这里,Toast的Window的创建到消失的整个流程就讲完了。

喜欢的朋友麻烦点一个赞吧~~

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值