Android源码解析——Toast

简介

Toast是一种向用户快速展示少量信息的视图。当它显示时,它会浮在整个应用层的上面,并且不会获取到焦点。它的设计思想是能够向用户展示些信息,但又能尽量不显得唐突。本篇我们来研读一下Toast的源码,并探明它的显示及隐藏机制。

源码解析

Toast

我们从Toast的最简单调用开始,它的调用代码是:

Toast.makeText(context,"Show toast",Toast.LENGTH_LONG).show();

在上面的代码中,我们是先调用Toast的静态方法来创建一个Toast,然后调用其show()方法将其显示出来。其中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,并设置要显示的View及时间。这里默认使用的Toast布局transient_notification.xml也在SDK中,代码如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:background="?android:attr/toastFrameBackground">

    <TextView  android:id="@android:id/message" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:layout_gravity="center_horizontal" android:textAppearance="@style/TextAppearance.Toast" android:textColor="@color/bright_foreground_dark" android:shadowColor="#BB000000" android:shadowRadius="2.75" />

</LinearLayout>

它只是一个简单的LinearLayout套一个TextView。顺便说一句,从布局上我们可以知道这个Toast的背景颜色是可以配置的,通过在theme中配置toastFrameBackground属性。不过遗憾的是,我后来又查了一下,发现这个属性被导出到public.xml当中,也就是我们还是不能在应用当中配置。

回到Toast,我们来看一下它的属性定义及构造方法。

public class Toast {
    final Context mContext;
    final TN mTN;
    int mDuration;
    View mNextView;

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

它的属性很简单,只有四个,分别是上下文对象mContextTN对象mTN,表示显示时长的mDuration,以及表示将要显示的视图mNextView。在它的构造方法中,初始化mTN及它的两个成员变量。
留意一下Toast中显示的View的命名为mNextView

我们再往下翻一下Toast的一些成员方法,会发现它的许多行为的实现,都是在操作mTN,比如设置gravity,水平外边距,x轴或y轴的偏移等等。而它的show()cancel()方法,也都是通过其来实现,如下:

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

    public void cancel() {
        mTN.hide();

        try {
            getService().cancelToast(mContext.getPackageName(), mTN);
        } catch (RemoteException e) {
            // Empty
        }
    }

那么TN究竟是个什么东西?

TN

我们来看一下TN的源码,可以知道,它是Toast里的一个静态内部类,继承自ITransientNotification.Stub,并实现其显示和隐藏的方法:

    private static class TN extends ITransientNotification.Stub {
        // 显示命令
        final Runnable mShow = new Runnable() {
            @Override
            public void run() {
                handleShow();
            }
        };
        //隐藏命令
        final Runnable mHide = new Runnable() {
            @Override
            public void run() {
                handleHide();
                // Don't do this in handleHide() because it is also invoked by handleShow()
                mNextView = null;
            }
        };

        private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();//悬浮窗口的参数
        final Handler mHandler = new Handler();    //用于将操作命令添加到线程的队列中

        //显示的View的相关布局参数
        int mGravity;
        int mX, mY;
        float mHorizontalMargin;
        float mVerticalMargin;


        View mView;    //当前显示的View
        View mNextView;// 下一个要显示的View

        WindowManager mWM;//用于显示悬浮窗口

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

        /** * schedule handleShow into the right thread */
        @Override
        public void show() {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.post(mShow);//将显示命令添加到线程队列当中
        }

        /** * schedule handleHide into the right thread */
        @Override
        public void hide() {
            if (localLOGV) Log.v(TAG, "HIDE: " + this);
            mHandler.post(mHide);//将隐藏命令添加到线程队列当中
        }
        // ...其他代码
    }

从上面的代码中,我们可以看到,一个Toast的显示和取消,并不是在Toast类本身中实现的,而是交给了TN,由它去实现。TN则是包装了一个Toast的内容及行为。TN的显示及隐藏的具体实现是handleShow()handleHide(),这两个方法的内容是向WindowManager添加和隐藏View,和我们平时写悬浮窗口的实现一样,这里略过不谈。
我们还可以看到,TN继承的ITransientNotification.Stub,它是AIDL接口的实现,该接口为ITransientNotification.aidl。AIDL是Android中用于进程间通信的一种机制。也就是对于我们的Toast,最终是交给另一个进程去管理它的显示及隐藏。
我们回到刚才读Toast时的show()cancel()的代码,可以看到先是获取一个服务INotificationManager service = getService();,显示时调用其service.enqueueToast(pkg, tn, mDuration);,隐藏时调用mTNhide()方法,并接着调用getService().cancelToast(mContext.getPackageName(), mTN);,其中,mTN是它的进程通信的回调

INotificationManager

INotificationManager也是一个AIDL接口,它定义一些通知管理服务的行为,位于Android源码中的frameworks\base\core\java\android\app,并没有包含在Android SDK中。代码如下:

interface INotificationManager
{
    /** @deprecated use {@link #enqueueNotificationWithTag} instead */
    void enqueueNotification(String pkg, int id, in Notification notification, inout int[] idReceived);
    /** @deprecated use {@link #cancelNotificationWithTag} instead */
    void cancelNotification(String pkg, int id);
    void cancelAllNotifications(String pkg);

