Android窗口机制:八、Toast机制-(封装可以在任何线程使用的toast)。(源码版本SDK31)

Android 窗口机制 SDK31源码分析 总目录

本章节带大家了解一下toast机制,并且简单封装一个可以在任何线程中使用的toast。

带着以下几个问题,我们去看源码:

  1. 想在子线程调用toast应该怎么处理?
  2. toastwindow是什么,为什么回到桌面依旧会显示呢?

源码分析

Toast的常规调用方式:Toast.makeText(context, str, duration).show()

所以先看makeText方法。

    Toast
    public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
        //传入的looper为null
        return makeText(context, null, text, duration);
    }
    public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
            @NonNull CharSequence text, @Duration int duration) {
        //对于以 Android 11(API 级别 30)或更高版本为目标平台的应用处于启用状态。
        //目的:文本消息框现在由 SystemUI 呈现,而不是在应用内呈现。具体toast的show操作,由SystemUI进行。
        if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
            Toast result = new Toast(context, looper);
            result.mText = text;
            result.mDuration = duration;
            return result;
        }
        ...
    }
	//构建Toast
    public Toast(@NonNull Context context, @Nullable Looper looper) {
        mContext = context;
        //构建token,一个Toast对应一个Token
        mToken = new Binder();
        //会判断如果传入的looper为null,则获取looper.mylooper。因为主线程创建之时就构建了Looper,故这里默认拿到的是主线程的Looper。
        looper = getLooper(looper);
        mHandler = new Handler(looper);
        mCallbacks = new ArrayList<>();
        mTN = new TN(context, context.getPackageName(), mToken,
                mCallbacks, looper);
        ...
    }
    private Looper getLooper(@Nullable Looper looper) {
        if (looper != null) {
            return looper;
        }
        return checkNotNull(Looper.myLooper(),
                "Can't toast on a thread that has not called Looper.prepare()");
    }

通过makeToast方法会构建一个Toast。而每一个Toast会对应到一个token。同时,默认情况下需要获取当前线程的looper。如果当前线程不存在looper则会抛异常。故,如果想要在子线程调用Toast,则必须要先构建looper

另外说明一下,Android11以上,会进行Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)判断,之后具体的toast弹出由系统负责(因为之前大家看到的可能会通过IPC回调到TN,然后进行showorhiden,这里不再是了),下面会介绍到。

好,下面继续分析show的操作。

    Toast
	public void show() {
        ...
		//获取到NotificationManagerService
        INotificationManager service = getService();
        //包名
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;
        tn.mNextView = mNextView;
        final int displayId = mContext.getDisplayId();

        try {
            if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
                if (mNextView != null) {
                    service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
                } else {
                    // 由于我们传入的是一个text,故走这里,注意并没有传入TN
                    ITransientNotificationCallback callback =
                            new CallbackBinder(mCallbacks, mHandler);
                    //调用了NotificationManagerService的enqueueTextToast方法,传入了包名、token、文字、mDuration、displayId、callback
                    //这个callback会在toast show 或者 hiden之后进行回调
                    service.enqueueTextToast(pkg, mToken, mText, mDuration, displayId, callback);
                }
            } else {
                service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
            }
        } 
        ...
    }

show首先获取到了NotificationManagerService,之后经过一系列的判断,调用到了enqueueTextToast方法。传入了包名、token等相关的参数。

接下来看NotificationManagerService相关的代码:

