Toast源码分析(Android 11)

Posted guangdeshishe

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Toast源码分析(Android 11)相关的知识,希望对你有一定的参考价值。

基于android R(11) ,targetSdkVersion 30源码分析

基本用法

  • 普通Toast:
Toast.makeText(this@MainActivity, "hello Toast!", Toast.LENGTH_SHORT).show()
  • 自定义View的Toast:
val toast = Toast(this@MainActivity)
val view = Button(this@MainActivity)
view.text = "custom toast!"
toast.setView(view)
toast.show()
  • 不同版本差异:
    • Android R以及以上:
      • Toast的setView方法标记了过期,使用了该方法的自定义Toast,如果当前APP不在前台则不会显示;也就是以后慢慢会禁止自定义Toast的View,官方建议使用Snackbar代替自定义View的Toast
    • Android Q以及以上:
      • 普通Toast由系统绘制显示,相当于显示一个系统组件;
      • 自定义View的Toast由客户端处理显示/隐藏
    • Android Q以下:不管是普通Toast还是自定义View的Toast,全部都由客户端处理显示/隐藏

源码分析

Toast.java关键源码:

public class Toast {
	private View mNextView;//Toast自定义View
	private CharSequence mText;//显示的文案
	int mDuration;//显示时长
	private final ToastPresenter mPresenter;//辅助类,Toast类型的Window添加和移除等
	
	class TN extends ITransientNotification.Stub {
		TN(){
			mHandler = new Handler(looper, null) {
				@Override
				public void handleMessage(Message msg) {
					switch (msg.what) {
						case SHOW: {
							IBinder token = (IBinder) msg.obj;
							handleShow(token);
							break;
						}
						case HIDE: {
							handleHide();
							break;
						}
					}
				}
			};
		}
		public void handleShow(IBinder windowToken) {
			mPresenter.show(mView, mToken, windowToken, mDuration, mGravity, mX, mY,
					mHorizontalMargin, mVerticalMargin,
					new CallbackBinder(getCallbacks(), mHandler));
        }

        @UnsupportedAppUsage
        public void handleHide() {
                mPresenter.hide(new CallbackBinder(getCallbacks(), mHandler));
        }
		public void show(IBinder windowToken) {
            mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
        }
		public void hide() {
            mHandler.obtainMessage(HIDE).sendToTarget();
        }
	}
	public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
            @NonNull CharSequence text, @Duration int duration) {
        if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
            Toast result = new Toast(context, looper);
            result.mText = text;
            result.mDuration = duration;
            return result;
        } else {
            Toast result = new Toast(context, looper);
            View v = ToastPresenter.getTextToastView(context, text);
            result.mNextView = v;
            result.mDuration = duration;

            return result;
        }
    }
	@Deprecated
	public void setView(View view) {
        mNextView = view;
    }
	public void show() {
		INotificationManager service = getService();
        try {
            if (Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM)) {
                if (mNextView != null) {
                    // It's a custom toast
                    service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
                } else {
                    // It's a text toast
                    ITransientNotificationCallback callback =
                            new CallbackBinder(mCallbacks, mHandler);
                    service.enqueueTextToast(pkg, mToken, mText, mDuration, displayId, callback);
                }
            } else {
                service.enqueueToast(pkg, mToken, tn, mDuration, displayId);
            }
        } catch (RemoteException e) {
            // Empty
        }
    }
}
  • mNextView:Toast自定义的View,AndroidQ以下默认是使用系统布局,Q以及以上则默认为null,由系统去显示
  • TN类:继承自Binder,自定义Toast的VIew时供NotificationManagerService调用,用于客户端控制Toast的显示/隐藏
  • Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM):可以简单的理解为判断是否Android Q以及以上系统

通过Toast.java源码可以看出:

Toast调用show方法显示时,并没有直接调用Toast的handleShow()方法 ;而是调用NotificationManagerService的相关方法,将Toast放入某个队列当中;
这是因为Toast每次只能显示一个,每个Toast显示时长也不一样,而且每个APP都可以显示很多个Toast,不同应用之间需要通过NMS统一调度,否则所有Toast各自显示各自的就乱了。

  • NotificationManagerService.enqueueToast:用于自定义View的Toast显示,NMS通过TN对象来回调客户端方法控制
  • NotificationManagerService.enqueueTextToast:用于默认普通Toast显示,使用的是系统组件去显示

