Toast显示流程

2018-05-02  本文已影响130人  涂豪_OP

最近在基于Toast做一个需求,其中一个功能是让Toast一直显示,做出来后,在6.0平台上,Toast能够正常显示;但是在8.1上就不行,所以专门研究了下Toast的运行流程.
手机上的所有Toast都由系统统一调度,更具体点说,就是NotificationManagerService这个类,这就涉及到跨进程通信,应用进程涉及到Toast的跨进程通信都由Toast的内部类TN来完成,TN是一个Binder实体,NotificationManagerService会记录每个TN的代理对象,NotificationManagerService有一个名为mToastQueue的ArrayList,这个容器存放了所有发往系统的Toast信息,Toast信息以ToastRecord的形式存在(跟ActivityRecord,ServiceRecord等一毛一样),用图形表示如下:


toast.PNG

Toast的使用非常的简单,如下所示:

Toast.makeText(this,"显示toast",Toast.LENGTH_SHORT).show();

上面的代码一共分成两部分,第一部分是makeText这个方法,第二个是show这个方法,先来看下makeText方法:

/**
 * Make a standard toast to display using the specified looper.
 * If looper is null, Looper.myLooper() is used.
 * @hide
 */
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
        @NonNull CharSequence text, @Duration int duration) {
    //首先创建一个toast对象
    Toast result = new Toast(context, looper);

    //设置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);
  
    //记住mNextView,在显示Toast的时候会用到它,这个view就是我们看到的Toast的效果
    result.mNextView = v;
    result.mDuration = duration;
    //返回toast
    return result;
}

当然,上面的方法是一个重载的方法,我们直接调用的不是这个方法,但是我们自己调用makeText方法最后肯定会走到上面的方法中去的,所以直接看这个方法.makeText方法其实不难,最主要的是创建Toast对象,我们来看下他的构造方法:

/**
 * Constructs an empty Toast object.  If looper is null, Looper.myLooper() is used.
 * @hide
 */
public Toast(@NonNull Context context, @Nullable Looper looper) {
    mContext = context;
    //创建一个TN对象,重要
    mTN = new TN(context.getPackageName(), looper);
    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对象是一个非常重要的对象,Toast的显示和消失,都是由系统来控制,由TN来实现;可以看出,一个Toast就对应一个TN对象,来看看TN的实现:

private static class TN extends ITransientNotification.Stub {
    //可以看到,TN是一个binder实体对象

    TN(String packageName, @Nullable Looper looper){
        //Toast窗口的属性
        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;
        //显示Toast的包名
        mPackageName = packageName;
        
        //初始化Looper对象
        if (looper == null) {
            // Use Looper.myLooper() if looper is not specified.
            looper = Looper.myLooper();
            if (looper == null) {
                throw new RuntimeException(
                        "Can't toast on a thread that has not called Looper.prepare()");
            }
        }
        
        //hanlder对象,控制了toast的显示和消失
        mHandler = new Handler(looper, null) {
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case SHOW: {
                        IBinder token = (IBinder) msg.obj;
                        handleShow(token);
                        break;
                    }
                    case HIDE: {
                        handleHide();
                        // Don't do this in handleHide() because it is also invoked by
                        // handleShow()
                        mNextView = null;
                        break;
                    }
                    case CANCEL: {
                        handleHide();
                        // Don't do this in handleHide() because it is also invoked by
                        // handleShow()
                        mNextView = null;
                        try {
                            getService().cancelToast(mPackageName, TN.this);
                        } catch (RemoteException e) {
                        }
                        break;
                    }
                }
            }
        };
    }
}

从上面的分析可以看出,当我们调用Toast.makeText的时候,系统创建了一个Toast对象,为他设置了布局,同时设置了Toast的窗口属性,还创建了一个真正实现Toast显示和隐藏功能的TN对象.
  下面再来分析下show方法:

 /**
  * Show the view for the specified duration.
  */