NotificationManagerService
@Override
public void enqueueTextToast(String pkg, IBinder token, CharSequence text, int duration, int displayId, @Nullable ITransientNotificationCallback callback) {
	enqueueToast(pkg, token, text, null, duration, displayId, callback);
}
private void enqueueToast(String pkg, IBinder token, @Nullable CharSequence text,
                          @Nullable ITransientNotification callback, int duration, int displayId,
                          @Nullable ITransientNotificationCallback textCallback) {
    ...
    //同步Toast队列
    synchronized (mToastQueue) {
        int callingPid = Binder.getCallingPid();
        final long callingId = Binder.clearCallingIdentity();
        try {
            ToastRecord record;
            int index = indexOfToastLocked(pkg, token);
            // 如果它已经在队列中,我们就地更新它,我们不会将它移动到队列的末尾。
            if (index >= 0) {
                record = mToastQueue.get(index);
                record.update(duration);
            } else {
                // 限制任何给定包可以排队的 toast 数量。 防止 DOS 攻击并处理泄漏。
                //MAX_PACKAGE_TOASTS 为5,所以限制每个应用在队列中只能保持5个ToastRcord
                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_TOASTS) {
                            Slog.e(TAG, "Package has already queued " + count
                                   + " toasts. Not showing more. Package=" + pkg);
                            return;
                        }
                    }
                }
				//产生一个窗口令牌,Toast拿到这个令牌之后才能创建系统级的Window
                Binder windowToken = new Binder();
                //注意传入的type类型是TYPE_TOAST,系统级type
                mWindowManagerInternal.addWindowToken(windowToken, TYPE_TOAST, displayId,
                                                      null /* options */);
                //创建toastRecord并加入队列
                record = getToastRecord(callingUid, callingPid, pkg, isSystemToast, token,
                                        text, callback, duration, windowToken, displayId, textCallback);
                mToastQueue.add(record);
                index = mToastQueue.size() - 1;
                keepProcessAliveForToastIfNeededLocked(callingPid);
            }
            if (index == 0) {
                //如果当前的正好处在第一位,则直接展示。
                showNextToastLocked(false);
            }
        } finally {
            Binder.restoreCallingIdentity(callingId);
        }
    }
}

调用到了enqueueToast方法,创建windowToken,而windowToken的类型是TYPE_TOAST。随后将创建好的ToastRecord插入队列。接下来调用showNextToastLocked

    NotificationManagerService
	void showNextToastLocked(boolean lastToastWasTextRecord) {
        ...
		//获取到ToastRecord
        ToastRecord record = mToastQueue.get(0);
        while (record != null) {
            int userId = UserHandle.getUserId(record.uid);
            boolean rateLimitingEnabled =
                    !mToastRateLimitingDisabledUids.contains(record.uid);
            boolean isWithinQuota = mToastRateLimiter.isWithinQuota(userId, record.pkg, TOAST_QUOTA_TAG)
                            || isExemptFromRateLimiting(record.pkg, userId);
            boolean isPackageInForeground = isPackageInForegroundForToast(record.uid);
			//showToast 
            if (tryShowToast(record, rateLimitingEnabled, isWithinQuota, isPackageInForeground)) {
                //到达指定时间后,隐藏toast
                scheduleDurationReachedLocked(record, lastToastWasTextRecord);
                mIsCurrentToastShown = true;
                if (rateLimitingEnabled && !isPackageInForeground) {
                    mToastRateLimiter.noteEvent(userId, record.pkg, TOAST_QUOTA_TAG);
                }
                return;
            }
			...
        }
    }

下面我们分别来查看展示和隐藏:

展示

    NotificationManagerService
	private boolean tryShowToast(ToastRecord record, boolean rateLimitingEnabled,
            boolean isWithinQuota, boolean isPackageInForeground) {
       	...
       	//这里会调用到SystemUi,具体为ToastUI的showToast方法
        return record.show();
    }
	ToastUI
    public void showToast(int uid, String packageName, IBinder token, CharSequence text,
            IBinder windowToken, int duration, @Nullable ITransientNotificationCallback callback) {
        Runnable showToastRunnable = () -> {
            UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
            Context context = mContext.createContextAsUser(userHandle, 0);
            //获取toast,在这里会进行布局填充。
            mToast = mToastFactory.createToast(mContext /* sysuiContext */, text, packageName,
                    userHandle.getIdentifier(), mOrientation);
			//动画
            if (mToast.getInAnimation() != null) {
                mToast.getInAnimation().start();
            }

            mCallback = callback;
            //在ToastPresenter的构造函数中,会初始化mWindowManager,获取到系统的context.getSystemService(WindowManager.class)
            mPresenter = new ToastPresenter(context, mIAccessibilityManager,
                    mNotificationManager, packageName);
            //设置为受信任的覆盖,以便触摸可以通过 toast
            mPresenter.getLayoutParams().setTrustedOverlay();
            mToastLogger.logOnShowToast(uid, packageName, text.toString(), token.toString());
            //展示,向WindowManager添加View
            mPresenter.show(mToast.getView(), token, windowToken, duration, mToast.getGravity(),
                    mToast.getXOffset(), mToast.getYOffset(), mToast.getHorizontalMargin(),
                    mToast.getVerticalMargin(), mCallback, mToast.hasCustomAnimation());
        };
        ...
    }
	ToastPresenter
    public void show(View view, IBinder token, IBinder windowToken, int duration, int gravity,
            int xOffset, int yOffset, float horizontalMargin, float verticalMargin,
            @Nullable ITransientNotificationCallback callback, boolean removeWindowAnimations) {
        ...
        mView = view;
        mToken = token;
		//设置LayoutParams,设置LayoutParams为windowToken,以及其他布局相关信息
        adjustLayoutParams(mParams, windowToken, duration, gravity, xOffset, yOffset,
                horizontalMargin, verticalMargin, removeWindowAnimations);
        //向WindowManager添加view
        addToastView();
        trySendAccessibilityEvent(mView, mPackageName);
        if (callback != null) {
            try {
                callback.onToastShown();
            } catch (RemoteException e) {
                Log.w(TAG, "Error calling back " + mPackageName + " to notify onToastShow()", e);
            }
        }
    }
	//向WindowManager添加view
    private void addToastView() {
        if (mView.getParent() != null) {
            mWindowManager.removeView(mView);
        }
        try {
            //添加view
            mWindowManager.addView(mView, mParams);
        } catch (WindowManager.BadTokenException e) {
            return;
        }
    }

