IPC在Toast中的应用

1、Toast概念和问题的引出

Toast 中文名”土司”,应该算是 Android 使用频率很高的一个 widget 了,一般使用它来做一些操作的提示信息,例如我们在淘宝点击收藏宝贝之后底部就会出现”收藏成功”这样的提示信息,这种就是 Toast 的功能了。

先抛出一个问题哦:那就是多个应用或者多个线程都要在显示 Toast 的时候系统是怎么去处理这个问题的?是串行执行还是并行执行呢?

2、如何使用 Toast

Toast 的使用可以说是非常简单,简单到一句话就可以显示一个 Toast 了,下面是具体的代码体现。

Toast.makeText(this, "hello Toast", Toast.LENGTH_SHORT).show();

3、构造 Toast 对象

下面就依据上面的那句代码分析 Toast 显示到手机屏幕上是需要经过什么过程。

主要入口为两个:

  • public static Toast makeText(Context context, CharSequence text, int duration)
  • public void show()

3.1、makeText 源码分析

makeText 方法内部主要做了这样几件事:

  • 创建一个 Toast 对象;
  • 加载一个系统布局,这个布局就是用显示这个 Toast 的;
  • 设置需要显示的文本内容,就是我们在上面设置的 “hello Toast” 和设置这个 Toast 需要显示的时间;

其实就根据传入到 makeText 内部的几个属性构造出一个 Toast 对象。

public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
    Toast result = new Toast(context);
    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);
    
    result.mNextView = v;
    result.mDuration = duration;
    return result;
}

3.2、Toast 构造方法

Toast 的构造方法内部很简单,主要是创建一个 TN 对象,并且赋值给 mTN,这个 TN 对象非常重要,在下面分析中再慢慢说明它的作用。因为 TN 对象 是在 Toast 构造中创建的,因此一个 Toast 对象的创建就对应创建一个 TN 对象,这句话也很重要。

public Toast(Context context) {
    mContext = context;
    //创建一个 TN 对象
    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);
}

4、TN 类的分析

看 TN 类先看它的声明,学过 AIDL 的同学应该都知道,TN 继承了 ITransientNotification.Stub 类,说明它是远程服务真正干活的类。这什么意思呢?其实是这样的,当客户端需要显示一个土司,那么这个要求是要交给远程服务 NotificationManagerService 去统一调度的【这里是第一次 IPC 过程:用户进程访问服务进程 NotificationManagerService 】。

而 NotificationManagerService 调度完成之后是怎么通知用户进程去 show / hide 土司的呢?远程服务进程通过 mTN 与用户进程(当前进程)进行交互(show,hide等操作)【这里是第二次 IPC 过程:服务进程 NotificationManagerService 访问 用户进程 TN】。

