NMS Toast

0x00 NMS Toast

Toast.makeText(Context, "Toast message content.", Toast.LENGTH_SHORT).show();

以下代码分析基于Android 8.1.0

0x01 Toast

Toast类只有500多行,逻辑比较简单,主要有三部分组成: Toast,INotificationManager和TN。Toast类负责构造Toast对象;NotificationManager 负责与 NotificationManagerService交互;TN负责Toast最终的显示。

首先构建一个Toast对象:

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);
    // 将加载的View设给Toast
    result.mNextView = v;
    result.mDuration = duration;
    return result;
}

调用Toast.show()方法:

public void show() {
    // mNextView为最终要展示的View,不能为null,Toast创建的时候默认创建一个,也可通过Toast.setView(View view)设置
    if (mNextView == null) {
        throw new RuntimeException("setView must have been called");
    }

    INotificationManager service = getService();
    String pkg = mContext.getOpPackageName();
    TN tn = mTN;
    tn.mNextView = mNextView;

    try {
        // 将TN发给NotificationManagerService
        service.enqueueToast(pkg, tn, mDuration);
    } catch (RemoteException e) {
        // Empty
    }
}

几行代码,获取 INotificationManager 服务。

// =======================================================================================
// All the gunk below is the interaction with the Notification Service, which handles
// the proper ordering of these system-wide.
// =======================================================================================

private static INotificationManager sService;

static private INotificationManager getService() {
    if (sService != null) {
        return sService;
    }
    sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
    return sService;
}

看到 INotificationManager.Stub.asInterface() 很自然的会去搜 NotificationManagerService,嗯嗯,AOSP
中全局搜索就可以了。至此,Toast的工作做完一半,下来的工作进入 NotificationManagerService。

0x02 NotificationManagerService

frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java

我们来看enqueueToast():

...
final ArrayList<ToastRecord> mToastQueue = new ArrayList<>();
...
private final IBinder mService = new INotificationManager.Stub() {
    // Toasts
    // ============================================================================

    @Override
    public void enqueueToast(String pkg, ITransientNotification callback, int duration)
    {
        if (DBG) {
            Slog.i(TAG, "enqueueToast pkg=" + pkg + " callback=" + callback
                    + " duration=" + duration);
        }

        if (pkg == null || callback == null) {
            Slog.e(TAG, "Not doing toast. pkg=" + pkg + " callback=" + callback);
            return ;
        }
        final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg));
        final boolean isPackageSuspended =
                isPackageSuspendedForUser(pkg, Binder.getCallingUid());

        if (ENABLE_BLOCKED_TOASTS && !isSystemToast &&
                (!areNotificationsEnabledForPackage(pkg, Binder.getCallingUid())
                        || isPackageSuspended)) {
            Slog.e(TAG, "Suppressing toast from package " + pkg
                    + (isPackageSuspended
                            ? " due to package suspended by administrator."
                            : " by user request."));
            return;
        }

        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
                if (!isSystemToast) {
                    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) {
                    // Toast已经存在
                    record = mToastQueue.get(index);
                    record.update(duration);
                    record.update(callback);
                } else {
                    // Toast不存在
                    Binder token = new Binder();
                    // 将这个token添加到系统,否则无法正常显示
                    mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
                    record = new ToastRecord(callingPid, pkg, callback, duration, token);
                    // 添加到队列,接下来我们去跟踪这个队列的出口
                    mToastQueue.add(record);
                    index = mToastQueue.size() - 1;
                }
                keepProcessAliveIfNeededLocked(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);
            }
        }
    }

    @GuardedBy("mToastQueue")
    void showNextToastLocked() {
        // 获取要展示的Toast
        ToastRecord record = mToastQueue.get(0);
        while (record != null) {
            if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
            try {
                // 显示Toast,这个callback是上面的TN对象,可以看到,这个时候又调回了Toast.TN.show(IBinder windowToken)
                record.callback.show(record.token);
                // 处理Toast显示时间
                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
                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;
                }
            }
        }
    }

    @GuardedBy("mToastQueue")
    private void scheduleTimeoutLocked(ToastRecord r)
    {
        // 从这里可以看到,是依赖Handler实现的
        mHandler.removeCallbacksAndMessages(r);
        Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
        // 这里定义Toast的显示时间,LONG:3.5s, SHORT:2s。注意区分显示时的hideTimeoutMilliseconds
        long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
        mHandler.sendMessageDelayed(m, delay);
    }
