Toast源代码分析

Toast源代码分析

日常开发中经常使用到Toast开发,用于显示一个提示用户的弹出消息,简单易用。

使用例程

Toast.makeText(this, "Hello world", Toast.LENGTH_SHORT).show();

这样一句代码即可实现通用的,系统默认样式的Toast。当然也可以自定义。关于如何自定义在分析完Toast的代码后,即可知道。

代码分析

  • makeText 创建Toast

    public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
        Toast result = new Toast(context);  //使用Toast的默认构造函数构造一个Toast 代码如下
    
        LayoutInflater inflate = (LayoutInflater)  //获取LayoutInflater
                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);  //inflate默认的样式
        TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);  //设置消息
        tv.setText(text);
    
        result.mNextView = v;
        result.mDuration = duration;
    
        return result;
    }
    
  • show 显示Toast

    public void show() {
        if (mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }
    
        INotificationManager service = getService();  //获取系统的INotificationManager 其实就是NotificationManagerService
        String pkg = mContext.getOpPackageName();  //获取包名
        TN tn = mTN;   // TN 后面再分析 比较麻烦
        tn.mNextView = mNextView;  //设置了文本TextView的索引
    
        try {
            service.enqueueToast(pkg, tn, mDuration);  //将要显示的内容打包进TN后压入service的Toast队列中
        } catch (RemoteException e) {
            // Empty
        }
    }
    
  • 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);
    }
    

看到这里,大致可以终结一下几点:
1. Toast show是通过将显示申请打包进TN,然后通过INotificationManager远程接口,实现进程间通信,添加TN到NotificationManagerService的ToastQuueue中。可以猜测,在服务程序中肯定是通过循环获取队列中的TN进行处理。
2. 有show函数,肯定还有cancle函数。cancle函数简单的设置了mTN的显示属性后,就调用getService.cancleToast

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

        try {
            getService().cancelToast(mContext.getPackageName(), mTN);
        } catch (RemoteException e) {
            // Empty
        }
    }
  1. 默认的Toast使用的是系统的默认Toast样式。通过代码可以知道如何自定义Toast显示样式。

下面分析Toast构造和INotificationManager

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

可以看到,核心内容还是TN,通过对TN进行了一些显示相关的参数配置。关于TN的详细内容,可以在代码Toast.java中查看。在后面再进行分析。

Toast加入队列

在show函数可以知道,显示一个Toast只是单纯的加入一个队列中。

public void show() {
    ...
    INotificationManager service = getService();  //获取系统的INotificationManager 其实就是NotificationManagerService
    ...
    service.enqueueToast(pkg, tn, mDuration);  //将要显示的内容打包进TN后压入service的Toast队列中
}

其中getService如下:

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

可以知道改函数获取的是系统的 NotificationManagerService,其实现类为NotificationManagerService.java 需要知道的是,上面的大量操作都在进行IPC操作。:(

下面直接分析NotificationManagerService 看看加入队列函数 enqueueToast 到底在做什么。

NotificationManagerService

    public void enqueueToast(String pkg, ITransientNotification callback, int duration)
    {
        ... 检测pkg和callback参数是否有效 以及判断是不是系统Toast 以及一些合法性检测

        synchronized (mToastQueue) {  //这里进行添加操作 加锁是为了防止多个应用程序同时提交出差
            int callingPid = Binder.getCallingPid();
            long callingId = Binder.clearCallingIdentity();
            try {
                ToastRecord record;
                int index = indexOfToastLocked(pkg, callback);  //在现有的队列中依据pkg和callback查看是不是Toast已经提交过 防止多次提交
                if (index >= 0) {
                    record = mToastQueue.get(index);
                    record.update(duration);  //如果提交过 只是单纯的跟新一下duration 。。。
                } else {
                    // 看注释就知道 下面的代码就是为了看看一个应用程序是不是提交的Toast显示已经超过了最大的许可 50 次 防止DOS攻击
                    // 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= 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);  //如果一切检测合法 而且没有添加过还可以添加的情况下 将请求的Toast信息构造成一个ToastRecord 然后加入mToastQueue
                    mToastQueue.add(record);  //mToastQueue是一个全局变量哦  整个系统只有一个
                    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) {  //这句话是重点 从这里启动了NotificationManagerService对Toast进行显示
                    showNextToastLocked();
                }
            } finally {
                Binder.restoreCallingIdentity(callingId);
            }
        }
    }

showNextToastLocked 代码分析

关于为何是showNextToastLocked开始了mToastQueue的提取以及Toast显示这里进行说明,因为mToastQueue函数里面 将一个新的Toast事件添加进队列的时候,是进行了加锁的。所以,当index == 0 就说明,当前队列之前是没有数据的,现在添加一个后刚好是第一个开始工作。也就是showNextToastLocked
在showNextToastLocked函数里面:

void showNextToastLocked() {
    ToastRecord record = mToastQueue.get(0);
    while (record != null) {  //虽然是个while但是 只是为了错误处理 真正的循环不是这里
        try {
            record.callback.show();  //调用callback进行show 下面分析
            scheduleTimeoutLocked(record);  //函数中进行了延时操作 同时这里使得下一个Toast能够继续显示
            return;
        } catch (RemoteException e) {
            ... do not care
        }
    }
}

scheduleTimeoutLocked

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);  //这这里 一个添加流程已经ok mToastQueue 锁已经释放 可以进行下一次添加
}