可以看到自定义View的Toast最后是通过ToastPresenter.java的show和hide方法控制显示的。

ToastPresenter.java关键源码

public class 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) {
        if (mView.getParent() != null) {
            mWindowManager.removeView(mView);
        }
        try {
            mWindowManager.addView(mView, mParams);
        } catch (WindowManager.BadTokenException e) {
            return;
        }

        if (callback != null) {
            try {
                callback.onToastShown();
            } catch (RemoteException e) {
                Log.w(TAG, "Error calling back " + mPackageName + " to notify onToastShow()", e);
            }
        }
    }

    /**
     * Hides toast that was shown using {@link #show(View, IBinder, IBinder, int,
     * int, int, int, float, float, ITransientNotificationCallback)}.
     *
     * <p>This method has to be called on the same thread on which {@link #show(View, IBinder,
     * IBinder, int, int, int, int, float, float, ITransientNotificationCallback)} was called.
     */
    public void hide(@Nullable ITransientNotificationCallback callback) {
        if (mView.getParent() != null) {
            mWindowManager.removeViewImmediate(mView);
        }
        try {
            mNotificationManager.finishToken(mPackageName, mToken);
        } catch (RemoteException e) {
            Log.w(TAG, "Error finishing toast window token from package " + mPackageName, e);
        }
        if (callback != null) {
            try {
                callback.onToastHidden();
            } catch (RemoteException e) {
                Log.w(TAG, "Error calling back " + mPackageName + " to notify onToastHide()", e);
            }
        }
    }
}
  • ToastPresenter.java中的mView其实就是Toast.java中的mNextView,也就是用户自定义的View
  • 可以看出Toast的显示和隐藏,本质上也就是通过WindowManager的addView和removeView方法完成的

NotificationManagerService主要源码

public class NotificationManagerService extends SystemService {
	final ArrayList<ToastRecord> mToastQueue = new ArrayList<>();//存放所有待显示的Toast
	static final int MAX_PACKAGE_NOTIFICATIONS = 25;//每个应用最多同时只能有25个Toast在队列中
	final IBinder mService = new INotificationManager.Stub() {
		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) {

            synchronized (mToastQueue) {
                try {
                    ToastRecord record;
                    int index = indexOfToastLocked(pkg, token);
                    // If it's already in the queue, we update it in place, we don't
                    // move it to the end of the queue.
                    if (index >= 0) {//判断Toast是否已经存在,存在则更新它的显示时长
                        record = mToastQueue.get(index);
                        record.update(duration);
                    } else {
                        // Limit the number of toasts that any given package can enqueue.
                        // Prevents DOS attacks and deals with leaks.
                        int count = 0;
                        final int N = mToastQueue.size();
						//判断该APP是否有超过最大Toast个数限制
                        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;
                                }
                            }
                        }

                        Binder windowToken = new Binder();
                        mWindowManagerInternal.addWindowToken(windowToken, TYPE_TOAST, displayId);
						//将Toast封装成ToastRecord;如果是自定义View则封装成CustomToastRecord,否则TextToastRecord
                        record = getToastRecord(callingUid, callingPid, pkg, token, text, callback,
                                duration, windowToken, displayId, textCallback);
						//加入队列中
                        mToastQueue.add(record);
                        index = mToastQueue.size() - 1;
                        keepProcessAliveForToastIfNeededLocked(callingPid);
                    }
                    // If it's at index 0, it's the current toast.  It doesn't matter if it's
                    // new or just been updated, show it.
                    // If the callback fails, this will remove it from the list, so don't
                    // assume that it's valid after this.
					//如果当前队列中只有一个Toast,则立刻显示
                    if (index == 0) {
                        showNextToastLocked();
                    }
                }
            }
        }
	}
	void showNextToastLocked() {
        ToastRecord record = mToastQueue.get(0);//获取队列中第一个Toast用于显示
        while (record != null) {
            if (record.show()) {//调用Toast显示的方法
                scheduleDurationReachedLocked(record);//显示成功,则通过Handler发送延迟消息去隐藏Toast
                return;
            }
            int index = mToastQueue.indexOf(record);
            if (index >= 0) {//显示失败则移除队列
                mToastQueue.remove(index);
            }
			//获取下一个Toast
            record = (mToastQueue.size() > 0) ? mToastQueue.get(0) : null;
        }
    }
	
}

主要流程: