欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android Toast机制实现原理

程序员文章站 2024-03-05 11:23:36
...

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。

相关标签: Toast