scheduleTimeoutLocked 没有什么特殊的,单纯的构造了一个Message 然后使用record里面的duration选择了一个delay值 然后使用了handler的sendMessageDealyed。至于handler的使用,再别处再论。这里直接找到mHandler的定义看看,消息是如何处理的。
在源代码中可以很方面的找到mHandler的定义:

private final class WorkerHandler extends Handler
{
    @Override
    public void handleMessage(Message msg)
    {
        switch (msg.what)
        {
            case MESSAGE_TIMEOUT:
                handleTimeout((ToastRecord)msg.obj);
                break;
            ... don't care
        }
    }

只看handlerTimeout:一看即明白

private void handleTimeout(ToastRecord record)
{
    if (DBG) Slog.d(TAG, "Timeout pkg=" + record.pkg + " callback=" + record.callback);
    synchronized (mToastQueue) {
        int index = indexOfToastLocked(record.pkg, record.callback);  //找到toast然后
        if (index >= 0) {
            cancelToastLocked(index);  //cancel掉
        }
    }
}

cancelToastLocked

void cancelToastLocked(int index) {
    ToastRecord record = mToastQueue.get(index);
    try {
        record.callback.hide();  //回调hide函数 后面分析
    } ...
    mToastQueue.remove(index); //从列表中移除
    keepProcessAliveLocked(record.pid);
    if (mToastQueue.size() > 0) {  //如果列表不为空 就接着显示 同样是加了mToastQueue锁的
        showNextToastLocked();
    }
}

至此,Toast的添加显示过程就分析明白了。下面看看show和hide函数都是什么。要分析show和hide还是要看Toast里面的TN的源码,因为TN继承了ITransientNotification.Stub实现了ITransientNotification接口

TN

private static class TN extends ITransientNotification.Stub {
    final Runnable mShow = new Runnable() {  //异步显示  必须要在UI线程进行
        @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();    
    ... 

    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   //这些配置使得Toast不能响应触摸点击事件
                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
    }


    @Override
    public void show() {
        if (localLOGV) Log.v(TAG, "SHOW: " + this);
        mHandler.post(mShow);  //用过handler post一个Runable 然后就在主线程执行了
    }

    /**
     * schedule handleHide into the right thread
     */
    @Override
    public void hide() {
        if (localLOGV) Log.v(TAG, "HIDE: " + this);
        mHandler.post(mHide);  //同上
    }

    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;
            。。。
            mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
            。。。
            mWM.addView(mView, mParams);  //然后就通过到windowManager add到线上屏上 

        }
    }

    public void handleHide() {
        if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
        if (mView != null) {

            if (mView.getParent() != null) {
                if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                mWM.removeView(mView);  //remove了
            }

            mView = null;
        }
    }
}

总结

至此 Toast 的添加显示销毁就分析完成了。大量细节使用了IPC实现。调用NotificationManagerService和WindowManager本质上都是IPC
1. 系统所有的Toast都是由NotificationManagerService的mToastQueue进行统一维护
2. 每次show操作本质上是向mToastQueue队列进行一个添加入队列的操作。
3. 每次对mToast的操作都会进行加锁
4. 显示持续时间效果是由 Handler 的 postDelay进行的实现
5. Toast的显示本质是使用WindowManager进行显示和销毁
6. Toast不能响应触摸点击事件 是系统进行了参数设置
7. Toast的显示样式可以进行定制 具体定义细节参考Toast的makeToast静态方法。

你可能感兴趣的:(Android)