同学,你的系统Toast可能需要修复一下

当然,使用DToast你也依然可以沿用这种封装方式,但这种方式在下面这个场景中可能会无法成功展示出弹窗(该场景下原生Toast也一样无法弹出), 不过请放心不会导致应用崩溃,而且这个场景出现的概率较小,有以下几个必要条件:

1.通知栏权限被关闭(通知栏权限默认都是打开的)
2.非MIUI手机
3.你的应用设置的targetSdkVersion>=26
4.Android8.0以上的部分手机。

所以,如果你的应用targetSdkVersion>=26,又想要保证在所有场景下都能正常展示弹窗,那么请在DToast.make(context)时传入Activity作为上下文,这样在该场景下DToast会启用ActivityToast展示出弹窗。而targetSdkVersion小于26的同学可以放心使用ApplicationContext创建DToast。

想了解为什么需要区别对待targetSdkVersion26+?点击查看API26做了什么

而如果你还不了解targetSdkVersion 点击这里查看

接下来再详细分析下上面提到的五个问题:

问题一:关闭通知权限时Toast不显示

看下方Toast源码中的show()方法,通过AIDL获取到INotificationManager,并将接下来的显示流程控制权
交给NotificationManagerService。
NMS中会对Toast进行权限校验,当通知权限校验不通过时,Toast将不做展示。
当然不同ROM中NMS可能会有不同,比如MIUI就对这部分内容进行了修改,所以小米手机关闭通知权限不会导致Toast不显示。

/**

  • Show the view for the specified duration.
    */
    public void show() {
    if (mNextView == null) {
    throw new RuntimeException(“setView must have been called”);
    }

INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;

try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}

如何解决这个问题?只要能够绕过NotificationManagerService即可。

DovaToast通过使用TYPE_TOAST实现全局弹窗功能,不使用系统Toast,也没有使用NMS服务,因此不受通知权限限制。

问题二:系统Toast的队列机制在不同手机上可能会不相同

我找了四台设备,创建两个Gravity不同的Toast并调用show()方法,结果出现了四种展示效果:

  • 荣耀5C-android7.0(只看到展示第一个Toast)
  • 小米8-MIUI10(只看到展示第二个Toast,即新的Toast.show会中止当前Toast的展示)
  • 红米6pro-MIUI9(两个Toast同时展示)
  • 荣耀5C-android6.0(第一个TOAST展示完成后,第二个才开始展示)

造成这个问题的原因应该是各大厂商ROM中NMS维护Toast队列的逻辑有差异。 同样的,DToast内部也维护着自己的队列逻辑,保证在所有手机上使用DToast的效果相同。

DToast中多个弹窗连续出现时:

1.相同优先级时,会终止上一个,直接展示后一个;
2.不同优先级时,如果后一个的优先级更高则会终止上一个,直接展示后一个。

问题三:系统Toast的BadTokenException问题

  • Toast有个内部类 TN(extends ITransientNotification.Stub),调用Toast.show()时会将TN传递给NMS;

public void show() {
if (mNextView == null) {
throw new RuntimeException(“setView must have been called”);
}
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}

  • 在NMS中会生成一个windowToken,并将windowToken给到WindowManagerService,WMS会暂时保存该token并用于之后的校验;

NotificationManagerService.java #enqueueToast源码:

synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
int index = indexOfToastLocked(pkg, callback);
// 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 {
// Limit the number of toasts that any given package except the android
// package can enqueue. Prevents DOS attacks and deals with leaks.
if (!isSystemToast) {
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_NOTIFICATIONS) {
Slog.e(TAG, "Package has already posted " + count

  • " toasts. Not showing more. Package=" + pkg);
    return;
    }
    }
    }
    }

Binder token = new Binder();//生成一个token
mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
record = new ToastRecord(callingPid, pkg, callback, duration, token);
mToastQueue.add(record);
index = mToastQueue.size() - 1;
keepProcessAliveIfNeededLocked(callingPid);
}
// If it’s at index 0, it’s the current toast. It doesn’t matter if it’s
// new or just been updated. Call back and tell it to show itself.
// 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();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}

  • 然后NMS通过调用TN.show(windowToken)回传token给TN;