这里获取到由NotificationManagerService产生的具有系统权限的token。之后将自己的视图添加上去。

展示完毕就是取消了,这里也是调用到了ToastUI

取消

    private void scheduleDurationReachedLocked(ToastRecord r, boolean lastToastWasTextRecord)
    {
        mHandler.removeCallbacksAndMessages(r);
        Message m = Message.obtain(mHandler, MESSAGE_DURATION_REACHED, r);
        int delay = r.getDuration() == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
        ...
        //根据延迟时间,通过handler延迟发送取消操作。
        mHandler.sendMessageDelayed(m, delay);
    }
	
	case MESSAGE_DURATION_REACHED:
		//进行取消调用
        handleDurationReached((ToastRecord) msg.obj);
        break;
    private void handleDurationReached(ToastRecord record)
    {
        ...
        synchronized (mToastQueue) {
            int index = indexOfToastLocked(record.pkg, record.token);
            if (index >= 0) {
                //真正的取消
                cancelToastLocked(index);
            }
        }
    }
    void cancelToastLocked(int index) {
        ToastRecord record = mToastQueue.get(index);
        //依旧是调用到了ToastUI,然后进行的hideToast,这里不再进行分析。
        record.hide();
        //接下来会将当前的ToastRecord移除队列、删除分配的token。
        ...
    	if (mToastQueue.size() > 0) {
            // 继续展示下一个
            showNextToastLocked(lastToast instanceof TextToastRecord);
        }
    }
	

以上就是SDK31整个toast的调用流程了。

接下来看一看,文章开头的问题:

  1. 想在子线程调用toast应该怎么处理?

    需要在子线程创建Looper,然后调用Toast才可以展示。

  2. toastwindow是什么,为什么回到桌面依旧会显示呢?

    通过在NotificationManagerService分配的token,类型为系统级,故创建了系统级的窗口,用于展示。

接下来封装一个可以在任何线程调用的Toast工具类吧!

/**
 * pumpkin
 * 封装,可以在任何线程使用的toast
 */
object ToastUtil {
    /**
     * toastShort
     */
    fun toastShort(str: String) {
        toast(str, Toast.LENGTH_SHORT)
    }

    /**
     * toastLong
     */
    fun toastLong(str: String) {
        toast(str, Toast.LENGTH_LONG)
    }

    /**
     * 可以在任何线程toast
     */
    private fun toast(str: String, duration: Int) {
        var myLooper: Looper? = null
        if (Looper.myLooper() == null) {
            Looper.prepare()
            myLooper = Looper.myLooper()
        }
		//注意:这里的AppUtil.application,换成自己的application
        Toast.makeText(AppUtil.application, str, duration).show()
        if (myLooper != null) {
            Looper.loop()
            //直接结束掉循环,防止内存泄漏
            myLooper.quit()
        }
    }
}
//使用
binding.btToast.setOnClickListener {
    ToastUtil.toastShort("主线程toast!!!")
    Thread {
        //异步toast
        ToastUtil.toastShort("异步线程toast!!!线程名字:${Thread.currentThread().name}")
    }.start()
}

创作不易,如有帮助一键三连咯🙆‍♀️。欢迎技术探讨噢!

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

pumpkin的玄学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值