...
};

0x03 Toast.TN

再回到Toast里,这次看里面的TN,从show(IBinder windowToken)开始。

private static class TN extends ITransientNotification.Stub {
    private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();

    private static final int SHOW = 0;
    private static final int HIDE = 1;
    private static final int CANCEL = 2;
    final Handler mHandler;

    int mGravity;
    int mX, mY;
    float mHorizontalMargin;
    float mVerticalMargin;

    View mView;
    View mNextView;
    int mDuration;

    WindowManager mWM;

    String mPackageName;

    // 默认显示时间
    static final long SHORT_DURATION_TIMEOUT = 4000; 
    static final long LONG_DURATION_TIMEOUT = 7000;

    TN(String packageName, @Nullable Looper looper) {
        // 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;

        mPackageName = packageName;

        // 子线程显示Toast就会抛这个异常
        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) {
            @Override
            public void handleMessage(Message msg) {
                switch (msg.what) {
                    case SHOW: {
                        // (2) 拿到token,展示
                        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;
                    }
                }
            }
        };
    }

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

    /**
     * schedule handleHide into the right thread
     */
    @Override
    public void hide() {
        if (localLOGV) Log.v(TAG, "HIDE: " + this);
        mHandler.obtainMessage(HIDE).sendToTarget();
    }
    /**
     * hide 和 cancel逻辑相同
     */
    public void cancel() {
        if (localLOGV) Log.v(TAG, "CANCEL: " + this);
        mHandler.obtainMessage(CANCEL).sendToTarget();
    }

    /**
     * (3)真正展示Toast的地方,最终还是WindowManager.addView()
     * @param windowToken 展示Toast的token
     */
    public void handleShow(IBinder windowToken) {
        if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                + " mNextView=" + mNextView);
        // 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.
        if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
            return;
        }
        // Toast没有显示过或者当前不再显示状态
        if (mView != mNextView) {
            // remove the old view if necessary
            // 移除之前的View
            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
            // 以下配置Toast的显示参数
            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;
            // 这种方法可以判断当前View是否正在显示,如果显示就remove掉
            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 */
            }
        }
    }

    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);
    }
    /**
     * hide逻辑比较简单,直接remove掉就可以了
     */
    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被置空
            mView = null;
        }
    }
}

至此,分析完毕。

0x03 疑问

  1. Toast为什么设计成一个远程调用?
  2. Toast和Notification什么关系?

这两个问题需要站在系统的角度考虑。简单谈谈我的理解。Toast和Notification都是系统提供的展示通知的基础组件,所有的APP都可以调用。所有的APP都可以调用,那系统就需要有一定的控制权,不然可能会被滥用。有些ROM显示Toast是需要系统授权的从侧面印证了这点。

0x04 答疑

  • Toast能改显示时间吗?

不可以,Toast的显示时间由系统决定,使用者只能从Toast.LENGTH_SHORT(4s)/Toast.LENGTH_LONG(7s)两种方式中选择一种。

  • 能不能改显示动画?

不可以,Toast.getWindowParams()为hide方法,不可以直接调用。

  • 创建Toast时对传入的Context有要求吗?

这个context有两个作用,inflate出Toast要展示的内容和获取包名等信息。这两个操作对Context都无特殊要求,Application,Activity,Service,BroadcastReceiver的Context都可以。

  • 如果我有两个屏幕,可以在第二个屏幕上显示Toast吗?

显示Toast的Token在创建时使用的DEFAULT_DISPLAY,因此不可以显示在第二块屏幕上。

0xFF 参考

  1. https://blog.csdn.net/lmj623565791/article/details/40481055
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值