    void enqueueToast(String pkg, ITransientNotification callback, int duration);
    void cancelToast(String pkg, ITransientNotification callback);
    void enqueueNotificationWithTag(String pkg, String tag, int id, in Notification notification, inout int[] idReceived);
    void cancelNotificationWithTag(String pkg, String tag, int id);
}

可以看到它管理的内容包含Toast提示及Notification通知,下面我们只关注Toast相关的两个接口。根据它的名字,我们找到这个接口的对应实现sources\android-23\android\app\NotificationManager.java

NotificationManagerService

在该类的代码中,有INotificationManager.Stub的匿名内部类实例,主要代码如下:

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

        @Override
        public void enqueueToast(String pkg, ITransientNotification callback, int duration)
        {
            if (DBG) {
                Slog.i(TAG, "enqueueToast pkg=" + pkg + " callback=" + callback
                        + " duration=" + duration);
            }

            if (pkg == null || callback == null) {
                Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);
                return ;
            }

            final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));

            if (ENABLE_BLOCKED_TOASTS && !noteNotificationOp(pkg, Binder.getCallingUid())) {
                if (!isSystemToast) {
                    Slog.e(TAG, "Suppressing toast from package " + pkg + " by user request.");
                    return;
                }
            }
            //对Toast队列加锁
            synchronized (mToastQueue) {
                int callingPid = Binder.getCallingPid();//获取调用进程id
                long callingId = Binder.clearCallingIdentity();//重置当前线程上进来的IPC的ID
                try {
                    ToastRecord record;
                    int index = indexOfToastLocked(pkg, callback);//判断我们的mTN是否存在队列中
                    // 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.
                        //如果不是系统Toast,则限制toast的数量,以避免DOS攻击及内存泄露。
                        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;
                                     }
                                 }
                            }
                        }

                        // 创建一个ToastRecord对象,并加入队列。
                        record = new ToastRecord(callingPid, pkg, callback, duration);
                        mToastQueue.add(record);
                        index = mToastQueue.size() - 1;
                        keepProcessAliveLocked(callingPid);//设置调用者的进程的活动状态。它会根据该进程的Toast的数量来设置是否为前台进程。
                    }
                    // 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.
                    //如果最后一个下标为0,则表示它是当前的toast,那么将不会管它是新创建的还是刚刚被更新过,
                    //而是回调告诉mTN把自己显示出来。
                    if (index == 0) {
                        showNextToastLocked();
                    }
                } finally {
                    Binder.restoreCallingIdentity(callingId);
                }
            }
        }

        @Override
        public void cancelToast(String pkg, ITransientNotification callback) {
            Slog.i(TAG, "cancelToast pkg=" + pkg + " callback=" + callback);

            if (pkg == null || callback == null) {
                Slog.e(TAG, "Not cancelling notification. pkg=" + pkg + " callback=" + callback);
                return ;
            }

            synchronized (mToastQueue) {
                long callingId = Binder.clearCallingIdentity();
                try {
                    //获取Toast的在队列中的下标
                    int index = indexOfToastLocked(pkg, callback);
                    if (index >= 0) {//如果存在
                        //从队列中移除,该方法还会设置调用进程的活动状态,并显示下一条要显示的Toast
                        cancelToastLocked(index);
                    } else {
                        Slog.w(TAG, "Toast already cancelled. pkg=" + pkg
                                + " callback=" + callback);
                    }
                } finally {
                    Binder.restoreCallingIdentity(callingId);
                }
            }
        }
        //其他接口的实现代码略
    }

其中显示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;
                }
            }
        }
    }
````
可以看到它会调用`TN`对象的`show()`方法,并接着调用`scheduleTimeoutLocked(record);`。
该方法代码如下:




<div class="se-preview-section-delimiter"></div>

```java
    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);
    }

也就是在显示之后,会在delay毫秒之后发送一个MESSAGE_TIMEOUT的消息来通知Toast隐藏,来让一个Toast只显示一小段时间。delay的时间根据Toast设置的duration是否为Toast.LENGTH_LONG来决定是LONG_DELAY(3.5秒)还是SHORT_DELAY(2秒)。
NotificationManagerService的实现代码很长,为避免因贴上太多代码而使篇幅变得太长,这里只介绍了主要流程的代码,具体每个步骤的方法实现,可以自己翻阅该类代码,文件位于SDK的sources\android-23\com\android\server\notification文件夹。

总结

对于Toast代码的分析在此告一段落,从上面的分析中我们可以得到以下结论:

  • 默认的Toast布局的背景可以在theme中配置。
  • Toast的显示及取消是通过NotificationManagerService来管理的,它跨进程,使用AIDL来实现进程间通信。
  • 所有Toast都会加到NotificationManagerService的队列中,对于非系统程序,它会限制Toast的数量(当前我所读的代码中该值为50)以防止DOS攻击及内存泄露的问题。
  • Toast里的TN对象的显示及隐藏命令通过new出来的handler来发送。所以没有队列的线程是不能显示Toast的。
  • Toast的显示的时间只有两个,duration相当于一个标志位,用于标志显示的时间是长还是短,而不是具体的显示时间。
  • 当有Toast要显示时,其所在进程会被设为前台进程。
    原文作者:貌似掉线
    原文地址: https://blog.csdn.net/maosidiaoxian/article/details/51327424
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