private static class TN extends ITransientNotification.Stub {
    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() {
        @Override
        public void handleMessage(Message msg) {
            IBinder token = (IBinder) msg.obj;
            handleShow(token);
        }
    };
    int mGravity;
    int mX, mY;
    float mHorizontalMargin;
    float mVerticalMargin;
    View mView;
    View mNextView;
    int mDuration;
    WindowManager mWM;
    static final long SHORT_DURATION_TIMEOUT = 5000;
    static final long LONG_DURATION_TIMEOUT = 1000;
    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
                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
    }
    /**
     * 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);
    }
    public void handleShow(IBinder windowToken) {
        if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                + " mNextView=" + mNextView);
        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().getConfigura
            final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDi
            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);
            mWM.addView(mView, mParams);
            trySendAccessibilityEvent();
        }
    }
    private void trySendAccessibilityEvent() {
        AccessibilityManager accessibilityManager =
                AccessibilityManager.getInstance(mView.getContext());
        if (!accessibilityManager.isEnabled()) {
            return;
        }
        // treat toasts as notifications since they are used to
        // announce a transient piece of information to the user
        AccessibilityEvent event = AccessibilityEvent.obtain(
                AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
        event.setClassName(getClass().getName());
        event.setPackageName(mView.getContext().getPackageName());
        mView.dispatchPopulateAccessibilityEvent(event);
        accessibilityManager.sendAccessibilityEvent(event);
    }        
    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);
            }
            mView = null;
        }
    }
}

5、显示一个Toast

  • 在 show 方法中首先判断 mNextView 是否为空,这个 mNextView 就是加载出来需要展示 Toast 的布局啦,如果连布局都没有,那么显示什么土司,直接就 throw 了。

  • 获取包名 pkg ,为什么需要包名呢,因为在远程服务它会拿这个包名去判断需要显示的 Toast 是不是来至同一个进程的(一个进程对应一个包名),具体的判断过程待会再分析。

  • 给 tn 赋值 mTN ,还记得这个 mTN 不?他就是在 Toast.makeText 内部方法中创建的(也就是在Toast构造中创建),我在前面说过一个 Toast 对象对应一个 mTN 对象。

  • 调用 IPC 通知远程服务 NotificationServiceManager 去enqueueToast,这个过程非常重要。我们看到它将一个 tn 作为参数传入,这是干嘛用呢?我们先前在介绍 TN 这个类时说过,这个类是用于进程间通讯的,因为显示和隐藏 Toast 是需要由远程的 NMS 去统一调度的,它内部维护一个集合存放一个个 Toast ,当到了指定时间可以显示这个 Toast 了,那么 NMS 就可以通过 TN 这个代理类去通知客户端去真正显示一个 Toast 。

public void show() {
    if (mNextView == null) {
        throw new RuntimeException("setView must have been called");
    }
    //获取远程服务 INotificationManager
    INotificationManager service = getService();
    //获取包名 pkg 
    String pkg = mContext.getOpPackageName();
    TN tn = mTN;
    tn.mNextView = mNextView;
    try {
        service.enqueueToast(pkg, tn, mDuration);
    } catch (RemoteException e) {
        // Empty
    }
}

5.1、INotificationManager.Stub.Proxy.enqueueToast

service.enqueueToast(pkg, tn, mDuration) 这个方法的调用实际上是通过
INotificationManager的代理对象去执行的,具体执行代码就在下面:主要就是将数据写入到 Parcel _data 中,然后调用 mRemote.transact 进行 RPC 远程调用操作,之后通过 _reply 获取返回的数据。

//INotificationManager.Stub.Proxy.enqueueToast 代码
public void enqueueToast(java.lang.String pkg, android.app.ITransientNotification callback, int duration) throws android.os.RemoteException
{
    android.os.Parcel _data = android.os.Parcel.obtain();
    android.os.Parcel _reply = android.os.Parcel.obtain();
    try {
        _data.writeInterfaceToken(DESCRIPTOR);
        _data.writeString(pkg);
                //callback就是 tn 对象,这里需要传递给远程服务,因此转化为 Bidner 
        _data.writeStrongBinder((((callback!=null))?(callback.asBinder()):(null)));
        _data.writeInt(duration);
        mRemote.transact(Stub.TRANSACTION_enqueueToast, _data, _reply, 0);
        _reply.readException();
    }
    finally {
        _reply.recycle();
        _data.recycle();
    }
}

6、开始远程服务调用 Toast

[ NMS 的源码] (https://android.googlesource.com/platform/frameworks/base/+/f76a50c/services/java/com/android/server/NotificationManagerService.java) 有一个网站 grepcode 是可以查阅源码的,有需要可以上这里看看源码。

在 enqueueToast 方法内部主要做了这几件事:

  • synchronize 加锁判断,这里为什么要加锁?
  • indexOfToastLocked校验操作;
  • 根据 indexOfToastLocked 的校验结果进行后续的操作。
public void enqueueToast(String pkg, ITransientNotification callback, int duration) {

    //pkg 和 callback 的空校验 代码省略...

    //加锁判断
    synchronized (mToastQueue) {
        int callingPid = Binder.getCallingPid();
        long callingId = Binder.clearCallingIdentity();
        try {
            ToastRecord record;
            //查找队列中是否由该 ToastRecord
            int index = indexOfToastLocked(pkg, callback);
            // If it's already in the queue, we update it in place, we don't
            // move it to the end of the queue.
            //表示在队列中找到对应的 ToastRecord
            if (index >= 0) {
                //更新这个 ToastRecord 的显示时间,这种情况是在同一个用用中,使用同一个 Toast 对象,在极短时间内多次调用show方法,就会出现这个情况。           
                record = mToastQueue.get(index);
                record.update(duration);
            } else {
                //没有找到的情况
                // Limit the number of toasts that any given package except the android
                // package can enqueue.  Prevents DOS attacks and deals with leaks.
                if (!"android".equals(pkg)) {//不是系统弹的土司
                    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++;
                            //控制 toast 的次数 50
                             if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                                 Slog.e(TAG, "Package has already posted " + count
                                        + " toasts. Not showing more. Package=" + pkg);
                                 return;
                             }
                         }
                    }
                }
                //没有找到的情况就创建一个 ToastReord 对象。
                record = new ToastRecord(callingPid, pkg, callback, duration);
                //添加到队列中。
                mToastQueue.add(record);
                
                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) {
                showNextToastLocked();
            }
        } finally {
            Binder.restoreCallingIdentity(callingId);
        }
    }
}

6.1、enqueueToast 加锁判断

这里要思考一下为什么这里要加锁?我们知道锁就是为了怕并发访问出现数据的不同步问题,NMS 是系统服务,那么它的对象就只有一个,多个进程(多个 APP) 或者说这多个线程都可以获取到这个服务,并且可以并发地去调用该服务的 enqueueToast 方法,这就像多个线程操作同一个对象的共享变量出现的数据不同步问题了,因此这里需要加锁。还有就是 enqueueToast 方法是服务端的方法,它会在Binder 线程池中去调用。

6.2、indexOfToastLocked的校验操作

检测当前pag 包名和 callback 是否在 mToastQueue 中已经存在?在分析之前首先了解一下 callback 是什么呢?我们在先前创建 Toast 构造提到过,一个 Toast 对象会对应一个 mTN 对象。这个 callback 就是这个 mTN 对象的代理对象,为什么?因为它是需要进行进程间通讯的,因此需要将 mTN 对象转化为 Binder 才能传输,而在 NMS 中获取到这个 Binder 之后会将其转化为 ITransientNotification.Proxy 代理对象。

该方法会返回一个整数,-1表示没有找到,其他值表示找到了。

private int indexOfToastLocked(String pkg, ITransientNotification callback)
{
    IBinder cbak = callback.asBinder();
    ArrayList<ToastRecord> list = mToastQueue;
    int len = list.size();
    for (int i=0; i<len; i++) {
        ToastRecord r = list.get(i);
         //包名和binder都一样时才表示是同一个 ToastRecord
        if (r.pkg.equals(pkg) && r.callback.asBinder() == cbak) {
            return i;
        }
    }
    return -1;
}

6.3、根据 indexOfToastLocked 的校验结果进行后续的操作。

1.如果找到了那么返回值不为-1,那么就直接更新一下 duration 并且不会去修改该 ToastRecord 为 mToastQueue 中的位置。意思是什么呢?这里举个栗子:这里 for 循环 500 次,每次都调用 toast.show() 方法,其运行结果只会显示一次,为什么呢?因为我们在上面提到过,使用 indexOfToastLocaked 内部是根据 pkg 和 callback 进行判断的,而这两个值是一样的,因此该方法返回的值不为 -1 ,因此就算是 show 500 次其 mToastQueue 中也只有一个 ToastRecord 对象。

Toast toast = Toast.makeText(this, "Toast", Toast.LENGTH_SHORT);
for (int i = 0; i < 500; i++) {
    toast.show();
}

2.如果找到的值为-1表示需要创建一个新的 ToastRecord 并且添加到 mToastQueue 中。因为之前得到的 index 为 -1 ,因此在这里需要重新计算 index = mToastQueue.size()-1,这个 index 就表示当前新创建 ToastRecord 在队列中的位置,当这个位置为 0 时表示只有队列中只有一个 ToastRecord ,直接执行 showNextToastLocked 即可。

3.注意这里,如果没有知道的情况,并且同一个 pkg 下的 ToastRecord 的数量超过了 MAX_PACKAGE_NOTIFICATIONS 那么就直接 return 了,官方表示这是为了避免 DOS attacks。

4.这里注意下面这一种情况,执行结果是 toast 会执行 50 次,多余都被抛弃了,原因看上面第 3 点。但是重点这种写法跟第 1 点的写法的区别就是 Toast 的创建时机不一样,这里是每循环一次就创建一个,也就是 Toast 的构造就会走一次,还记得上面说过 mTN 是在 Toast 构造创建的,每次调用 makeText 都会创建一个新的 Toast 对象,也就是 mTN 也是一个新的对象。那么传递给 NMS 的 mTN 就不一样了,那么在进行 indexOfToastLocked 就会返回 -1 因此这里就是这段代码的区别。

for (int i = 0; i < 500; i++) {
    Toast toast = Toast.makeText(this, "Toast", Toast.LENGTH_SHORT).show();
}

7、远程服务端调度显示和隐藏操作

7.1、showNextToastLocked()

这个方法主要做了一下几件事:

  • 从队列中取出一个 ToastRecord 并且通过 IPC 调用用户进程的 TN 中的 show 方法真正去显示一个 Toast 。

  • 通过 Handler 发送一个延时任务,时间为 duration ,在这个时间过后就取消显示这个 Toast。

private void showNextToastLocked() {
    //从队列中取出第一个 ToastRecord 对象
    ToastRecord record = mToastQueue.get(0);
    //while 循环是怕应用进程出现异常 RemoteException
    while (record != null) {
        if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
        try {
            //通知应用进程去执行显示操作,实际上会调用 TN 的 show() 方法。
            record.callback.show();
            //在指定时间隐藏这个 Toast
            scheduleTimeoutLocked(record, false);
            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);
            }
            keepProcessAliveLocked(record.pid);
            //从队列中再取出下一个 ToastRecord 去执行。
            if (mToastQueue.size() > 0) {
                record = mToastQueue.get(0);
            } else {
                //如果没有就置null,退出循环
                record = null;
            }
        }
    }
}

7.2、record.callback.show();

record.callback 就是用户进程传递过来服务进程的 ITransientNotification 的代理对象,通过调用该代理对象的 show() 方法,这个过程是一个 IPC 的过程,这个最终会调用用户进程的 TN 中的 show() 方法。

public void show() {
    //通过 handler 去 post 一个任务
    mHandler.post(mShow);
}

final Runnable mShow = new Runnable() {
    public void run() {
        handleShow();
    }
};

//通过 WindowManager 将 View 显示出来
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 = WindowManagerImpl.getDefault();
        final int gravity = mGravity;
        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;
        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);
        mWM.addView(mView, mParams);
        trySendAccessibilityEvent();
    }
}

7.3、 scheduleTimeoutLocked

使用 Handler 发送一个延时消息,时间就是 Toast 设置的 duration 属性。

private void scheduleTimeoutLocked(ToastRecord r, boolean immediate)
{
    Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
    long delay = immediate ? 0 : (r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY);
    mHandler.removeCallbacksAndMessages(r);
    mHandler.sendMessageDelayed(m, delay);
}

7.4、 handleTimeout

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

private void handleTimeout(ToastRecord record)
{
  
    synchronized (mToastQueue) {
        int index = indexOfToastLocked(record.pkg, record.callback);
        if (index >= 0) {
            cancelToastLocked(index);
        }
    }
}

7.5、cancelToastLocked

当通过 Handler 发送的延时任务的时间到了那么就需要通知应用进程去 hide 这个 Toast 。具体操作还是通过 IPC 过程去真正调用 TN 的 hide 方法实现真正的隐藏操作。

当操作完成之后判断队列中是否还有 ToastRecord 没有执行,如果有那么就调用 showNextToastLocked 方法再次的去取出 ToastRecord 去执行。

private void cancelToastLocked(int index) {
    ToastRecord record = mToastQueue.get(index);
    try {
        //远程调用应用进程的 TN 的 hide() 方法隐藏 toast
        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
    }
    mToastQueue.remove(index);
    keepProcessAliveLocked(record.pid);
    //如果队列中还有数据那么就再次调用 showNextToastLocked 方法流程跟上面是一样的。
    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();
    }
}

    原文作者:未见哥哥
    原文地址: https://www.jianshu.com/p/e4ae61e9f6f7
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