Android Toast机制实现原理

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/mingC0758/article/details/81915255

Toast的实现原理

通过本文,你将懂得:
1. 为什么调用Toast的子线程需要Looper.prepare()
2. Toast的Window是在哪里创建的

Toast中的IPC通信

在Toast的实现中主要有两类IPC通信:
1. 从Toast通过IPC访问NotificationManagerService(以下简称NMS)
2. 从NMS通过IPC访问Toast

其中,Toast通过SystemServer来获取NMS的远程代理对象;NMS通过Toast传递过来的远程代理对象TN来进行IPC。

TN类 典型的AIDL生成的IBinder服务端Stub类。

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

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

NotificationManagerService 是通过ToastRecord中的callback来回调TN中的方法的,实际上callback就是TN在客户端中的类。

private static final class ToastRecord
{
        final int pid;
        final String pkg;
        final ITransientNotification callback;
        int duration;
        Binder token;
}

源码分析

从Toast类的show()方法开始:

    public void show() {
        if (mNextView == null) {
            throw new RuntimeException("setView must have been called");
        }

        INotificationManager service = getService(); //获得NMS的Proxy对象
        String pkg = mContext.getOpPackageName(); //包名
        TN tn = mTN; //Toast端的IBinder对象,用来给NMS回调
        tn.mNextView = mNextView;

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

    //获得Service的远程代理对象
     static private INotificationManager getService() {
        if (sService != null) {
            return sService;
        }
        sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
        return sService;
    }

然后就来到了NMS的enqueueToast方法:

//这里只展示主要语句
public void enqueueToast(String pkg, ITransientNotification callback, int duration) {
    //判断是否是系统调用
    final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));
    //同步Toast队列
    synchronized (mToastQueue) {
        int callingPid = Binder.getCallingPid();
        long callingId = Binder.clearCallingIdentity();
        try {
            ToastRecord record;
            int index = indexOfToastLocked(pkg, callback); //查找是否已经存在
            if (index >= 0) {
                record = mToastQueue.get(index);
                record.update(duration); //更新展示时间
            } else {
                //如果是非系统的toast,限制每个应用在队列中只能保持个ToastRcord
                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拿到这个令牌之后才能创建系统级的Window
                Binder token = new Binder();
                mWindowManagerInternal.addWindowToken(token,
                        WindowManager.LayoutParams.TYPE_TOAST);
                //创建toastRecord并加入队列
                record = new ToastRecord(callingPid, pkg, callback, duration, token);
                mToastQueue.add(record);
                index = mToastQueue.size() - 1;
                keepProcessAliveIfNeededLocked(callingPid);
            }

            if (index == 0) {
                //展示下个toast
                showNextToastLocked();
            }
        } finally {
            Binder.restoreCallingIdentity(callingId);
        }
    }
}   

void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0);
while (record != null) {
    //回调Toast.TN的show方法
    record.callback.show(record.token);
    scheduleTimeoutLocked(record);
    return;
}

//展示时间完毕之后,再回调TN的cancel方法
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);
}

这里Toast通过调用NMS的远程方法,把pkg包名、callback回调、duration,构建一个ToastRecord,加入到ToastQueue队列中。

NMS通过内部的WindowManagerInternal服务来产生一个token,把这个token通过IPC再回传给Toast。

 final Handler mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                IBinder token = (IBinder) msg.obj;
                handleShow(token);
            }
        };

 public void handleShow(IBinder windowToken) {
            if (mView != mNextView) {
                //如果之前调用过,先取消
                handleHide();
                mView = mNextView;
                Context context = mView.getContext().getApplicationContext();
                String packageName = mView.getContext().getOpPackageName();
                if (context == null) {
                    context = mView.getContext();
                }
                //获取WindowManager
                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;
                //设置token
                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);
                //把视图添加到Toast类型的Window里面
                mWM.addView(mView, mParams);
                trySendAccessibilityEvent();
            }
        }

Toast拿到具有创建系统级窗口权限的token之后就可以创建Window,并且把自己的视图添加上去。

展示时间完毕,或者主动cancel的时候:

void cancelToastLocked(int index) {
        ToastRecord record = mToastQueue.get(index);
        try {
            //回调TN的hide方法
            record.callback.hide();
        } catch (RemoteException e) {

        }
        //移出队列
        ToastRecord lastToast = mToastQueue.remove(index);
        //删除分配的token
        mWindowManagerInternal.removeWindowToken(lastToast.token, true);

        keepProcessAliveIfNeededLocked(record.pid);
        if (mToastQueue.size() > 0) {
            //只有当一个Toast执行完毕的时候才进行下一个Toast
            showNextToastLocked();
        }
    }

解决问题

现在来解决开头的问题:

1、为什么调用Toast的子线程需要Looper.prepare()

因为TN中的方法show()、hide()是在Binder线程池执行的,需要通过Handler机制把消息发送到调用Toast的子线程中执行进一步的操作。

2、Toast的Window是在哪里创建的

在Toast中创建,但是需要NMS提供的token,Toast类型的Window是系统级的Window,有了这个token才有权限创建系统级的Window。

猜你喜欢

转载自blog.csdn.net/mingC0758/article/details/81915255
今日推荐