首页
登录 | 注册

Toast初探

总结:

  • 首先Toast的make方法主要就是封装TN(Binder)这个对象,这里有几点需要注意
    1. Toast默认的是TextView,必须需要有个显示信息的UI,否则会报错
    2. 当前线程必须有相对应的Looper,否则报错,因为里面的显示机制还是跟Handler有关的,主线程是默认有MainLooper的
  • show方法:其实是系统内部维护着一个队列,通过不断的从该队列中取出ToastRecord(对TN的封装),这个队列是通过AIDL进行通讯的,从Toast–通过AIDL–TN,然后再通过WidowManager显示到界面上
  • 值得注意的是,我们无法对系统内部的队列进行操作。回顾整个流程,其实我们无法控制的是AIDL通讯的部分,如果我们需要完全控制Toast的话,完全可以把AIDL这部分取消掉。关于这点我们以后再单独讨论

源码分析

我们通常这样使用Toast:

public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
        return makeText(context, null, text, duration);
}

    /**
     * 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 result = new Toast(context, looper);

        LayoutInflater inflate = (LayoutInflater)
                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
                
        //这个布局就只有一个TextView
        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;
    }
    /**
     * Constructs an empty Toast object.  If looper is null, Looper.myLooper() is used.
     * @hide
     */
    public Toast(@NonNull Context context, @Nullable Looper looper) {
        mContext = context;
        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);
    }

我们看到Toast的makeText方法其实就是先新建一个Toast对象,然后其各个赋好值,这里有个TN类:

TN extends ITransientNotification.Stub
        TN(String packageName, @Nullable Looper looper) {
        .......
        //这里也说明了为什么直接在子线程中使用Toast会报错了,除非先执行looper.prepare
            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()");
                }
            }
            mHandler = new Handler(looper, null) {
            .........
        }

我们知道TN是一个继承自ITransientNotification.Stub的类,以及looper不能为空,为的是Handler的实例化,这里我们暂时只用知道他的继承关系就好了,知道其是一个Binder对象,可以用于进程间通讯,然后回到我们的makeText方法,在调用了Toast的构造方法创建了Toast对象之后,我们又通过context.getSystemService方法获取到LayoutInflater,然后通过调用LayoutInflater的inflate方法加载到了Toast的布局文件,这个布局文件其实就是一个LinearLayout+TextView,我们来看看show方法的具体实现:

    /**
     * Show the view for the specified duration.
     */
    public void show() {
        if (mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }
        //NotificationManagerService
        INotificationManager service = getService();
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        tn.mNextView = mNextView;

        try {
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
            // Empty
        }
    }

这里的INotificationManager是服务器端NotificationManagerService的Binder客户端,然后我们调用了service.enqueueToast方法

//callback也就是传进来的TN对象
        public void enqueueToast(String pkg, ITransientNotification callback, int duration)
        {
            .......
            
            synchronized (mToastQueue) {
                int callingPid = Binder.getCallingPid();
                long callingId = Binder.clearCallingIdentity();
                try {
                    ToastRecord record;
                    int index;
                    // All packages aside from the android package can enqueue one toast at a time
                    //是否是系统Toast
                    if (!isSystemToast) {
                    //里面实现很简单,其实就是在一个List<ToastRecord>中遍历去查找,找到第一个与pkg相同的ToastRecord,如果没有返回-1
                        index = indexOfToastPackageLocked(pkg);
                    } else {
                        index = indexOfToastLocked(pkg, callback);
                    }
                    // If the package already has a toast, we update its toast
                    // in the queue, we don't move it to the end of the queue.
                    if (index >= 0) {
                        record = mToastQueue.get(index);
                        //也就是属性的赋值
                        record.update(duration);
                        record.update(callback);
                    } else {
                    //也就是第一次把Toast加入进来,mToastQueue里面是为空
                        Binder token = new Binder();
                        mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
                        //这里我们知道ToastRecord也就是属性的封装
                        record = new ToastRecord(callingPid, pkg, callback, duration, token);
                        mToastQueue.add(record);
                        index = mToastQueue.size() - 1;
                    }
                    keepProcessAliveIfNeededLocked(callingPid);
                  //如果不等于0的话就排队
                    if (index == 0) {
                        showNextToastLocked();
                    }
                } finally {
                    Binder.restoreCallingIdentity(callingId);
                }
            }
        }

