android源码学习-Toast实现原理讲解
Toast的全流程讲解,包含原生Toast和自定义Toast两种方式。Toast相关的常问的知识点以及使用Toast过程中频繁遇到的问题和解决方案。
前言:
前些日志QQ群有朋友发了一个Toast的崩溃日志。Toast如此简单的用法怎么会崩溃呢?所以顺便就学习了一下Toast在源码中的实现,不算复杂,但内容挺多的,这里就来分享一下,方便读者。
一.基本使用方式
主要有两种实现方式:
1.最基本的使用方式:
使用方式很简单,直接沟通过静态方法构传入context,显示内容以及显示时长三个参数,构造Toast对象,然后通过show显示。
Toast toast = Toast.makeText(getBaseContext(), "显示内容", Toast.LENGTH_LONG);
toast.show();
2.自定义View的实现方式
这种使用使用方式也很简单,首先构造一个View,然后通过setView方法传入这个自定义View,最终也是通过show方法显示。
View selfToastView = View.inflate(getBaseContext(), R.layout.self_toast, null);
Toast toast = Toast.makeText(getBaseContext(), "显示内容", Toast.LENGTH_LONG);
toast.setView(selfToastView);
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:显示时间较长,为3.5S。其3500ms的值定义在NotificationManagerService.LONG_DELAY。
Toast.LENGTH_SHORT:显示时间较短,为2S。其2000ms的值定义在NotificationManagerService.SHORT_DELAY。
但是真实显示的时间,却不是3.5和2S,实际显示时间会比这两个时间更长一些,这个后面会讲。
最终生成一个Toast对象返回,这里需要注意的是,原生Toast和自定义View的Toast的唯一区别就是原生Toast对象中mNextView对象为null。
public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
@NonNull CharSequence text, @Duration int duration) {
//这里默认配置为true,走上面这个判断逻辑
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;
}
}
然后Toast的构造方法如下,主要是构建几个后门需要使用到的对象:
public Toast(@NonNull Context context, @Nullable Looper looper) {
mContext = context;
mToken = new Binder();
looper = getLooper(looper);
mHandler = new Handler(looper);
mCallbacks = new ArrayList<>();
mTN = new TN(context, context.getPackageName(), mToken,
mCallbacks, looper);
mTN.mY = context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.toast_y_offset);
mTN.mGravity = context.getResources().getInteger(
com.android.internal.R.integer.config_toastDefaultGravity);
}
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方法,这个我们会在第四章讲解。
public void show() {
...
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
final int displayId = mContext.getDisplayId();
...
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);
}
}
...
}
三.Toast显示的完整流程
3.1 Service中接收
上文讲到通过binder传输,此时NotificationManagerService中的mService对象中的enqueueTextToast()方法会接收到通知,具体参数解释如下:
/**
*
* @param pkg 包名
* @param token APP端binder
* @param text 显示内容
* @param duration 持续时间
* @param displayId 标记唯一显示区域的ID,对应的实体类是DisplayContent
* @param callback 跨进程的callBack对象,自定义View的Toast有值。默认的Toast方法为null
*/
@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);
}
这个方法会传递到enqueueToast方法(这里稍微扩展一下,其实自定义View的Toast也会走到这个方法)。
3.2 enqueueToast方法中处理队列逻辑
我们都知道,Toast显示是有时序的,先调用的Toast一定会先展示,所以这就需要一个集合来维护这个先后的关系,而这个集合就是mToastQueue。
final ArrayList<ToastRecord> mToastQueue = new ArrayList<>();
上一小节的流程进入enqueueToast方法后,其实主要分为两块逻辑,核心代码如下:
private void enqueueToast(String pkg, IBinder token, @Nullable CharSequence text,
@Nullable ITransientNotification callback, int duration, int displayId,
@Nullable ITransientNotificationCallback textCallback) {
...
//上面的内容都是做参数合法性检查
final int callingUid = Binder.getCallingUid();
...
//此方法做权限检查
if (!checkCanEnqueueToast(pkg, callingUid, isAppRenderedToast, isSystemToast)) {
return;
}
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
final long callingId = Binder.clearCallingIdentity();
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) {
record = mToastQueue.get(index);
record.update(duration);
} else {
//插入逻辑
...
}
...
if (index == 0) {
showNextToastLocked(false);
}
}
...
}
}
这个方法中,首先我们看到了加锁的代码:synchronized (mToastQueue),所以说明这是一个多线程的场景。binder机制中,作为server端会有一个线程池来处理client发过来的binder请求,每个请求都会分配一个线程去处理,所以这里才会有多线程的加锁逻辑。
方法中如下逻辑分为以下两块:
1.首先做参数合法性检查以及权限检查,
2.然后进入队列逻辑。
队列逻辑中,首先根据pkg和token通过indexOfToastLocked方法判断在集合中是否存在。
int index = indexOfToastLocked(pkg, token);
如果index>=0,则说明mToastQueue中已经存在了传入APP所对应的binder对象,则直接更新所对应的record的持续时间。
indexOfToastLocked方法如下:
int indexOfToastLocked(String pkg, IBinder token) {
ArrayList<ToastRecord> list = mToastQueue;
int len = list.size();
for (int i=0; i<len; i++) {
ToastRecord r = list.get(i);
if (r.pkg.equals(pkg) && r.token == token) {
return i;
}
}
return -1;
}
是通过循环便利的方式来进行判断的,效率略微有些低,这里略微吐槽一下源码,也许使用TreeMap会是一个更好的选择(key=pkg+token.hashcode)。当然,google也是是考虑到Toast排队的场景较少,所才选择使用ArrayList。
由于每个Toast都对应一个binder对象,所以如果toast是复用的,则短时间内多次调用show放,也只会对应同一个Record对象,所以也只会显示一次。
如果index<0,则说明mToastQueue不存在该toast所对应的binder,则进入插入的逻辑。
3.3 插入逻辑
插入逻辑的代码如下
} 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();
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;
}
}
}
Binder windowToken = new Binder();
mWindowManagerInternal.addWindowToken(windowToken, TYPE_TOAST, displayId,
null /* options */);
record = getToastRecord(callingUid, callingPid, pkg, isSystemToast, 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.
if (index == 0) {
showNextToastLocked(false);
}
首先判断同一个包名下是否已经存在了5条(含)以上的未显示Toast,如果有则不允许继续添加。
否则,通过getToastRecord方法生成一个ToastRecord对象加入到集合最尾端,并且通过keepProcessAliveForToastIfNeededLocked方法保证弹Toast的进程不被杀死,如果当前只有一条记录的话,则直接调用showNextToastLocke方法进行显示。
ToastRecord其实是一个抽象方法,它有两个实现类,TextToastRecord和CustomToastRecord。getToastRecord方法中会根据callback是否为空来进行对应的生成,其中callback==null时生成的是TextToastRecord类型对象。
3.4 生产者消费者模型
这里又涉及到生产者消费者模式了,既然APP端通过binder方法向mToastQueue集合中插入数据,那么就一定有消费者来消费。而这个消费者就是showNextToastLocked方法。
由于上面所说的加锁逻辑,所以永远只会有一个线程在执行showNextToastLocked方法。
方法如下:
void showNextToastLocked(boolean lastToastWasTextRecord) {
if (mIsCurrentToastShown) {
return; // Don't show the same toast twice.
}
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);
if (tryShowToast(
record, rateLimitingEnabled, isWithinQuota, isPackageInForeground)) {
scheduleDurationReachedLocked(record, lastToastWasTextRecord);
mIsCurrentToastShown = true;
if (rateLimitingEnabled && !isPackageInForeground) {
mToastRateLimiter.noteEvent(userId, record.pkg, TOAST_QUOTA_TAG);
}
return;
}
int index = mToastQueue.indexOf(record);
if (index >= 0) {
mToastQueue.remove(index);
}
record = (mToastQueue.size() > 0) ? mToastQueue.get(0) : null;
}
}
代码虽较长,但核心逻辑只有三块:
1.按照先后顺序便利mToastQueue集合,取出record对象。
2.通过tryShowToast方法尝试显示record对象。如果成功,则执行scheduleDurationReachedLocked方法。
3.如果失败,则从集合中删除。就是说如果Toast显示时如果失败了也不会再次尝试。
tryShowToast的逻辑我们下一小节会讲,这里看一下scheduleDurationReachedLocked的实现:
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;
//通过无障碍辅助功能修正这个delay值,如果开始无障碍辅助的话,事件会比正常值要长一些
delay = mAccessibilityManager.getRecommendedTimeoutMillis(delay,
AccessibilityManager.FLAG_CONTENT_TEXT);
//如果上一个Toast还在显示,则流出来上一个Toast的离场动画事件。
if (lastToastWasTextRecord) {
delay += 250; // delay to account for previous toast's "out" animation
}
//如果是TextToastRecord类型,则流出来动画进场时间。
if (r instanceof TextToastRecord) {
delay += 333; // delay to account for this toast's "in" animation
}
mHandler.sendMessageDelayed(m, delay);
}
首先从Looper中的mQueue中删除带当前TaskRecord对象的Message,
然后从对象池中重新生成一个带TaskRecord对象的Message,加入到延时任务中。延时时间恰好就是duration中设置的2S或者3.5S。
另外还要流出来进场和出场的动画时间,所以最终的延时时间会比2S或者3.5S要长。这一块的逻辑其实就是删除Toast的,所以,这里的延时时间变长了,就会导致最终的实际时间要比要比设置值更长一些。
handler在时间到了之后,会执行MESSAGE_DURATION_REACHED类型的事件,调用handleDurationReached方法,该方法中又回调用cancelToastLocked方法:
void cancelToastLocked(int index) {
//1.回调APP层TN的hide方法进行通知;
ToastRecord record = mToastQueue.get(index);
record.hide();
if (index == 0) {
mIsCurrentToastShown = false;
}
//2.对队列中删除ToastRecorde对象
ToastRecord lastToast = mToastQueue.remove(index);
//3.删除在WMS中的注册的WindowToken
mWindowManagerInternal.removeWindowToken(lastToast.windowToken, false /* removeWindows */,
lastToast.displayId);
//4.再发一个延时信号确保token删除完成
scheduleKillTokenTimeout(lastToast);
//5.确保Toast显示过程中进程不会被杀死
keepProcessAliveForToastIfNeededLocked(record.pid);
//6.集合中如果还有消息,就继续执行
if (mToastQueue.size() > 0) {
// Show the next one. If the callback fails, this will remove
// it from the list, so don't assume that the list hasn't changed
// after this point.
showNextToastLocked(lastToast instanceof TextToastRecord);
}
}
主要执行了以下几段逻辑:
1.回调APP层TN的hide方法进行通知;(后续逻辑4.3小节会讲)
2.对队列中删除ToastRecorde对象
3.删除在WMS中的注册的WindowToken
4.再发一个延时信号确保token删除完成
5.确保Toast显示过程中进程不会被杀死
6.集合中如果还有消息,就继续执行
3.5 tryShowToast方法尝试显示
该方法也是很简单的,进行相关逻辑判断是否可以显示,如果可以直接调用record.show方法。
private boolean tryShowToast(ToastRecord record, boolean rateLimitingEnabled,
boolean isWithinQuota, boolean isPackageInForeground) {
if (rateLimitingEnabled && !isWithinQuota && !isPackageInForeground) {
reportCompatRateLimitingToastsChange(record.uid);
Slog.w(TAG, "Package " + record.pkg + " is above allowed toast quota, the "
+ "following toast was blocked and discarded: " + record);
return false;
}
if (blockToast(record.uid, record.isSystemToast, record.isAppRendered(),
isPackageInForeground)) {
Slog.w(TAG, "Blocking custom toast from package " + record.pkg
+ " due to package not in the foreground at the time of showing the toast");
return false;
}
return record.show();
}
这时候,我们就要看ToastRecord的show方法了。之前说了,有两种类实现,分别是TextToastRecord和CustomToastRecord两种类型。CustomToastRecord中的类型就是自定义View的Toast,我们下一章专门来讲,这里我们只讲TextToastRecord的类型。
这里我们先看TextToastRecord类型的实现:
@Override
public boolean show() {
...
mStatusBar.showToast(uid, pkg, token, text, windowToken, getDuration(), mCallback);
return true;
}
这里的mStatusBar又是一个注册的service,其实现类在StatusBarManagerService.java中:
private final StatusBarManagerInternal mInternalService = new StatusBarManagerInternal() {}
我们直接看其showToast方法:
public void showToast(int uid, String packageName, IBinder token, CharSequence text,
IBinder windowToken, int duration,
@Nullable ITransientNotificationCallback callback) {
if (mBar != null) {
try {
mBar.showToast(uid, packageName, token, text, windowToken, duration, callback);
} catch (RemoteException ex) { }
}
}
这里的mBar其实是一个binder的引用,其server的实现在SystemUI进程中,实现类是CommandQueue。所以,最终会转交到CommandQueue的showToast方法中进行处理:
@Override
public void showToast(int uid, String packageName, IBinder token, CharSequence text,
IBinder windowToken, int duration, @Nullable ITransientNotificationCallback callback) {
synchronized (mLock) {
SomeArgs args = SomeArgs.obtain();
args.arg1 = packageName;
args.arg2 = token;
args.arg3 = text;
args.arg4 = windowToken;
args.arg5 = callback;
args.argi1 = uid;
args.argi2 = duration;
mHandler.obtainMessage(MSG_SHOW_TOAST, args).sendToTarget();
}
}
通过handler转发到主线程,代码如下:
case MSG_SHOW_TOAST: {
args = (SomeArgs) msg.obj;
String packageName = (String) args.arg1;
IBinder token = (IBinder) args.arg2;
CharSequence text = (CharSequence) args.arg3;
IBinder windowToken = (IBinder) args.arg4;
ITransientNotificationCallback callback =
(ITransientNotificationCallback) args.arg5;
int uid = args.argi1;
int duration = args.argi2;
for (Callbacks callbacks : mCallbacks) {
callbacks.showToast(uid, packageName, token, text, windowToken, duration,
callback);
}
break;
}
主线程中通过mCallBacks回调显示,这里的Callbacks的实现在com.android.systemui.toast.ToastUIjava类中。
3.6 ToastUI.showToast完成显示流程
showToast方法中中,委托给ToastPresenter进行逻辑的显示,经典的MVP架构。
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);
mToast = mToastFactory.createToast(mContext /* sysuiContext */, text, packageName,
userHandle.getIdentifier(), mOrientation);
if (mToast.getInAnimation() != null) {
mToast.getInAnimation().start();
}
mCallback = callback;
mPresenter = new ToastPresenter(context, mIAccessibilityManager,
mNotificationManager, packageName);
// Set as trusted overlay so touches can pass through toasts
mPresenter.getLayoutParams().setTrustedOverlay();
mToastLogger.logOnShowToast(uid, packageName, text.toString(), token.toString());
mPresenter.show(mToast.getView(), token, windowToken, duration, mToast.getGravity(),
mToast.getXOffset(), mToast.getYOffset(), mToast.getHorizontalMargin(),
mToast.getVerticalMargin(), mCallback, mToast.hasCustomAnimation());
};
if (mToastOutAnimatorListener != null) {
// if we're currently animating out a toast, show new toast after prev toast is hidden
mToastOutAnimatorListener.setShowNextToastRunnable(showToastRunnable);
} else if (mPresenter != null) {
// if there's a toast already showing that we haven't tried hiding yet, hide it and
// then show the next toast after its hidden animation is done
hideCurrentToast(showToastRunnable);
} else {
// else, show this next toast immediately
showToastRunnable.run();
}
}
3.7 ToastPresenter.show()方法完成最终Toast的显示
所以接下来我们就要看ToastPresenter中的show方法了,也是最终在该方法中完成了普通Toast的展示。方法如下:
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) {
checkState(mView == null, "Only one toast at a time is allowed, call hide() first.");
mView = view;
mToken = token;
adjustLayoutParams(mParams, windowToken, duration, gravity, xOffset, yOffset,
horizontalMargin, verticalMargin, removeWindowAnimations);
addToastView();
trySendAccessibilityEvent(mView, mPackageName);
if (callback != null) {
try {
callback.onToastShown();
} catch (RemoteException e) {
Log.w(TAG, "Error calling back " + mPackageName + " to notify onToastShow()", e);
}
}
}
该方法中,主要还是做了两件事:
1.设置mParams中的属性值。
2.添加到windowManager中完成最终的显示。
我们接下来分开来讲。
3.8 adjustLayoutParams方法配置mParams参数
首先,根据传入的参数调整mParams中的属性值,该属性值决定Toast显示的位置,以及显示时间等等,方法如下:
private void adjustLayoutParams(WindowManager.LayoutParams params, IBinder windowToken,
int duration, int gravity, int xOffset, int yOffset, float horizontalMargin,
float verticalMargin, boolean removeWindowAnimations) {
Configuration config = mResources.getConfiguration();
int absGravity = Gravity.getAbsoluteGravity(gravity, config.getLayoutDirection());
params.gravity = absGravity;
if ((absGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
params.horizontalWeight = 1.0f;
}
if ((absGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
params.verticalWeight = 1.0f;
}
params.x = xOffset;
params.y = yOffset;
params.horizontalMargin = horizontalMargin;
params.verticalMargin = verticalMargin;
params.packageName = mContext.getPackageName();
params.hideTimeoutMilliseconds =
(duration == Toast.LENGTH_LONG) ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
params.token = windowToken;
if (removeWindowAnimations && params.windowAnimations == R.style.Animation_Toast) {
params.windowAnimations = 0;
}
}
这里我们看一下hideTimeoutMilliseconds参数,就是这个来控制最终的显示时间的,当然,这个时间是显示的最长时间,实际情况下,3.4小节中有讲到,在延时时间到了最后,会发通知销毁Toast,所以,hideTimeoutMilliseconds在绝大场景下并不会生效。
SHORT_DURATION_TIMEOUT和LONG_DURATION_TIMEOUT的设置在代码中设置如下:
private static final long SHORT_DURATION_TIMEOUT = 4000;
private static final long LONG_DURATION_TIMEOUT = 7000;
这里需要额外说明一点,params的配置的参数在构造方法中也有一部分:
private WindowManager.LayoutParams createLayoutParams() {
WindowManager.LayoutParams params = new WindowManager.LayoutParams();
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
params.width = WindowManager.LayoutParams.WRAP_CONTENT;
params.format = PixelFormat.TRANSLUCENT;
params.windowAnimations = R.style.Animation_Toast;
params.type = WindowManager.LayoutParams.TYPE_TOAST;
params.setFitInsetsIgnoringVisibility(true);
params.setTitle(WINDOW_TITLE);
params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
setShowForAllUsersIfApplicable(params, mPackageName);
return params;
}
我们这里重点看下面这一行
params.type=WindowManager.LayoutParams.TYPE_TOAST;
在安卓中,type代表window的优先层级,数字越大代表优先级越高,就会盖在上面显示。TYPE_TOAST=2005,而Activity所对应的Window优先级是最低的,其所对应的type=1,所以Toast会在Activity的上面显示。
具体代码参考如下:
public static final int TYPE_BASE_APPLICATION = 1;
public static final int FIRST_SYSTEM_WINDOW = 2000;
public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;
所以
TYPE_TOAST = 2005
TYPE_BASE_APPLICATION = 1
//Activity中设置的type参数的代码,代码在ActivityThread的handleResumeActivity方法中
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
addToastView方法添加到windowManager中
方法内容如下,这里就比较简单了,直接添加到windowManager上。
private void addToastView() {
if (mView.getParent() != null) {
mWindowManager.removeView(mView);
}
try {
mWindowManager.addView(mView, mParams);
} catch (WindowManager.BadTokenException e) {
// 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.
Log.w(TAG, "Error while attempting to show toast from " + mPackageName, e);
return;
}
}
需要注意的是,第三种从service中的binder接收开始,代码是执行在NoticationManagerService所属的SystemServer进程,以及ToastPresenter所在的SystemUI进程,都不是APP进程,所以如果在显示了Toast后立马杀掉APP进程,Toast仍然会正常显示。
Toast超时隐藏流程
toast有显示,有windowManager.addView的流程,那么等到持续时间一到,自然有隐藏的Toast的流程。
既然讲到这里,那就不得不讲一下addView之后的流程,主要流程如下:
所以最终会调用WindowManagerService的addWindow方法中。
整个方法流程太长了,所以我们只看和Toast相关的这一部分,代码中会注册一个延时消息,而延时的时间恰恰就是之前设置到mParams中的hideTimeoutMilliseconds,也就是我们上面所说的4S或者7S。
public int addWindow(Session session, IWindow client, LayoutParams attrs, int viewVisibility,
int displayId, int requestUserId, InsetsVisibilities requestedVisibilities,
InputChannel outInputChannel, InsetsState outInsetsState,
InsetsSourceControl[] outActiveControls) {
...
if (type == TYPE_TOAST) {
if (!displayContent.canAddToastWindowForUid(callingUid)) {
ProtoLog.w(WM_ERROR, "Adding more than one toast window for UID at a time.");
return WindowManagerGlobal.ADD_DUPLICATE_ADD;
}
// Make sure this happens before we moved focus as one can make the
// toast focusable to force it not being hidden after the timeout.
// Focusable toasts are always timed out to prevent a focused app to
// show a focusable toasts while it has focus which will be kept on
// the screen after the activity goes away.
if (addToastWindowRequiresToken
|| (attrs.flags & FLAG_NOT_FOCUSABLE) == 0
|| displayContent.mCurrentFocus == null
|| displayContent.mCurrentFocus.mOwnerUid != callingUid) {
mH.sendMessageDelayed(
mH.obtainMessage(H.WINDOW_HIDE_TIMEOUT, win),
win.mAttrs.hideTimeoutMilliseconds);
}
}
...
return res;
}
所以接下来我们就要处理H.WINDOW_HIDE_TIMEOUT事件的代码:
case WINDOW_HIDE_TIMEOUT: {
final WindowState window = (WindowState) msg.obj;
synchronized (mGlobalLock) {
...
window.mAttrs.flags &= ~FLAG_KEEP_SCREEN_ON;
window.hidePermanentlyLw();
window.setDisplayLayoutNeeded();
mWindowPlacerLocked.performSurfacePlacement();
}
break;
}
然后WindowState.java的hidePermanentlyLw方法如如下,通过hide方法去实现隐藏,所以隐藏的流程是不需要客户端或者NotificationManagerService来控制的,而是WMS自己来维护的。
public void hidePermanentlyLw() {
if (!mPermanentlyHidden) {
mPermanentlyHidden = true;
hide(true /* doAnimation */, true /* requestAnim */);
}
}
至于hide隐藏,或者show展示的流程,我们这里不展开讲了,这一块其实属于View的完整显示流程中的内容,会有另外的文章专门来讲。这里我们只需要知道,把Window注册到WMS中后,并不是立马显示的,而是在下一个Vsync信号来临时执行的渲染流程并最终显示到屏幕上的就好了。
四.自定义View的Toast流程讲解
4.1转发到APP层执行逻辑
上文讲到,自定义View的实现类型是CustomToastRecord,其show()方法如下:
public boolean show() {
...
callback.show(windowToken);
...
}
就是简单的完成了callback的show回调。而这个callback又是binder对象,其实现是APP侧Toast中TN对象。所以我们接着看一下TN中的show方法:
public void show(IBinder windowToken) {
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
通过mHander从binder线程转发事件到Toast所绑定的looper的线程进行处理(一般是主线程,但并不绝对是)。handler中会执行handleShow方法,代码如下:
public void handleShow(IBinder windowToken) {
...
//如果已经传入了取消和隐藏的信号,那就没必要继续显示了
if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
return;
}
//mNextView是自定义的View,而mView是上一次显示的内容(如果Toast复用的话)
if (mView != mNextView) {
// remove the old view if necessary
handleHide();
mView = mNextView;
mPresenter.show(mView, mToken, windowToken, mDuration, mGravity, mX, mY,
mHorizontalMargin, mVerticalMargin,
new CallbackBinder(getCallbacks(), mHandler));
}
}
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进行处理
public void handleHide() {
if (mView != null) {
...
mPresenter.hide(new CallbackBinder(getCallbacks(), mHandler));
mView = null;
}
}
ToastPresenter中hide方法如下:
public void hide(@Nullable ITransientNotificationCallback callback) {
checkState(mView != null, "No toast to hide.");
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);
}
}
mView = null;
mToken = null;
}
具体代码如下,主要执行了以下的流程:
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()方法中:
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()");
}
如果子线程默认当前线程是不会绑定looper的,所以会报错。那么如果我们子线程中初始化Looper呢?那就可以了,只不过最终实现上还有一些区别。TextToastRecord最终仍然会在SystemServer进程中被add到WindowManager中,而CustomToastRecord类型的最终会在APP侧的子线程中显示。另外要注意,prepare一定要和loop方法搭配使用才可以,如下:
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
Toast toast = Toast.makeText(getBaseContext(), "显示内容", Toast.LENGTH_LONG);
toast.show();
Looper.loop();
}
}).start();
2.为什么有时候显示Toast会提示要打开通知权限?
上面3.2小节中有讲到,显示Toast之前会进行权限检查,代码如下:
private boolean checkCanEnqueueToast(String pkg, int callingUid,
boolean isAppRenderedToast, boolean isSystemToast) {
//当前APP是否被挂起
final boolean isPackageSuspended = isPackagePaused(pkg);
//当前APP是否有通知权限android.Manifest.permission.INTERACT_ACROSS_USERS
final boolean notificationsDisabledForPackage = !areNotificationsEnabledForPackage(pkg,
callingUid);
//APP是否在后台
final boolean appIsForeground;
final long callingIdentity = Binder.clearCallingIdentity();
try {
appIsForeground = mActivityManager.getUidImportance(callingUid)
== IMPORTANCE_FOREGROUND;
} finally {
Binder.restoreCallingIdentity(callingIdentity);
}
//首先在非系统Toast情况下,APP进程在后台并且没有INTERACT_ACROSS_USERS权限,或者APP进程被挂起,都不会显示
if (!isSystemToast && ((notificationsDisabledForPackage && !appIsForeground)
|| isPackageSuspended)) {
Slog.e(TAG, "Suppressing toast from package " + pkg
+ (isPackageSuspended ? " due to package suspended."
: " by user request."));
return false;
}
//上一个自定义toast卡住了
if (blockToast(callingUid, isSystemToast, isAppRenderedToast,
isPackageInForegroundForToast(callingUid))) {
Slog.w(TAG, "Blocking custom toast from package " + pkg
+ " due to package not in the foreground at time the toast was posted");
return false;
}
return true;
}
总结一下:
首先在非系统Toast情况下,APP进程在后台并且没有INTERACT_ACROSS_USERS权限,或者APP进程被挂起,都不会显示Toast。
也就是说,如果有INTERACT_ACROSS_USERS权限就可以在后台显示Toast了。
3.先调用show的Toast一定会先显示吗?
其实这道题问的有点没有意义,一般来说是Toast越先调用show方法会越早显示。
但是也有一些特殊情况,比如两个进程或者两个线程中,A先执行Toast,但是卡住了没有立马执行到显示流程。这时候B线程中也只执行了一次show方法。过了100毫秒,A又执行了一次show。
大体流程如下
A线程中: toast.show()
B线程中: toast.show()
//100号秒后
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 错误解决
具体报错如下:
java.lang.IllegalArgumentException: View=androidx.recyclerview.widget.RecyclerView{adb693 VFED..... ........ 0,0-1080,1548 #7f0800d1 app:id/recycler} not attached to window manager
at android.view.WindowManagerGlobal.findViewLocked(WindowManagerGlobal.java:534)
at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:438)
at android.view.WindowManagerImpl.removeView(WindowManagerImpl.java:157)
at android.widget.ToastPresenter.addToastView(ToastPresenter.java:303)
at android.widget.ToastPresenter.show(ToastPresenter.java:231)
at android.widget.ToastPresenter.show(ToastPresenter.java:214)
at android.widget.Toast$TN.handleShow(Toast.java:699)
at android.widget.Toast$TN$1.handleMessage(Toast.java:631)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loopOnce(Looper.java:201)
at android.os.Looper.loop(Looper.java:288)
首先我们看addToastView方法:
if (mView.getParent() != null) {
mWindowManager.removeView(mView);
}
如果mView.getParent不为空,则去WindowManager中removeView该view。
然后在看WindowManagerGlobal中findViewLocked方法:
private int findViewLocked(View view, boolean required) {
final int index = mViews.indexOf(view);
if (required && index < 0) {
throw new IllegalArgumentException("View=" + view + " not attached to window manager");
}
return index;
}
会去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 错误解决
具体报错如下:
java.lang.IllegalArgumentException: View=android.widget.LinearLayout{12b02f3 V.E...... ......ID 0,52-629,368} not attached to window manager
at android.view.WindowManagerGlobal.findViewLocked(WindowManagerGlobal.java:572)
at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:476)
at android.view.WindowManagerImpl.removeViewImmediate(WindowManagerImpl.java:144)
at android.widget.ToastPresenter.hide(ToastPresenter.java:230)
at android.widget.Toast$TN.handleHide(Toast.java:826)
at android.widget.Toast$TN$1.handleMessage(Toast.java:746)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:236)
at com.xxx.NeverCrash$1.run(NeverCrash.java:39)
at android.os.Handler.handleCallback(Handler.java:938)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:236)
at android.app.ActivityThread.main(ActivityThread.java:8060)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:656)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:967)
我们可以看流程图中画红圈的部分:
当tryShowToast返回true时,就一定会走到ToastPresenter的hide流程。但是tryShowToast返回true的时候一定会显示成功吗?
上面的流程图中我们可以知道tryShowToast的返回值是有CustomToastRecord的show()方法返回,我们看一下这个方法:
@Override
public boolean show() {
...
try {
callback.show(windowToken);
return true;
} catch (RemoteException e) {
...
mNotificationManager.keepProcessAliveForToastIfNeeded(pid);
return false;
}
}
只有binder通讯失败的时候才会返回false,其余都返回true。而最终APP层一定能WindowManager.addView()成功吗?其答案自然是否定的。
我们在看最终显示的ToastPresenter.addToastView方法:
private void addToastView() {
if (mView.getParent() != null) {
mWindowManager.removeView(mView);
}
try {
mWindowManager.addView(mView, mParams);
} catch (WindowManager.BadTokenException e) {
return;
}
}
也就是说既然添加失败了也没有任何处理。
所以也就是说,如果显示自定义Toast的时候,如果因为某种原因导致最终addView失败,那么等到时间到了,就会导致上面所说的崩溃。这个可以理解为源码中存在的问题,show的时候做了保护,但是hide的时候并未做任何的保护。
那么如何解决这种问题呢?既然是系统的问题,解决起来还是比较麻烦的。
我的想法是这这样的:既然是show的时候try catch了,那么hide的时候能否也进行try catch呢?
我们可以通过反射的时候拿到Toast.TN中的Handler对象,然后通过对其进行代理来实现我们先要的效果。
首先,通过反射拿到Toast.TN中的Handler对象mHandler,然后在创建一个和其同looper的Handler对象,通过反射替换掉原来的mHandler。
自定义的Handler实现伪代码如下:
Handler oldHandler = null;//反射拿到的原来的handler
Handler mHandler = new Handler(oldHandler.getLooper(), null) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 0: {
oldHandler.obtainMessage(0, msg.obj).sendToTarget();
break;
}
case 1: {
try{
//反射调用handleHide();方法
}catch (Exception e){
e.printStackTrace();
}
//反射把mNextView = null;
break;
}
case 2: {
//和1的流程类似
}
}
}
};
总体来说,因为过多的使用反射,效率不高,但是理论上确实能解决Toast问题问题。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)