• android源码学习-Toast实现原理讲解


    前言:

    前些日志QQ群有朋友发了一个Toast的崩溃日志。Toast如此简单的用法怎么会崩溃呢?所以顺便就学习了一下Toast在源码中的实现,不算复杂,但内容挺多的,这里就来分享一下,方便读者。

    一.基本使用方式

    主要有两种实现方式:

    1.最基本的使用方式:

    使用方式很简单,直接沟通过静态方法构传入context,显示内容以及显示时长三个参数,构造Toast对象,然后通过show显示。

    1. Toast toast = Toast.makeText(getBaseContext(), "显示内容", Toast.LENGTH_LONG);
    2. toast.show();

    2.自定义View的实现方式

    这种使用使用方式也很简单,首先构造一个View,然后通过setView方法传入这个自定义View,最终也是通过show方法显示。

    1. View selfToastView = View.inflate(getBaseContext(), R.layout.self_toast, null);
    2. Toast toast = Toast.makeText(getBaseContext(), "显示内容", Toast.LENGTH_LONG);
    3. toast.setView(selfToastView);
    4. toast.show();

    3.使用方式总结

    两种使用方式都很简单,区别只是第二种方式多传入了一个自定义View而已。但是为什么要分开来讲呢?因为虽然使用时仅仅只差一步,但是其实现原理是完全不一样的。一个是通过NotificationManagerService去显示的,而另外一个则是APP自身处理的。接下来,我们就依次的讲一下两种使用方式的实现原理。

    二.Toast的创建显示流程原理讲解

    1.Toast.makeText

    这个的实现方式还是比较简单的,最终的生成方式传入4个参数,分别为

    Context:绑定的上下文对象

    Looper:绑定线程的Looper,可以为空。为空时则默认使用当前线程的looper。PS:每个线程都只能绑定唯一的一个Looper,想了解这一块的可以看我的另外一篇文章:android源码学习-Handler机制及其六个核心点_失落夏天的博客-CSDN博客_安卓开发handler机制

    text:显示内容

    duration:持续时间。有两种参数:
    Toast.LENGTH_LONG:显示时间较长,为7S。
    Toast.LENGTH_SHORT:显示时间较短,为4S。

    这两个时间长度是不是和你们认知有区别?是不是还以为是2.5和3.5S呢?从某个版本开始,Toast的时间确实已经改成了4S和7S了,后面会讲到这两个时间的具体设置的地方。

    最终生成一个Toast对象返回,这里需要注意的是,原生Toast和自定义View的Toast的唯一区别就是原生Toast对象中mNextView对象为null。

    1. public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
    2. @NonNull CharSequence text, @Duration int duration) {
    3. //这里默认配置为true,走上面这个判断逻辑
    4. if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
    5. Toast result = new Toast(context, looper);
    6. result.mText = text;
    7. result.mDuration = duration;
    8. return result;
    9. } else {
    10. Toast result = new Toast(context, looper);
    11. View v = ToastPresenter.getTextToastView(context, text);
    12. result.mNextView = v;
    13. result.mDuration = duration;
    14. return result;
    15. }
    16. }

    然后Toast的构造方法如下,主要是构建几个后门需要使用到的对象:

    1. public Toast(@NonNull Context context, @Nullable Looper looper) {
    2. mContext = context;
    3. mToken = new Binder();
    4. looper = getLooper(looper);
    5. mHandler = new Handler(looper);
    6. mCallbacks = new ArrayList<>();
    7. mTN = new TN(context, context.getPackageName(), mToken,
    8. mCallbacks, looper);
    9. mTN.mY = context.getResources().getDimensionPixelSize(
    10. com.android.internal.R.dimen.toast_y_offset);
    11. mTN.mGravity = context.getResources().getInteger(
    12. com.android.internal.R.integer.config_toastDefaultGravity);
    13. }

    mContext:Context对象

    mToken:构造binder对象,后面和NotificationManagerService通信都是通过这个binder。

    looper:当前Toast绑定的线程looper,传入null时默认为当前线程。

    mTN:Binder.Stub类型对象,作为binder的client端。其接受跨进程传递过来的信息时是在单独的binder线程中处理的。

    mTN.mY:纵坐标偏移量,简单来说就是控制Toast在屏幕中显示位置是靠上一点还是靠下一点的。

    mTN.mGravity:控制Toast的显示位置。一般是局中,靠下两种。

    2.Toast.show()方法

    show方法中主要是执行三段逻辑,

    首先把Toast的mNextView赋值给tn.mNextView,如果Toast的mNextView为null,那么tn.mNextView自然也是null;

    然后获取NotificationManager的binder引用service;

    接下来走一个判断逻辑,

    1.如果mNextView==null时,走service.enqueueToast逻辑,通过binder跨进程通讯,会调用到NotificationManagerService中的enqueueToast方法,这个我们会在第三章讲解。

    2.如果mNextView!=null时,通过调用service.enqueueTextToast方法,通过binder跨进程通讯,会调用到NotificationManagerService中的enqueueTextToast方法,这个我们会在第四章讲解。

    1. public void show() {
    2. ...
    3. INotificationManager service = getService();
    4. String pkg = mContext.getOpPackageName();
    5. TN tn = mTN;
    6. tn.mNextView = mNextView;
    7. final int displayId = mContext.getDisplayId();
    8. ...
    9. if (mNextView != null) {
    10. // It's a custom toast
    11. //自定义的方式第四章讲解
    12. service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
    13. } else {
    14. // It's a text toast
    15. //默认方式第三章讲解
    16. ITransientNotificationCallback callback =
    17. new CallbackBinder(mCallbacks, mHandler);
    18. service.enqueueTextToast(pkg, mToken, mText, mDuration, displayId, callback);
    19. }
    20. }
    21. ...
    22. }

    三.Toast显示的完整流程

    3.1 Service中接收

    上文讲到通过binder传输,此时NotificationManagerService中的mService对象中的enqueueTextToast()方法会接收到通知,具体参数解释如下:

    1. /**
    2. *
    3. * @param pkg 包名
    4. * @param token APP端binder
    5. * @param text 显示内容
    6. * @param duration 持续时间
    7. * @param displayId 标记唯一显示区域的ID,对应的实体类是DisplayContent
    8. * @param callback 跨进程的callBack对象,自定义View的Toast有值。默认的Toast方法为null
    9. */
    10. @Override
    11. public void enqueueTextToast(String pkg, IBinder token, CharSequence text, int duration,
    12. int displayId, @Nullable ITransientNotificationCallback callback) {
    13. enqueueToast(pkg, token, text, null, duration, displayId, callback);
    14. }

    这个方法会传递到enqueueToast方法(这里稍微扩展一下,其实自定义View的Toast也会走到这个方法)。

    3.2 enqueueToast方法中处理队列逻辑

    我们都知道,Toast显示是有时序的,先调用的Toast一定会先展示,所以这就需要一个集合来维护这个先后的关系,而这个集合就是mToastQueue。

    final ArrayList<ToastRecord> mToastQueue = new ArrayList<>();

    上一小节的流程进入enqueueToast方法后,其实主要分为两块逻辑,核心代码如下:

    1. private void enqueueToast(String pkg, IBinder token, @Nullable CharSequence text,
    2. @Nullable ITransientNotification callback, int duration, int displayId,
    3. @Nullable ITransientNotificationCallback textCallback) {
    4. ...
    5. //上面的内容都是做参数合法性检查
    6. final int callingUid = Binder.getCallingUid();
    7. ...
    8. //此方法做权限检查
    9. if (!checkCanEnqueueToast(pkg, callingUid, isAppRenderedToast, isSystemToast)) {
    10. return;
    11. }
    12. synchronized (mToastQueue) {
    13. int callingPid = Binder.getCallingPid();
    14. final long callingId = Binder.clearCallingIdentity();
    15. try {
    16. ToastRecord record;
    17. int index = indexOfToastLocked(pkg, token);
    18. // If it's already in the queue, we update it in place, we don't
    19. // move it to the end of the queue.
    20. if (index >= 0) {
    21. record = mToastQueue.get(index);
    22. record.update(duration);
    23. } else {
    24. //插入逻辑
    25. ...
    26. }
    27. ...
    28. if (index == 0) {
    29. showNextToastLocked(false);
    30. }
    31. }
    32. ...
    33. }
    34. }

    这个方法中,首先我们看到了加锁的代码:synchronized (mToastQueue),所以说明这是一个多线程的场景。binder机制中,作为server端会有一个线程池来处理client发过来的binder请求,每个请求都会分配一个线程去处理,所以这里才会有多线程的加锁逻辑。

    方法中如下逻辑分为以下两块:

    1.首先做参数合法性检查以及权限检查,

    2.然后进入队列逻辑。

    队列逻辑中,首先根据pkg和token通过indexOfToastLocked方法判断在集合中是否存在。

    int index = indexOfToastLocked(pkg, token);

    如果index>=0,则说明mToastQueue中已经存在了传入APP所对应的binder对象,则直接更新所对应的record的持续时间。

    indexOfToastLocked方法如下:

    1. int indexOfToastLocked(String pkg, IBinder token) {
    2. ArrayList list = mToastQueue;
    3. int len = list.size();
    4. for (int i=0; i
    5. ToastRecord r = list.get(i);
    6. if (r.pkg.equals(pkg) && r.token == token) {
    7. return i;
    8. }
    9. }
    10. return -1;
    11. }

    是通过循环便利的方式来进行判断的,效率略微有些低,这里略微吐槽一下源码,也许使用TreeMap会是一个更好的选择(key=pkg+token.hashcode)。当然,google也是是考虑到Toast排队的场景较少,所才选择使用ArrayList。

    由于每个Toast都对应一个binder对象,所以如果toast是复用的,则短时间内多次调用show放,也只会对应同一个Record对象,所以也只会显示一次。

    如果index<0,则说明mToastQueue不存在该toast所对应的binder,则进入插入的逻辑。

    3.3 插入逻辑

    插入逻辑的代码如下

    1. } else {
    2. // Limit the number of toasts that any given package can enqueue.
    3. // Prevents DOS attacks and deals with leaks.
    4. int count = 0;
    5. final int N = mToastQueue.size();
    6. for (int i = 0; i < N; i++) {
    7. final ToastRecord r = mToastQueue.get(i);
    8. if (r.pkg.equals(pkg)) {
    9. count++;
    10. if (count >= MAX_PACKAGE_TOASTS) {
    11. Slog.e(TAG, "Package has already queued " + count
    12. + " toasts. Not showing more. Package=" + pkg);
    13. return;
    14. }
    15. }
    16. }
    17. Binder windowToken = new Binder();
    18. mWindowManagerInternal.addWindowToken(windowToken, TYPE_TOAST, displayId,
    19. null /* options */);
    20. record = getToastRecord(callingUid, callingPid, pkg, isSystemToast, token,
    21. text, callback, duration, windowToken, displayId, textCallback);
    22. mToastQueue.add(record);
    23. index = mToastQueue.size() - 1;
    24. keepProcessAliveForToastIfNeededLocked(callingPid);
    25. }
    26. // If it's at index 0, it's the current toast. It doesn't matter if it's
    27. // new or just been updated, show it.
    28. // If the callback fails, this will remove it from the list, so don't
    29. // assume that it's valid after this.
    30. if (index == 0) {
    31. showNextToastLocked(false);
    32. }

    首先判断同一个包名下是否已经存在了5条(含)以上的未显示Toast,如果有则不允许继续添加。

    否则,通过getToastRecord方法生成一个ToastRecord对象加入到集合最尾端,并且通过keepProcessAliveForToastIfNeededLocked方法保证弹Toast的进程不被杀死,如果当前只有一条记录的话,则直接调用showNextToastLocke方法进行显示。

    ToastRecord其实是一个抽象方法,它有两个实现类,TextToastRecord和CustomToastRecord。getToastRecord方法中会根据callback是否为空来进行对应的生成,其中callback==null时生成的是TextToastRecord类型对象。

    3.4 生产者消费者模型

    这里又涉及到生产者消费者模式了,既然APP端通过binder方法向mToastQueue集合中插入数据,那么就一定有消费者来消费。而这个消费者就是showNextToastLocked方法。

    由于上面所说的加锁逻辑,所以永远只会有一个线程在执行showNextToastLocked方法。

    方法如下:

    1. void showNextToastLocked(boolean lastToastWasTextRecord) {
    2. if (mIsCurrentToastShown) {
    3. return; // Don't show the same toast twice.
    4. }
    5. ToastRecord record = mToastQueue.get(0);
    6. while (record != null) {
    7. int userId = UserHandle.getUserId(record.uid);
    8. boolean rateLimitingEnabled =
    9. !mToastRateLimitingDisabledUids.contains(record.uid);
    10. boolean isWithinQuota =
    11. mToastRateLimiter.isWithinQuota(userId, record.pkg, TOAST_QUOTA_TAG)
    12. || isExemptFromRateLimiting(record.pkg, userId);
    13. boolean isPackageInForeground = isPackageInForegroundForToast(record.uid);
    14. if (tryShowToast(
    15. record, rateLimitingEnabled, isWithinQuota, isPackageInForeground)) {
    16. scheduleDurationReachedLocked(record, lastToastWasTextRecord);
    17. mIsCurrentToastShown = true;
    18. if (rateLimitingEnabled && !isPackageInForeground) {
    19. mToastRateLimiter.noteEvent(userId, record.pkg, TOAST_QUOTA_TAG);
    20. }
    21. return;
    22. }
    23. int index = mToastQueue.indexOf(record);
    24. if (index >= 0) {
    25. mToastQueue.remove(index);
    26. }
    27. record = (mToastQueue.size() > 0) ? mToastQueue.get(0) : null;
    28. }
    29. }

    代码虽较长,但核心逻辑只有三块:

    1.按照先后顺序便利mToastQueue集合,取出record对象。

    2.通过tryShowToast方法尝试显示record对象。如果成功,则执行scheduleDurationReachedLocked方法。

    3.如果失败,则从集合中删除。就是说如果Toast显示时如果失败了也不会再次尝试。

    tryShowToast的逻辑我们下一小节会讲,这里看一下scheduleDurationReachedLocked的实现:

    1. private void scheduleDurationReachedLocked(ToastRecord r, boolean lastToastWasTextRecord)
    2. {
    3. mHandler.removeCallbacksAndMessages(r);
    4. Message m = Message.obtain(mHandler, MESSAGE_DURATION_REACHED, r);
    5. int delay = r.getDuration() == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
    6. //通过无障碍辅助功能修正这个delay值,如果开始无障碍辅助的话,事件会比正常值要长一些
    7. delay = mAccessibilityManager.getRecommendedTimeoutMillis(delay,
    8. AccessibilityManager.FLAG_CONTENT_TEXT);
    9. //如果上一个Toast还在显示,则流出来上一个Toast的离场动画事件。
    10. if (lastToastWasTextRecord) {
    11. delay += 250; // delay to account for previous toast's "out" animation
    12. }
    13. //如果是TextToastRecord类型,则流出来动画进场时间。
    14. if (r instanceof TextToastRecord) {
    15. delay += 333; // delay to account for this toast's "in" animation
    16. }
    17. mHandler.sendMessageDelayed(m, delay);
    18. }

    首先从Looper中的mQueue中删除带当前TaskRecord对象的Message,

    然后从对象池中重新生成一个带TaskRecord对象的Message,加入到延时任务中。延时时间恰好就是duration中设置的4S或者7S。

    handler在时间到了之后,会执行MESSAGE_DURATION_REACHED类型的事件,调用handleDurationReached方法,该方法中又回调用cancelToastLocked方法:

    1. void cancelToastLocked(int index) {
    2. //1.回调APP层TN的hide方法进行通知;
    3. ToastRecord record = mToastQueue.get(index);
    4. record.hide();
    5. if (index == 0) {
    6. mIsCurrentToastShown = false;
    7. }
    8. //2.对队列中删除ToastRecorde对象
    9. ToastRecord lastToast = mToastQueue.remove(index);
    10. //3.删除在WMS中的注册的WindowToken
    11. mWindowManagerInternal.removeWindowToken(lastToast.windowToken, false /* removeWindows */,
    12. lastToast.displayId);
    13. //4.再发一个延时信号确保token删除完成
    14. scheduleKillTokenTimeout(lastToast);
    15. //5.确保Toast显示过程中进程不会被杀死
    16. keepProcessAliveForToastIfNeededLocked(record.pid);
    17. //6.集合中如果还有消息,就继续执行
    18. if (mToastQueue.size() > 0) {
    19. // Show the next one. If the callback fails, this will remove
    20. // it from the list, so don't assume that the list hasn't changed
    21. // after this point.
    22. showNextToastLocked(lastToast instanceof TextToastRecord);
    23. }
    24. }

    主要执行了以下几段逻辑:

    1.回调APP层TN的hide方法进行通知;(后续逻辑4.3小节会讲)

    2.对队列中删除ToastRecorde对象

    3.删除在WMS中的注册的WindowToken

    4.再发一个延时信号确保token删除完成

    5.确保Toast显示过程中进程不会被杀死

    6.集合中如果还有消息,就继续执行

    3.5 tryShowToast方法尝试显示

    该方法也是很简单的,进行相关逻辑判断是否可以显示,如果可以直接调用record.show方法。

    1. private boolean tryShowToast(ToastRecord record, boolean rateLimitingEnabled,
    2. boolean isWithinQuota, boolean isPackageInForeground) {
    3. if (rateLimitingEnabled && !isWithinQuota && !isPackageInForeground) {
    4. reportCompatRateLimitingToastsChange(record.uid);
    5. Slog.w(TAG, "Package " + record.pkg + " is above allowed toast quota, the "
    6. + "following toast was blocked and discarded: " + record);
    7. return false;
    8. }
    9. if (blockToast(record.uid, record.isSystemToast, record.isAppRendered(),
    10. isPackageInForeground)) {
    11. Slog.w(TAG, "Blocking custom toast from package " + record.pkg
    12. + " due to package not in the foreground at the time of showing the toast");
    13. return false;
    14. }
    15. return record.show();
    16. }

    这时候,我们就要看ToastRecord的show方法了。之前说了,有两种类实现,分别是TextToastRecord和CustomToastRecord两种类型。CustomToastRecord中的类型就是自定义View的Toast,我们下一章专门来讲,这里我们只讲TextToastRecord的类型。

    这里我们先看TextToastRecord类型的实现:

    1. @Override
    2. public boolean show() {
    3. ...
    4. mStatusBar.showToast(uid, pkg, token, text, windowToken, getDuration(), mCallback);
    5. return true;
    6. }

    这里的mStatusBar又是一个注册的service,其实现类在StatusBarManagerService.java中:

     private final StatusBarManagerInternal mInternalService = new StatusBarManagerInternal() {}

    我们直接看其showToast方法:

    1. public void showToast(int uid, String packageName, IBinder token, CharSequence text,
    2. IBinder windowToken, int duration,
    3. @Nullable ITransientNotificationCallback callback) {
    4. if (mBar != null) {
    5. try {
    6. mBar.showToast(uid, packageName, token, text, windowToken, duration, callback);
    7. } catch (RemoteException ex) { }
    8. }
    9. }

    又是一层转发逻辑,转交到CommandQueue的showToast方法中:

    1. @Override
    2. public void showToast(int uid, String packageName, IBinder token, CharSequence text,
    3. IBinder windowToken, int duration, @Nullable ITransientNotificationCallback callback) {
    4. synchronized (mLock) {
    5. SomeArgs args = SomeArgs.obtain();
    6. args.arg1 = packageName;
    7. args.arg2 = token;
    8. args.arg3 = text;
    9. args.arg4 = windowToken;
    10. args.arg5 = callback;
    11. args.argi1 = uid;
    12. args.argi2 = duration;
    13. mHandler.obtainMessage(MSG_SHOW_TOAST, args).sendToTarget();
    14. }
    15. }

    通过handler转发到主线程,代码如下:

    1. case MSG_SHOW_TOAST: {
    2. args = (SomeArgs) msg.obj;
    3. String packageName = (String) args.arg1;
    4. IBinder token = (IBinder) args.arg2;
    5. CharSequence text = (CharSequence) args.arg3;
    6. IBinder windowToken = (IBinder) args.arg4;
    7. ITransientNotificationCallback callback =
    8. (ITransientNotificationCallback) args.arg5;
    9. int uid = args.argi1;
    10. int duration = args.argi2;
    11. for (Callbacks callbacks : mCallbacks) {
    12. callbacks.showToast(uid, packageName, token, text, windowToken, duration,
    13. callback);
    14. }
    15. break;
    16. }

    主线程中通过mCallBacks回调显示,这里的Callbacks的实现在com.android.systemui.toast.ToastUtils.java类中。

    3.6 ToastU.showToast完成显示流程

    showToast方法中中,委托给ToastPresenter进行逻辑的显示,经典的MVP架构。

    1. public void showToast(int uid, String packageName, IBinder token, CharSequence text,
    2. IBinder windowToken, int duration, @Nullable ITransientNotificationCallback callback) {
    3. Runnable showToastRunnable = () -> {
    4. UserHandle userHandle = UserHandle.getUserHandleForUid(uid);
    5. Context context = mContext.createContextAsUser(userHandle, 0);
    6. mToast = mToastFactory.createToast(mContext /* sysuiContext */, text, packageName,
    7. userHandle.getIdentifier(), mOrientation);
    8. if (mToast.getInAnimation() != null) {
    9. mToast.getInAnimation().start();
    10. }
    11. mCallback = callback;
    12. mPresenter = new ToastPresenter(context, mIAccessibilityManager,
    13. mNotificationManager, packageName);
    14. // Set as trusted overlay so touches can pass through toasts
    15. mPresenter.getLayoutParams().setTrustedOverlay();
    16. mToastLogger.logOnShowToast(uid, packageName, text.toString(), token.toString());
    17. mPresenter.show(mToast.getView(), token, windowToken, duration, mToast.getGravity(),
    18. mToast.getXOffset(), mToast.getYOffset(), mToast.getHorizontalMargin(),
    19. mToast.getVerticalMargin(), mCallback, mToast.hasCustomAnimation());
    20. };
    21. if (mToastOutAnimatorListener != null) {
    22. // if we're currently animating out a toast, show new toast after prev toast is hidden
    23. mToastOutAnimatorListener.setShowNextToastRunnable(showToastRunnable);
    24. } else if (mPresenter != null) {
    25. // if there's a toast already showing that we haven't tried hiding yet, hide it and
    26. // then show the next toast after its hidden animation is done
    27. hideCurrentToast(showToastRunnable);
    28. } else {
    29. // else, show this next toast immediately
    30. showToastRunnable.run();
    31. }
    32. }

    3.7 ToastPresenter.show()方法完成最终Toast的显示

    所以接下来我们就要看ToastPresenter中的show方法了,也是最终在该方法中完成了普通Toast的展示。方法如下:

    1. public void show(View view, IBinder token, IBinder windowToken, int duration, int gravity,
    2. int xOffset, int yOffset, float horizontalMargin, float verticalMargin,
    3. @Nullable ITransientNotificationCallback callback, boolean removeWindowAnimations) {
    4. checkState(mView == null, "Only one toast at a time is allowed, call hide() first.");
    5. mView = view;
    6. mToken = token;
    7. adjustLayoutParams(mParams, windowToken, duration, gravity, xOffset, yOffset,
    8. horizontalMargin, verticalMargin, removeWindowAnimations);
    9. addToastView();
    10. trySendAccessibilityEvent(mView, mPackageName);
    11. if (callback != null) {
    12. try {
    13. callback.onToastShown();
    14. } catch (RemoteException e) {
    15. Log.w(TAG, "Error calling back " + mPackageName + " to notify onToastShow()", e);
    16. }
    17. }
    18. }

    该方法中,主要还是做了两件事:

    1.设置mParams中的属性值。

    2.添加到windowManager中完成最终的显示。

    我们接下来分开来讲。

     
    

    3.8 adjustLayoutParams方法配置mParams参数

    首先,根据传入的参数调整mParams中的属性值,该属性值决定Toast显示的位置,以及显示时间等等,方法如下:

    1. private void adjustLayoutParams(WindowManager.LayoutParams params, IBinder windowToken,
    2. int duration, int gravity, int xOffset, int yOffset, float horizontalMargin,
    3. float verticalMargin, boolean removeWindowAnimations) {
    4. Configuration config = mResources.getConfiguration();
    5. int absGravity = Gravity.getAbsoluteGravity(gravity, config.getLayoutDirection());
    6. params.gravity = absGravity;
    7. if ((absGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
    8. params.horizontalWeight = 1.0f;
    9. }
    10. if ((absGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
    11. params.verticalWeight = 1.0f;
    12. }
    13. params.x = xOffset;
    14. params.y = yOffset;
    15. params.horizontalMargin = horizontalMargin;
    16. params.verticalMargin = verticalMargin;
    17. params.packageName = mContext.getPackageName();
    18. params.hideTimeoutMilliseconds =
    19. (duration == Toast.LENGTH_LONG) ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
    20. params.token = windowToken;
    21. if (removeWindowAnimations && params.windowAnimations == R.style.Animation_Toast) {
    22. params.windowAnimations = 0;
    23. }
    24. }

    这里我们看一下hideTimeoutMilliseconds参数,就是这个来控制最终的显示时间的,SHORT_DURATION_TIMEOUT和LONG_DURATION_TIMEOUT的设置在代码中设置如下:

    1. private static final long SHORT_DURATION_TIMEOUT = 4000;
    2. private static final long LONG_DURATION_TIMEOUT = 7000;

    这里需要额外说明一点,params的配置的参数在构造方法中也有一部分:

    1. private WindowManager.LayoutParams createLayoutParams() {
    2. WindowManager.LayoutParams params = new WindowManager.LayoutParams();
    3. params.height = WindowManager.LayoutParams.WRAP_CONTENT;
    4. params.width = WindowManager.LayoutParams.WRAP_CONTENT;
    5. params.format = PixelFormat.TRANSLUCENT;
    6. params.windowAnimations = R.style.Animation_Toast;
    7. params.type = WindowManager.LayoutParams.TYPE_TOAST;
    8. params.setFitInsetsIgnoringVisibility(true);
    9. params.setTitle(WINDOW_TITLE);
    10. params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
    11. | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
    12. | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
    13. setShowForAllUsersIfApplicable(params, mPackageName);
    14. return params;
    15. }

    我们这里重点看下面这一行

    params.type=WindowManager.LayoutParams.TYPE_TOAST;

    在安卓中,type代表window的优先层级,数字越大代表优先级越高,就会盖在上面显示。TYPE_TOAST=2005,而Activity所对应的Window优先级是最低的,其所对应的type=1,所以Toast会在Activity的上面显示。

    具体代码参考如下:

    1. public static final int TYPE_BASE_APPLICATION = 1;
    2. public static final int FIRST_SYSTEM_WINDOW = 2000;
    3. public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;
    4. 所以
    5. TYPE_TOAST = 2005
    6. TYPE_BASE_APPLICATION = 1
    7. //Activity中设置的type参数的代码,代码在ActivityThread的handleResumeActivity方法中
    8. l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;

    addToastView方法添加到windowManager中

    方法内容如下,这里就比较简单了,直接添加到windowManager上。

    1. private void addToastView() {
    2. if (mView.getParent() != null) {
    3. mWindowManager.removeView(mView);
    4. }
    5. try {
    6. mWindowManager.addView(mView, mParams);
    7. } catch (WindowManager.BadTokenException e) {
    8. // Since the notification manager service cancels the token right after it notifies us
    9. // to cancel the toast there is an inherent race and we may attempt to add a window
    10. // after the token has been invalidated. Let us hedge against that.
    11. Log.w(TAG, "Error while attempting to show toast from " + mPackageName, e);
    12. return;
    13. }
    14. }

    需要注意的是,第三种从service中的binder接收开始,所有的代码都是执行在NoticationManagerService所属的SystemServer进程,所以如果在显示了Toast后立马杀掉APP进程,Toast仍然会正常显示。

    Toast隐藏的流程

    toast有显示,有windowManager.addView的流程,那么等到持续时间一到,自然有隐藏的Toast的流程。

    既然讲到这里,那就不得不讲一下addView之后的流程,主要流程如下:

     所以最终会调用WindowManagerService的addWindow方法中。

    整个方法流程太长了,所以我们只看和Toast相关的这一部分,代码中会注册一个延时消息,而延时的时间恰恰就是之前设置到mParams中的hideTimeoutMilliseconds,也就是我们上面所说的4S或者7S。

    1. public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
    2. int displayId, int requestUserId, InsetsVisibilities requestedVisibilities,
    3. InputChannel outInputChannel, InsetsState outInsetsState,
    4. InsetsSourceControl[] outActiveControls) {
    5. ...
    6. if (type == TYPE_TOAST) {
    7. if (!displayContent.canAddToastWindowForUid(callingUid)) {
    8. ProtoLog.w(WM_ERROR, "Adding more than one toast window for UID at a time.");
    9. return WindowManagerGlobal.ADD_DUPLICATE_ADD;
    10. }
    11. // Make sure this happens before we moved focus as one can make the
    12. // toast focusable to force it not being hidden after the timeout.
    13. // Focusable toasts are always timed out to prevent a focused app to
    14. // show a focusable toasts while it has focus which will be kept on
    15. // the screen after the activity goes away.
    16. if (addToastWindowRequiresToken
    17. || (attrs.flags & FLAG_NOT_FOCUSABLE) == 0
    18. || displayContent.mCurrentFocus == null
    19. || displayContent.mCurrentFocus.mOwnerUid != callingUid) {
    20. mH.sendMessageDelayed(
    21. mH.obtainMessage(H.WINDOW_HIDE_TIMEOUT, win),
    22. win.mAttrs.hideTimeoutMilliseconds);
    23. }
    24. }
    25. ...
    26. return res;
    27. }

    所以接下来我们就要处理H.WINDOW_HIDE_TIMEOUT事件的代码:

    1. case WINDOW_HIDE_TIMEOUT: {
    2. final WindowState window = (WindowState) msg.obj;
    3. synchronized (mGlobalLock) {
    4. ...
    5. window.mAttrs.flags &= ~FLAG_KEEP_SCREEN_ON;
    6. window.hidePermanentlyLw();
    7. window.setDisplayLayoutNeeded();
    8. mWindowPlacerLocked.performSurfacePlacement();
    9. }
    10. break;
    11. }

    然后WindowState.java的hidePermanentlyLw方法如如下,通过hide方法去实现隐藏,所以隐藏的流程是不需要客户端或者NotificationManagerService来控制的,而是WMS自己来维护的。

    1. public void hidePermanentlyLw() {
    2. if (!mPermanentlyHidden) {
    3. mPermanentlyHidden = true;
    4. hide(true /* doAnimation */, true /* requestAnim */);
    5. }
    6. }

    至于hide隐藏,或者show展示的流程,我们这里不展开讲了,这一块其实属于View的完整显示流程中的内容,会有另外的文章专门来讲。这里我们只需要知道,把Window注册到WMS中后,并不是立马显示的,而是在下一个Vsync信号来临时执行的渲染流程并最终显示到屏幕上的就好了。

    四.自定义View的Toast流程讲解

    4.1转发到APP层执行逻辑

    上文讲到,自定义View的实现类型是CustomToastRecord,其show()方法如下:

    1. public boolean show() {
    2. ...
    3. callback.show(windowToken);
    4. ...
    5. }

    就是简单的完成了callback的show回调。而这个callback又是binder对象,其实现是APP侧Toast中TN对象。所以我们接着看一下TN中的show方法:

    1. public void show(IBinder windowToken) {
    2. mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
    3. }

    通过mHander从binder线程转发事件到Toast所绑定的looper的线程进行处理(一般是主线程,但并不绝对是)。handler中会执行handleShow方法,代码如下:

    1. public void handleShow(IBinder windowToken) {
    2. ...
    3. //如果已经传入了取消和隐藏的信号,那就没必要继续显示了
    4. if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
    5. return;
    6. }
    7. //mNextView是自定义的View,而mView是上一次显示的内容(如果Toast复用的话)
    8. if (mView != mNextView) {
    9. // remove the old view if necessary
    10. handleHide();
    11. mView = mNextView;
    12. mPresenter.show(mView, mToken, windowToken, mDuration, mGravity, mX, mY,
    13. mHorizontalMargin, mVerticalMargin,
    14. new CallbackBinder(getCallbacks(), mHandler));
    15. }
    16. }

    toast对象首次显示的话,mView==null。则后续主要分为两块逻辑:

    1.先调用handleHide隐藏当前的mView,其最终的实现也是通过ToastPresenter.hide来实现的。具体实现逻辑我们4.3中再讲

    2.通过ToastPresenter.show方法进行显示流程。

    4.2 ToastPresenter.show显示Toast

    执行ToastPresenter.show之前,首先会把mNextView设置为当前将要显示的mView。

     mView = mNextView;

    show()方法上面的3.7章节我们已经讲过了,就不再重复讲述了。唯一的区别就是这里此时的代码是在APP进程中执行的,而3.7是在SystemServer进程中。

    所以自定义的View最终也是通过WindowManager.addView的方式进行显示的。

    4.3 Toast的隐藏流程

    上面3.4小节的时候还讲到,等到设置的显示时间到了,会通过binder机制通知到Toast.TN中的hide方法。

    hide方法中通过handler从binder线程转发到looper所在的线程。

    然后Handler中交给handleHide方法进行处理,另外4.1中显示一个自定义view的Toast之前,也会调用handleHide的逻辑,handleHide的代码如下,主要是交给ToastPresenter.hide进行处理

    1. public void handleHide() {
    2. if (mView != null) {
    3. ...
    4. mPresenter.hide(new CallbackBinder(getCallbacks(), mHandler));
    5. mView = null;
    6. }
    7. }

     ToastPresenter中hide方法如下:

    1. public void hide(@Nullable ITransientNotificationCallback callback) {
    2. checkState(mView != null, "No toast to hide.");
    3. if (mView.getParent() != null) {
    4. mWindowManager.removeViewImmediate(mView);
    5. }
    6. try {
    7. mNotificationManager.finishToken(mPackageName, mToken);
    8. } catch (RemoteException e) {
    9. Log.w(TAG, "Error finishing toast window token from package " + mPackageName, e);
    10. }
    11. if (callback != null) {
    12. try {
    13. callback.onToastHidden();
    14. } catch (RemoteException e) {
    15. Log.w(TAG, "Error calling back " + mPackageName + " to notify onToastHide()",
    16. e);
    17. }
    18. }
    19. mView = null;
    20. mToken = null;
    21. }

    具体代码如下,主要执行了以下的流程:

    1.如果mView有parent的话,首先从WindowManager中删除,其中mView一定是最顶层的View。

    2.通知NotificationManagerService

    3.执行毁掉onToastHidden进行通知

    4.清空mView和mToken,因为上一个流程执行完成了。

    4.4 小节

    所以总结一下,自定义View的toast显示和隐藏,其实就类似于APP侧把一个自定义View添加到WindowManager上,然后定时时间到了之后在从WindowManager中移除该自定义View。

    五.总结

    我们来总结一下,其实Toast显示主要分为两种类型,Text类型和Custom类型。

    如果Text类型的话,最终交给SystemServer进程的负责显示,最终会交给ToastUI负责最终的显示工作。它再向WMS注册添加window的时候,会附带传入结束时候,由WMS在时间到了之后负责隐藏Window。

    而Custom类型,最终交回给APP进程负责显示,最终也是通过向WMS添加Window的方式进行显示的。此时NotificationManagerService负责记录时间,时间到了之后通知APP进程进行隐藏工作。

    Toast显示流程的主要流程图可以总结如下:

    六.几个相关问题的拓展

    1.Toast可以子线程使用吗?

    答:这个问题和子线程中是否可以更新UI有一点类似。只不过检查点和流程略微有一些区别。

    首先生成Toast对象的时候会有一个检查Toast.getLooper()方法中:

    1. private Looper getLooper(@Nullable Looper looper) {
    2. if (looper != null) {
    3. return looper;
    4. }
    5. return checkNotNull(Looper.myLooper(),
    6. "Can't toast on a thread that has not called Looper.prepare()");
    7. }

    如果子线程默认当前线程是不会绑定looper的,所以会报错。那么如果我们子线程中初始化Looper呢?那就可以了,只不过最终实现上还有一些区别。TextToastRecord最终仍然会在SystemServer进程中被add到WindowManager中,而CustomToastRecord类型的最终会在APP侧的子线程中显示。另外要注意,prepare一定要和loop方法搭配使用才可以,如下:

    1. new Thread(new Runnable() {
    2. @Override
    3. public void run() {
    4. Looper.prepare();
    5. Toast toast = Toast.makeText(getBaseContext(), "显示内容", Toast.LENGTH_LONG);
    6. toast.show();
    7. Looper.loop();
    8. }
    9. }).start();

    2.为什么有时候显示Toast会提示要打开通知权限?

    上面3.2小节中有讲到,显示Toast之前会进行权限检查,代码如下:

    1. private boolean checkCanEnqueueToast(String pkg, int callingUid,
    2. boolean isAppRenderedToast, boolean isSystemToast) {
    3. //当前APP是否被挂起
    4. final boolean isPackageSuspended = isPackagePaused(pkg);
    5. //当前APP是否有通知权限android.Manifest.permission.INTERACT_ACROSS_USERS
    6. final boolean notificationsDisabledForPackage = !areNotificationsEnabledForPackage(pkg,
    7. callingUid);
    8. //APP是否在后台
    9. final boolean appIsForeground;
    10. final long callingIdentity = Binder.clearCallingIdentity();
    11. try {
    12. appIsForeground = mActivityManager.getUidImportance(callingUid)
    13. == IMPORTANCE_FOREGROUND;
    14. } finally {
    15. Binder.restoreCallingIdentity(callingIdentity);
    16. }
    17. //首先在非系统Toast情况下,APP进程在后台并且没有INTERACT_ACROSS_USERS权限,或者APP进程被挂起,都不会显示
    18. if (!isSystemToast && ((notificationsDisabledForPackage && !appIsForeground)
    19. || isPackageSuspended)) {
    20. Slog.e(TAG, "Suppressing toast from package " + pkg
    21. + (isPackageSuspended ? " due to package suspended."
    22. : " by user request."));
    23. return false;
    24. }
    25. //上一个自定义toast卡住了
    26. if (blockToast(callingUid, isSystemToast, isAppRenderedToast,
    27. isPackageInForegroundForToast(callingUid))) {
    28. Slog.w(TAG, "Blocking custom toast from package " + pkg
    29. + " due to package not in the foreground at time the toast was posted");
    30. return false;
    31. }
    32. return true;
    33. }

    总结一下:

    首先在非系统Toast情况下,APP进程在后台并且没有INTERACT_ACROSS_USERS权限,或者APP进程被挂起,都不会显示Toast。

    也就是说,如果有INTERACT_ACROSS_USERS权限就可以在后台显示Toast了。

    3.先调用show的Toast一定会先显示吗?

    其实这道题问的有点没有意义,一般来说是Toast越先调用show方法会越早显示。

    但是也有一些特殊情况,比如两个进程或者两个线程中,A先执行Toast,但是卡住了没有立马执行到显示流程。这时候B线程中也只执行了一次show方法。过了100毫秒,A又执行了一次show。

    大体流程如下

    1. A线程中: toast.show()
    2. B线程中: toast.show()
    3. //100号秒后
    4. A线程中: toast.show()

    这种情况下说A在B前面也行,说A在B后面也行,最终仍然是A先执行。A的第二次show调用在最终显示之前只是会更新其在NMS中对应的ToastRecord对象中的参数而异。

    4.为什么Toast会显示在Activity上面,而不会被Activity覆盖?

    这个就涉及到Window的优先级的概念了。3.8小节中有细讲。

    5.显示Toast后立马杀掉进程,Toast会立马消失吗?

    不会,一样分两种场景:

    如果是默认Toast,最终addWindow和removeWindow的操作都在SystemServer进程中,自然杀掉APP进程对Toast的展示没有任何影响。(实际场景验证过)

    如果是自定义Toast,显示会在APP进程。3.3小节中有讲到,显示Toast时会调用keepProcessAliveForToastIfNeededLocked方法保证显示Toast的进程不被杀死,所以此时自定义Toast应该还是可以正常显示的。(从代码进行的推论,没有验证过,有热心的朋友可以帮忙验证下)

    6.Toast的显示时间一定是4S或者7S嘛?

    由于时间计算是在系统侧的,所以只能是4S或者7S,当然并不一定是绝对值。

    3.4小节中有讲到,获取到delay延时时间后,如果开启了无障碍辅助功能,首先会通过无障碍修正这个delay时间。其次,Toast的入场和出场动画,也都是单独计算时间的。

    所以最终显示时间会略大于4S或者7S。

    下面在举两个Toast中经常容易遇到的错误。

    7.show的时候提示not attached to window manager 错误解决

     具体报错如下:

    1. java.lang.IllegalArgumentException: View=androidx.recyclerview.widget.RecyclerView{adb693 VFED..... ........ 0,0-1080,1548 #7f0800d1 app:id/recycler} not attached to window manager
    2. at android.view.WindowManagerGlobal.findViewLocked(WindowManagerGlobal.java:534)
    3. at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:438)
    4. at android.view.WindowManagerImpl.removeView(WindowManagerImpl.java:157)
    5. at android.widget.ToastPresenter.addToastView(ToastPresenter.java:303)
    6. at android.widget.ToastPresenter.show(ToastPresenter.java:231)
    7. at android.widget.ToastPresenter.show(ToastPresenter.java:214)
    8. at android.widget.Toast$TN.handleShow(Toast.java:699)
    9. at android.widget.Toast$TN$1.handleMessage(Toast.java:631)
    10. at android.os.Handler.dispatchMessage(Handler.java:106)
    11. at android.os.Looper.loopOnce(Looper.java:201)
    12. at android.os.Looper.loop(Looper.java:288)

    首先我们看addToastView方法:

    1. if (mView.getParent() != null) {
    2. mWindowManager.removeView(mView);
    3. }

    如果mView.getParent不为空,则去WindowManager中removeView该view。

    然后在看WindowManagerGlobal中findViewLocked方法:

    1. private int findViewLocked(View view, boolean required) {
    2. final int index = mViews.indexOf(view);
    3. if (required && index < 0) {
    4. throw new IllegalArgumentException("View=" + view + " not attached to window manager");
    5. }
    6. return index;
    7. }

    会去mViews中寻找该view,如果找不到,则会抛出上文中的错误。

    mViews中保存着APP中所有注册的window的rootView。该View不在mViews中,但又有parentView,则说明该View已经绑定了parentView。

    我们在根据下面代码分析可得出结论,setView传入的view是有问题的,是已经绑定了parentView的,这种View自然不能作为rootView。

    View=androidx.recyclerview.widget.RecyclerView{adb693 VFED..... ........ 0,0-1080,1548 #7f0800d1 app:id/recycler}

    8.hide的时候提示not attached to window manager 错误解决

    具体报错如下:

    1. java.lang.IllegalArgumentException: View=android.widget.LinearLayout{12b02f3 V.E...... ......ID 0,52-629,368} not attached to window manager
    2. at android.view.WindowManagerGlobal.findViewLocked(WindowManagerGlobal.java:572)
    3. at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:476)
    4. at android.view.WindowManagerImpl.removeViewImmediate(WindowManagerImpl.java:144)
    5. at android.widget.ToastPresenter.hide(ToastPresenter.java:230)
    6. at android.widget.Toast$TN.handleHide(Toast.java:826)
    7. at android.widget.Toast$TN$1.handleMessage(Toast.java:746)
    8. at android.os.Handler.dispatchMessage(Handler.java:106)
    9. at android.os.Looper.loop(Looper.java:236)
    10. at com.xxx.NeverCrash$1.run(NeverCrash.java:39)
    11. at android.os.Handler.handleCallback(Handler.java:938)
    12. at android.os.Handler.dispatchMessage(Handler.java:99)
    13. at android.os.Looper.loop(Looper.java:236)
    14. at android.app.ActivityThread.main(ActivityThread.java:8060)
    15. at java.lang.reflect.Method.invoke(Native Method)
    16. at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:656)
    17. at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:967)

    我们可以看流程图中画红圈的部分:

     当tryShowToast返回true时,就一定会走到ToastPresenter的hide流程。但是tryShowToast返回true的时候一定会显示成功吗?

    上面的流程图中我们可以知道tryShowToast的返回值是有CustomToastRecord的show()方法返回,我们看一下这个方法:

    1. @Override
    2. public boolean show() {
    3. ...
    4. try {
    5. callback.show(windowToken);
    6. return true;
    7. } catch (RemoteException e) {
    8. ...
    9. mNotificationManager.keepProcessAliveForToastIfNeeded(pid);
    10. return false;
    11. }
    12. }

    只有binder通讯失败的时候才会返回false,其余都返回true。而最终APP层一定能WindowManager.addView()成功吗?其答案自然是否定的。

    我们在看最终显示的ToastPresenter.addToastView方法:

    1. private void addToastView() {
    2. if (mView.getParent() != null) {
    3. mWindowManager.removeView(mView);
    4. }
    5. try {
    6. mWindowManager.addView(mView, mParams);
    7. } catch (WindowManager.BadTokenException e) {
    8. return;
    9. }
    10. }

    也就是说既然添加失败了也没有任何处理。

    所以也就是说,如果显示自定义Toast的时候,如果因为某种原因导致最终addView失败,那么等到时间到了,就会导致上面所说的崩溃。这个可以理解为源码中存在的问题,show的时候做了保护,但是hide的时候并未做任何的保护。

    那么如何解决这种问题呢?既然是系统的问题,解决起来还是比较麻烦的。

    我的想法是这这样的:既然是show的时候try catch了,那么hide的时候能否也进行try catch呢?

    我们可以通过反射的时候拿到Toast.TN中的Handler对象,然后通过对其进行代理来实现我们先要的效果。

    首先,通过反射拿到Toast.TN中的Handler对象mHandler,然后在创建一个和其同looper的Handler对象,通过反射替换掉原来的mHandler。

    自定义的Handler实现伪代码如下:

    1. Handler oldHandler = null;//反射拿到的原来的handler
    2. Handler mHandler = new Handler(oldHandler.getLooper(), null) {
    3. @Override
    4. public void handleMessage(Message msg) {
    5. switch (msg.what) {
    6. case 0: {
    7. oldHandler.obtainMessage(0, msg.obj).sendToTarget();
    8. break;
    9. }
    10. case 1: {
    11. try{
    12. //反射调用handleHide();方法
    13. }catch (Exception e){
    14. e.printStackTrace();
    15. }
    16. //反射把mNextView = null;
    17. break;
    18. }
    19. case 2: {
    20. //和1的流程类似
    21. }
    22. }
    23. }
    24. };

    总体来说,因为过多的使用反射,效率不高,但是理论上确实能解决Toast问题问题。

  • 相关阅读:
    奔腾电力面试题
    安卓讲课笔记1.1 搭建开发环境
    微服务中的Feign:优雅实现远程调用的秘密武器(二)
    【Linux】Linux环境基础开发工具使用
    web前端css基本内容
    特征工程特征预处理归一化与标准化、鸢尾花种类预测代码实现
    JavaScript操作BOM
    Fastjson 1.2.24反序列化漏洞(Vulhub)使用方法
    OpenLayers实战,WebGL图层根据Feature要素的变量动态渲染多种颜色、不同长度和不同透明度的长方形(矩形)图形,适用于大量矩形图形渲染
    OpenAI抓内鬼出奇招,奥特曼耍了所有人:GPT搜索鸽了!改升级GPT-4
  • 原文地址:https://blog.csdn.net/AA5279AA/article/details/126328866