Toast原理解析

谈到Toast,我想只要是做过Android开发的恐怕没有不知道的吧,毫无疑问,Toast是一个非常方便的组件,能在任何能获取上下文的地方进行弹窗提示,既然是弹窗那么我们就可以知道,Toast应该是Window的一部分了。

在这篇文章中,不对Window讲解,默认这部分内容是已知的。

我对Toast的理解最先来自 艺术探索 这本书,在了解了其基本原理之后,我觉得自己去摸索下源码来巩固自己的理解,于是有了这篇文章。本文在写作过程当作由于是边看源码,边写作,可能稍显混乱。我的基本思路如下:

Toast的调用过程如下:

Toast.makeText(this,"test",Toast.LENGTH_LONG).show()

上述代码可以看出,最先是调用了Toast的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;
    }

这就是Toast的makeText的实现,我们来分析一下。

首先,第一行代码,Toast result = new Toast(context),去调用Toast的构造方法来初始化了一个Toast,是怎么初始化的呢?深入进去看看:

 public Toast(Context context) {
        mContext = context;
        mTN = new TN();
        mTN.mY = context.getResources().getDimensionPixelSize(
                com.android.internal.R.dimen.toast_y_offset);
        mTN.mGravity = context.getResources().getInteger(
                com.android.internal.R.integer.config_toastDefaultGravity);
     }

从代码中我们可以看到,Toast的初始化需要上下文,这也是我们在调用Toast的时候,都必须获取上下文的原因。然后新建了一个mTN对象,mTN是一个binder对象,用于IPC,在这部分代码中,这个TN的初始化主要是进行了Window布局元素的创建,具体如下,逻辑还是比较简单的:

 TN() {
            // XXX This should be changed to use a Dialog, with a Theme.Toast
            // defined that sets up the layout params appropriately.
            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;
            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;
        }

接下来继续看,在创建出一个mTN之后,就是为mTN的两个成员进行赋值:mY,mGravity。
完成了Toast的初始化之后,跳出来看下面的代码:

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

这一部分的代码,我想不用我说了吧?不过还是说说
在这部分代码中是加载了一个布局,这个布局相当简单,就是一个TextView,然后找到布局中的TextView,然后将我们需要显示的Toast显示在这个地方。
完成了数据的绑定之后,接下来继续看:

        result.mNextView = v;
        result.mDuration = duration;

这两行代码,将之前我们所初始化的view,以及我们传入的需要延时的时长传入。
到此,makeText方法里的逻辑就分析完了,现在来总结以下,它干了什么事:
可以看出,正如方法名所显示的那样,其主要就是确定Toast需要显示的位置(使用window确定),然后确定Toast所需要显示的View,并且绑定到Toast中。到此为止,Toast创建完毕。不过并没有show。那么接下来我们来看看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
        }
    }

有了上面的基础,这部分理解上并不存在问题,来看看:
首先判断我们创建的view是否为null,为null直接抛出。
然後获取NotificationManager 的IPC接口,INotificationManager 。
获取到包名,再将我们之前所处理好的view传递给布局的binder对象mTN。
最后,用接口发起跨进程通信。