这里的callback就是上面说的TN对象,如果没找到,则新建ToastRecord对象:

 ToastRecord(int pid, String pkg, ITransientNotification callback, int duration,
                    Binder token) {
            this.pid = pid;
            this.pkg = pkg;
            this.callback = callback;
            this.duration = duration;
            this.token = token;
        }

ToastRecord其实就是记录一些信息的类罢了,callback指的是TN这个类的对象,接着我们看看showNextToastLocked:

    void showNextToastLocked() {
        ToastRecord record = mToastQueue.get(0);
        while (record != null) {
            try {
                //调用TN对象的show方法
                record.callback.show(record.token);
                //从mToastQueue队列中移除掉record,继续处理剩下的record
                scheduleDurationReachedLocked(record);
                return;
            } catch (RemoteException e) {
                // remove it from the list and let the process die
                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;
                }
            }
        }
    }

上面已经说了,callback其实就是传进来的TN对象,然后就会调用TN的show方法:

        public void show(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
        }

mHandler在TN构造函数中就定义了:

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

show方法传的是SHOW,执行的是handleShow:

       public void handleShow(IBinder windowToken) {
           
            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;
                mParams.hideTimeoutMilliseconds = mDuration ==
                    Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
                mParams.token = windowToken;
                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);
                // Since the notification manager service cancels the token right
                // after it notifies us to cancel the toast there is an inherent
                // race and we may attempt to add a window after the token has been
                // invalidated. Let us hedge against that.
                try {
                    mWM.addView(mView, mParams);
                    trySendAccessibilityEvent();
                } catch (WindowManager.BadTokenException e) {
                    /* ignore */
                }
            }
        }

一开始mView默认是为null的,mNextView在Toast的makeToast中已经有默认的值,或者使用Toast对象的setView方法,而且在Toast的show方法中明确的指出mNextView为null是会抛出异常的,所以这时候mView!=mNextView,接着看handleHide方法:

        public void handleHide() {
            if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
            if (mView != null) {
                // note: checking parent() just to make sure the view has
                // been added...  i have seen cases where we get here when
                // the view isn't yet added, so let's try not to crash.
                if (mView.getParent() != null) {
                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
                    mWM.removeViewImmediate(mView);
                }

                // Now that we've removed the view it's safe for the server to release
                // the resources.
                try {
                    getService().finishToken(mPackageName, this);
                } catch (RemoteException e) {
                }

                mView = null;
            }
        }
    }

很明显,先将View从Window移除( mWM.removeViewImmediate(mView);然后释放这个View资源,接着看handleShow方法,剩下也就是配置一些属性值,然后通过WindowManager把View加到Window中,回到NotificationManagerService类法,后面还有scheduleDurationReachedLocked这个方法执行:

    private void scheduleDurationReachedLocked(ToastRecord r)
    {
        mHandler.removeCallbacksAndMessages(r);
        Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
        //这里有个需要的注意点,就是Toast显示的时间长度只能有这两种情况
        long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
        mHandler.sendMessageDelayed(m, delay);
    }

duration也就是makeToast中的时间,what=MESSAGE_TIMEOUT,将会执行的方法: handleTimeout((ToastRecord)msg.obj):

    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);
            if (index >= 0) {
                cancelToastLocked(index);
            }
        }
    }
    void cancelToastLocked(int index) {
        ToastRecord record = mToastQueue.get(index);
        try {
            record.callback.hide();
        } catch (RemoteException e) {
            Slog.w(TAG, "Object died trying to hide notification " + record.callback
                    + " in package " + record.pkg);
            // don't worry about this, we're about to remove it from
            // the list anyway
        }
         ToastRecord lastToast = mToastQueue.remove(index);
        mWindowManagerInternal.removeWindowToken(lastToast.token, true, DEFAULT_DISPLAY);

        keepProcessAliveIfNeededLocked(record.pid);
        if (mToastQueue.size() > 0) {
            // Show the next one. If the callback fails, this will remove
            // it from the list, so don't assume that the list hasn't changed
            // after this point.
            showNextToastLocked();
        }

很明显,又调回了TN的hide方法:handleHide();同时在MToastQueue移除掉该ToastRecord,如果MToastQueue的大小还大于0,则继续执行showNextToastLocked操作



2020 jeepxie.net webmaster#jeepxie.net
10 q. 0.009 s.
京ICP备10005923号