/**

  • schedule handleShow into the right thread
    */
    @Override
    public void show(IBinder windowToken) {
    if (localLOGV) Log.v(TAG, "SHOW: " + this);
    mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
    }

  • TN使用该token尝试向WindowManager中添加Toast视图(mParams.token = windowToken);

在API25的源码中,Toast的WindowManager.LayoutParams参数新增了一个token属性,用于对添加的窗口进行校验。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 当param.token为空时,WindowManagerImpl会为其设置 DefaultToken;

@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

private void applyDefaultToken(@NonNull ViewGroup.LayoutParams params) {
// Only use the default token if we don’t have a parent window.
if (mDefaultToken != null && mParentWindow == null) {
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException(“Params must be WindowManager.LayoutParams”);
}
// Only use the default token if we don’t already have a token.
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (wparams.token == null) {
wparams.token = mDefaultToken;
}
}
}

  • 当WindowManager收到addView请求后会检查 mParams.token 是否有效,若有效则添加窗口展示,否则抛出BadTokenException异常.

switch (res) {
case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window – token " + attrs.token

  • " is not valid; is your activity running?");
    case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
    throw new WindowManager.BadTokenException(
    "Unable to add window – token " + attrs.token
  • " is not for an application");
    case WindowManagerGlobal.ADD_APP_EXITING:
    throw new WindowManager.BadTokenException(
    "Unable to add window – app for token " + attrs.token
  • " is exiting");
    case WindowManagerGlobal.ADD_DUPLICATE_ADD:
    throw new WindowManager.BadTokenException(
    "Unable to add window – window " + mWindow
  • " has already been added");
    case WindowManagerGlobal.ADD_STARTING_NOT_NEEDED:
    // Silently ignore – we would have just removed it
    // right away, anyway.
    return;
    case WindowManagerGlobal.ADD_MULTIPLE_SINGLETON:
    throw new WindowManager.BadTokenException("Unable to add window "
  • mWindow + " – another window of type "
  • mWindowAttributes.type + " already exists");
    case WindowManagerGlobal.ADD_PERMISSION_DENIED:
    throw new WindowManager.BadTokenException("Unable to add window "
  • mWindow + " – permission denied for window type "
  • mWindowAttributes.type);
    case WindowManagerGlobal.ADD_INVALID_DISPLAY:
    throw new WindowManager.InvalidDisplayException("Unable to add window "
  • mWindow + " – the specified display can not be found");
    case WindowManagerGlobal.ADD_INVALID_TYPE:
    throw new WindowManager.InvalidDisplayException("Unable to add window "
  • mWindow + " – the specified window type "
  • mWindowAttributes.type + " is not valid");
    }

什么情况下windowToken会失效?

UI线程发生阻塞,导致TN.show()没有及时执行,当NotificationManager的检测超时后便会删除WMS中的该token,即造成token失效。

如何解决?

Google在API26中修复了这个问题,即增加了try-catch:

// 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.
try {
mWM.addView(mView, mParams);
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}

因此对于8.0之前的我们也需要做相同的处理。DToast是通过反射完成这个动作,具体看下方实现:

//捕获8.0之前Toast的BadTokenException,Google在Android 8.0的代码提交中修复了这个问题
private void hook(Toast toast) {
try {
Field sField_TN = Toast.class.getDeclaredField(“mTN”);
sField_TN.setAccessible(true);
Field sField_TN_Handler = sField_TN.getType().getDeclaredField(“mHandler”);
sField_TN_Handler.setAccessible(true);

Object tn = sField_TN.get(toast);
Handler preHandler = (Handler) sField_TN_Handler.get(tn);
sField_TN_Handler.set(tn, new SafelyHandlerWrapper(preHandler));
} catch (Exception e) {
e.printStackTrace();
}
}

public class SafelyHandlerWrapper extends Handler {
private Handler impl;

public SafelyHandlerWrapper(Handler impl) {
this.impl = impl;
}

@Override
public void dispatchMessage(Message msg) {
try {
impl.dispatchMessage(msg);
} catch (Exception e) {
}
}

@Override
public void handleMessage(Message msg) {
impl.handleMessage(msg);//需要委托给原Handler执行
}
}

问题四:Android8.0之后的token null is not valid问题

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
外链图片转存中…(img-8EViebG8-1715865876957)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值