在服务端,我们来看看这个enqueueToast(pkg, tn, mDuration)的实现,此方法代码量较大,我们挑主要的看:

            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) {
                        record = mToastQueue.get(index);
                        record.update(duration);
                    } 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++;
                                     if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                                         Slog.e(TAG, "Package has already posted " + count
                                                + " toasts. Not showing more. Package=" + pkg);
                                         return;
                                     }
                                 }
                            }
                        }

                        record = new ToastRecord(callingPid, pkg, callback, duration);
                        mToastQueue.add(record);
                        index = mToastQueue.size() - 1;
                        keepProcessAliveLocked(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.
                    if (index == 0) {
                        showNextToastLocked();
                    }

在此方法中,首先涉及到同步过程,其所用的锁为一个ArrayList mToastQueue,从名字也可以看出来,它保存的东西是ToastRecord。从这里可以看出一个问题,为啥系统中,应用中如此多的Toast,却能一一显示,并不出现问题,它们是保存在一个队列中的,会一个一个地取出来显示。

接下来的代码,我们慢慢看:
首先,int index = indexOfToastLocked(pkg, callback)
进入此方法:

int indexOfToastLocked(String pkg, ITransientNotification callback)
    {
        IBinder cbak = callback.asBinder();
        ArrayList<ToastRecord> list = mToastQueue;
        int len = list.size();
        for (int i=0; i<len; i++) {
            ToastRecord r = list.get(i);
            if (r.pkg.equals(pkg) && r.callback.asBinder() == cbak) {
                return i;
            }
        }
        return -1;
    }

此方法中主要是遍历了mToastQueue将我们传入的pkg以及mTN与其中的ToastRecord对比,若是存在,那么直接返回这一条记录的index,若是不存在那么返回-1。
看接下来的方法:

 if (index >= 0) {
                        record = mToastQueue.get(index);
                        record.update(duration);
                    } 

之前说明了,在这个if中处理的就是能在mToastQueue中找到的情况:
首先获取到这条记录,然后更新,其中传入的参数是延时时间,这个函数没有做什么其他的操作,仅仅只是赋值了新的时间。现在来看看没有加入的情况,这部分是重点:

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++;
                                     if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                                         Slog.e(TAG, "Package has already posted " + count
                                                + " toasts. Not showing more. Package=" + pkg);
                                         return;
                                     }
                                 }
                            }
                        }

从代码中可以看出,此方法使用来限制Toast的数量的,详细看下:
首先若其不是系统应用,那么执行这个方法吗,先获取到enqueue的大小N,设定count计数器,然后遍历,若是遍历项的pkg和我们传入的pkg相同,那么count++。即是说,统计一个package中的Toast的数量,Android只允许最多50个Toast的数量,不过可以知道的是,基本不会有应用能用到那么多,哈哈。
接下来继续解析代码:

  record = new ToastRecord(callingPid, pkg, callback, duration);
                        mToastQueue.add(record);
                        index = mToastQueue.size() - 1;
                        keepProcessAliveLocked(callingPid);

接下来的代码,首先新创建了ToastRecord,然后将这条记录添加到mToastQueue,index是用来判断是否是当前Toast的。接下来:

 if (index == 0) {
                        showNextToastLocked();
                    }

若是当前Toast,那么就显示,看来此方法是真正显示的方法,深入进去看看:

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();
                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);
                }
                keepProcessAliveLocked(record.pid);
                if (mToastQueue.size() > 0) {
                    record = mToastQueue.get(0);
                } else {
                    record = null;
                }
            }
        }
    }

下面来分析一下这段代码:

因为是当前Toast,那么直接获取到第一个 ToastRecord即可。接下来调用record.callback.show(),看来我们离真相越来越近了,这里的callback就是TN对象,在这里,show()方法又是一次IPC,那么深入这个show()方法:

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

可以看到,这里就是实际进行显示的地方,Handler的东西就不再多说了,说说这个mShow,其是一个Runnable,实际切换线程之后,运行的是它:

final Runnable mShow = new Runnable() {
            @Override
            public void run() {
                handleShow();
            }
        };

唉,层层套啊,现在深入看看 handleShow()方法:

 public void handleShow() {
            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                    + " mNextView=" + mNextView);
            if (mView != mNextView) {
                // remove the old view if necessary
                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;
                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);
                mWM.addView(mView, mParams);
                trySendAccessibilityEvent();
            }
        }

代码量有点大,不过我们需要注意的核心是:

mWM.addView(mView, mParams)

到此,Toast终于在屏幕上显示出来!整个流程结束。关于Toast的cancel方法,分析方法和步骤都差不多,就不赘述了。

整个流程分析完,感觉Android系统真的是相当牛的,深入学习Android,学习大师们的思想是很棒的学习方法,我觉得对于我来说,分析Android这个具体的分析过程,其实是理解软件思想与计算机的知识,计算机相关知识分析到最后都是相通的,所以这部分的付出是值得的。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值