博主做了一个小游戏平台,有意思,欢迎参观。
Android 窗口机制 SDK31源码分析 总目录
- 初识
- DecorView与SubDecor的创建加载
- Window与Window Manager的创建加载
- ViewRootImpl的创建以及视图真正加载
- ViewRootImpl的事件分发
- 一定要在主线程才可以更新UI吗?为什么?
- Activity的Token和Dialog的关系
- Toast机制 - 封装可以在任何线程调用的toast
- 总结
本章节带大家了解一下toast机制,并且简单封装一个可以在任何线程中使用的toast。
带着以下几个问题,我们去看源码:
- 想在子线程调用
toast
应该怎么处理? toast
的window
是什么,为什么回到桌面依旧会显示呢?
源码分析
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,然后进行show
orhiden
,这里不再是了),下面会介绍到。
好,下面继续分析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
的调用流程了。
接下来看一看,文章开头的问题:
-
想在子线程调用
toast
应该怎么处理?需要在子线程创建Looper,然后调用Toast才可以展示。
-
toast
的window
是什么,为什么回到桌面依旧会显示呢?通过在
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()
}
创作不易,如有帮助一键三连咯🙆♀️。欢迎技术探讨噢!