public void show() {
    //这个mNextView是创建Toast的时候赋值的,一般不为null
    if (mNextView == null) {
        throw new RuntimeException("setView must have been called");
    }
    
    //获取通知服务
    INotificationManager service = getService();
    
    //显示Toast的包名
    String pkg = mContext.getOpPackageName();
    TN tn = mTN;
    tn.mNextView = mNextView;

    try {
        //调用NotificationManagerService的enqueueToast方法
        service.enqueueToast(pkg, tn, mDuration);
    } catch (RemoteException e) {
        // Empty
    }
}

show方法本身非常简单,不用多说,直接看NotificationManagerService的enqueueToast方法:

//第一个参数是显示Toast的包名;第二个参数就是上面分析过的TN对象,
//在这里他是TN的一个代理对象;第三个参数是Toast的显示时间
@Override
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{   
    //如果包名或者TN为空,那么直接返回
    if (pkg == null || callback == null) {
        Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);
        return ;
    }
    
    //是不是系统弹的Toast,isCallerSystemOrPhone方法很简单,不多说
    final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg));
    
    //应用被挂起?
    final boolean isPackageSuspended =
                    isPackageSuspendedForUser(pkg, Binder.getCallingUid());

    //如果不是系统弹的Toast,而且允许Toast被阻塞,而且该应用
    //允许弹通知或者挂起,那么该Toast将不会被弹出来
    if (ENABLE_BLOCKED_TOASTS && !isSystemToast &&
                (!areNotificationsEnabledForPackage(pkg, Binder.getCallingUid())
                        || isPackageSuspended)) {
        return;
    }
    
    //mToastQueue是一个ToastRecord列表
    synchronized (mToastQueue) {
        int callingPid = Binder.getCallingPid();
        long callingId = Binder.clearCallingIdentity();
        
        try {
            ToastRecord record;
            //该包在Toast列表中的索引
            int index;
            // All packages aside from the android package can enqueue one toast at a time
            //如果不是系统Toast的话,直接根据包名去找ToastRecord
            if (!isSystemToast) {
                index = indexOfToastPackageLocked(pkg);
            } else {
                //普通应用的话,不仅要根据包名,还要根据TN去找
                index = indexOfToastLocked(pkg, callback);
            }
            
            //如果index大于0,说明此应用之前显示过Toast
            if (index >= 0) {
                //取出ToastRecord,然后更新;更新过程非常简单,
                //就是重新设置duration和callback
                record = mToastQueue.get(index);
                record.update(duration);
                record.update(callback);
            } else {
                //进入这个分支,说明此应用从没显示过Toast
                Binder token = new Binder();
                //处理token
                mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
                //创建一个ToastRecord
                record = new ToastRecord(callingPid, pkg, callback, duration, token);
                //添加进ToastRecord列表(的末尾)
                mToastQueue.add(record);
                //此应用在Toast列表的索引
                index = mToastQueue.size() - 1;
            }
            
            //处理进程,意思应该是一个正要显示Toast的应用应该处于活跃状态
            keepProcessAliveIfNeededLocked(callingPid);
            
            //如果该Toast位于Toast列表的第一个,那么调用showNextToastLocked显示它
            if (index == 0) {
                showNextToastLocked();
            }
        }finally {
            Binder.restoreCallingIdentity(callingId);
        }
    }
}

enqueueToast其实也不难,首先处理一些不应该显示Toast的场景;其次根据包名或者TN去查找ToastRecord,找到了就更新下,没找到就创建一个添加进Toast列表;接着就更新进程相关的信息;最后如果该Toast位于Toast列表的第一个,那么调用showNextToastLocked显示这个Toast:

@GuardedBy("mToastQueue")
void showNextToastLocked() {
    //既然要显示,肯定先显示列表中的第一个,所以取出第一个ToastRecord
    ToastRecord record = mToastQueue.get(0);
    while (record != null) {
        if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
        try {
            //调用TN的show方法显示Toast
            record.callback.show(record.token);
            //超时处理,不管
            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
            //如果Toast显示失败,那么将这个ToastRecord中列表中移除,然后显示下一个
            int index = mToastQueue.indexOf(record);
            if (index >= 0) {
                mToastQueue.remove(index);
            }
            keepProcessAliveIfNeededLocked(record.pid);
            if (mToastQueue.size() > 0) {
                record = mToastQueue.get(0);
            } else {
                record = null;
            }
        }
    }
}

可以看到,showNextToastLocked其实也很简单,就是调用TN的show方法;如果显示失败,接着显示下一个Toast.
  到目前为止,Toast的framework层的流程已经分析完毕,很简单;下面分析TN是怎么显示Toast的:

/**
 * schedule handleShow into the right thread
 */
@Override
public void show(IBinder windowToken) {
    if (localLOGV) Log.v(TAG, "SHOW: " + this);
    mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}

--------------------------------------------------------------------------

mHandler = new Handler(looper, null) {
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case SHOW: {
                IBinder token = (IBinder) msg.obj;
                handleShow(token);
                break;
            }
            ......
}

--------------------------------------------------------------------------

public void handleShow(IBinder windowToken) {
    // If a cancel/hide is pending - no need to show - at this point
    // the window token is already invalid and no need to do any work.
    //如果当前Toast正在等待消失或者隐藏,那么此Toast就不显示了,直接返回
    if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
        return;
    }
    
    //mView是上次Toast显示的view,mNextView是这次显示的view
    if (mView != mNextView) {
        //移除上次显示的view,简单,不多说
        handleHide();
        
        //下面一坨代码都是处理Toast的窗口属性
        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);
        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;
        }
        mParams.x = mX;
        mParams.y = mY;
        mParams.verticalMargin = mVerticalMargin;
        mParams.horizontalMargin = mHorizontalMargin;
        mParams.packageName = packageName;
        mParams.hideTimeoutMilliseconds = mDuration ==
            Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
        mParams.token = windowToken;
        
        //下面几行代码的意思是如果此View有父View的话,就将View移除,不太懂
        if (mView.getParent() != null) {
            if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
            mWM.removeView(mView);
        }
        
        //将此View添加进WindowManager,添加进去后就会触发
        //测量,布局,绘制三大步骤,然后Toast就显示出来了
        try {
            mWM.addView(mView, mParams);
            trySendAccessibilityEvent();
        } catch (WindowManager.BadTokenException e) {
            /* ignore */
        }
    }
}

总结:
 Toast的显示流程包括三个部分,第一个是Toast的创建;第二个是Toast在framework的处理;第三是Toast的显示:
  1.Toast的创建是由Toast.makeText触发的,makeText方法会导致一个空的Toast对象和TN对象的创建,空的Toast创建后,就会加载布局,默认是transient_notification.xml,然后把这个View赋值给mNextView;在创建空的Toast对象的时候,还会创建一个TN对象,在TN对象的构造函数里面会初始化Toast的窗口属性,然后里面还有个Handler,用于控制Toast的显示,隐藏,取消;
    2.Toast在framework的处理流程从enqueueToast方法开始,首先他会判断要不要显示此Toast,如果不显示,那么直接return;接着根据包名或者包名+TN去Toast队列里面查找相应的ToastRecord,一个ToastRecord对应一个应用进程里面的Toast;如果找到了,就更新这个ToastRecord的duration和TN;如果没有找到,就创建一个ToastRecord对象,然后添加进Toast列表的末尾;如果添加进去的Toast是Toast队列的第一个Toast,那么就显示这个Toast;显示的流程也很简单,就是死循环,不停的从队列里面拿到ToastRecord,然后调用Toast对应的TN的show方法显示Toast,一直到没有Toast为止;
    3.TN收到显示的消息后,就会创建WindowManager对象,设置窗口属性,然后将第一步的View添加进WindowManager,这样,Toast就显示出来了.
  以上,就是Toast的显示流程

上一篇 下一篇

猜你喜欢

热点阅读