链接:https://www.jianshu.com/p/1d64a5ccbc7c
作者注:没错,分析系统源代码,这个问题是原生安卓的Bug,也就是说只发现小米手机经过了特殊处理,而华为三星等手机并没有改变Toast API
1、概述吐司就不出来了。解决方案:ToastUtils
https://github.com/getActivity/ToastUtils
接下来,让我们开始分析这个问题是如何出现的,解决它的过程,以及解决方案
先来看看大厂APP的吐司
问题
1.连吐司都不会玩的手机是什么梗?
2.是少数车型还是多数车型?
3.为什么通知栏关闭后不能播放?
4.为什么有的模特会玩,有的不会?
解释
1.自从我的ToastUtils框架发布以来,最常被问到的问题之一,您是否可以关闭toast的通知栏?所以我拿着我的小米8和红米Note5测试了一下。我发现没有这个问题,就统一回复了。这是兼容性问题,少数型号可能有问题。为了保证框架的稳定性,不要给出兼容性
2.于是有人陆续给我反馈了这个问题。反馈的人都是华为机型的问题,我就开始关注了。一个同事刚刚用了华为P9,我借了他一会手机。借一下午也没关系。估计同事的心都崩溃了,因为这个问题已经100%重现了,通知栏真的关了以后敬酒也打不出来
3.于是我搜索了Toast的源代码。底层的toast是由WindowManager实现的,但是这和通知栏的权限有什么关系呢?
就算和NotificationManager有关系,和WindowManager有什么关系?检查系统源代码后发现,toast是用WindowManager创建的,但是toast是用INotificationManager显示的,看类名肯定和NotificationManager有关,这也是通知栏权限关闭后不能显示toast的原因。
4.现在经过测试,大部分小米机型都不会因为通知栏权限关闭而播放原Toast,但华为荣耀、三星等都会关闭通知栏权限,导致原Toast显示。这可能是小米手机对这个吐司的显示做了特殊处理。这个问题会出现在Github上的top Toast框架中,一些大厂商的应用(除了QQ微信和美团)也会有这个问题。还没有人开始完美解决这个问题。
吐司打不出去的后果
吐司是我们日常开发中最常用的类。如果我们的APP在通知栏推送更多消息,用户会屏蔽我们的通知栏权限,但这样会引起联合反应,即应用中所有使用Toast的地方都不会显示,完全变成一个哑应用,比如下面的场景:
账户密码输入错误,吐司弹不出来用户网络支付失败,吐司弹不出来网络请求错误,吐司弹不出来双击退出应用,吐司弹不出来等等情况,只要用到原生 Toast 都显示不出来其实这是一个系统Bug。谷歌使用与通知栏相关的应用编程接口,以便在其他应用程序上显示应用程序的吐司。但是由于用户屏蔽了通知栏,系统误以为你没有通知栏的权限,导致这个API变得不可用,间接导致系统拦截Toast的show请求。
2.Toast源代码分析
先看吐司的成分
让我们看看吐司里面的API
还有一个内部类,再看看内部API
由此不难推断Toast只是一个外观类,最终的实现还是由其内部类实现的。因为这个内部类太长了,我们把这个内部类的源代码放在这里,简单的过一遍。
privatesticclasstnextendsiltancientnotification。存根{
privatefinitilwindowmanager。layout params mParams = new window manager。layout params();
privatestaticfinaintshow = 0;
privateStaticFinalinted = 1;
privateStaticFinalintCancel = 2;
finalHandler mHandler
查看mView
查看mNextView
intmdduration;
WindowManager mWM
字符串mPackageName
staticfinallownshort _ DURATION _ time out = 4000;
statiFinallonglong _ DURATION _ time out = 7000;
TN(String packageName,@NullableLooper looper) {
finallywindowmanager。LayoutParams params
params.type = WindowManager。LayoutParams . TYPE _ TOAST
mPackageName = packageName
MH handler = new handler(looper,null) {
@覆盖
publicvoidhandleMessage(消息消息){
switch(msg.what) {
案例展示:{
IBinder token =(IBinder)msg . obj;
handleShow(令牌);
打破;
}
caseHIDE: {
handleHide();
mNextView = null
打破;
}
caseCANCEL: {
handleHide();
mNextView = null
尝试{
getService()。cancelToast(包装名称,TN。这个);
} catch(RemoteException e) {
}
打破;
}
}
}
};
}
@覆盖
public void show(IBinder WindowToken){
获取消息(显示,窗口标记)。sendToTarget();
}
@覆盖
publicvoidhide(){
获取消息(隐藏)。sendToTarget();
}
publicvoidcancel(){
获取消息(取消)。sendToTarget();
}
public void handleshow(IBinder WindowToken){
if(mView!= mNextView) {
handleHide();
mView = mNextView
context context = MView . GetContext()。getApplicationContext();
string package name = MView . GetContext()。getopbackagename();
if(context == null) {
context = MView . GetContext();
}
mWM =(window manager)Context . getsystemservice(Context。WINDOW _ SERVICE);
mParams.token = windowToken
mwm . addview(mv view,mParams);
trisendaccessibilityevent();
}
}
publicvoidhandleHide(){
if(mView!= null) {
if(mView.getParent()!= null) {
mwm . removeview immediate(mv view);
}
mView = null
}
}
}
您只需要简单地看一下,就可以理解Toast的底层是用这个内部类实现的。记住,这个内部类叫TN,字段名叫mTN。我们先来看看Toast中cancel方法的源代码。
Cancel最后调用了内部类TN中同名的方法,然后看Toast中show方法的源代码
仔细观察的同学会发现,这个show方法不仅仅是像cancel一样调用TN内部类中同名的方法,还调用了INotificationManager的API。事实上,不难发现这个创新经理是系统的AIDL。如果你不相信,让我们再来看看这个入侵管理器。
我相信学过AIDL的学生会明白,这里不再讲AIDL相关的知识。如果需要了解,请自行百度。
专注于INotificationManager,一个AIDL系统实现的类,不同系统对应AIDL的类是不一样的。这充分说明了为什么小米的机型仍然可以显示通知栏权限,而华为却不能。具体原因请参考源代码
因为APPlication的包名传递给了系统的通知栏,如果关闭了这个包名对应的app的通知栏的权限,吐司自然不会播放出来。
3.那么如何解决这个问题呢
首先想一个问题。吐司显示使用INotificationManager,与通知栏有关,而吐司创建使用WindowManager,与通知栏无关。那么我们可以通过WindowManager创建类似Toast的东西吗?答案是肯定的。
然而,在这个过程中会出现非常困难的问题。接下来我们来解决这些问题
首先,创建窗口管理器需要一个视图参数和窗口管理器。LayoutParams参数。在这里,我们来谈谈创建窗口管理器。布局并直接复制一些吐司代码:
WindowManager。LayoutParams params = newWindowManager。layout params();
params.height = WindowManager。LayoutParams . WRAP _ CONTENT
params.width = WindowManager。LayoutParams . WRAP _ CONTENT
params.format = PixelFormat。半透明的;
//找不到com . Android . internal . r . style . animation _ toast
//params . window animations = com . Android . internal . r . style . animation _ Toast;
params . window动画=-1;
params.type = WindowManager。LayoutParams . TYPE _ TOAST
params . settitle(" Toast ");
params.flags = WindowManager。标志_保持_屏幕_打开
| WindowManager。layout params . FLAG _ NOT _ FOCUSABLE
| WindowManager。layout params . FLAG _ NOT _ TOUCH able;
然后使用窗口管理器调用添加视图显示,然后报告一个错误
Android . view . Window manager $ BadTokenException:无法添加窗口令牌nullisnotvalid你的活动正在进行吗?
原因是我们使用了type,为什么不能添加TYPE_TOAST,因为在关闭通知权限后将显示类型设置为TOAST会报错,所以这里我们标注了这段代码,然后就可以显示了
// params.type = WindowManager。LayoutParams . TYPE _ TOAST
自建WindowManager没有吐司的显示效果
原因是我们复制了Toast的一些代码,其中有些引用了系统R文件中的资源,但是我不能直接用Java代码引用
params . window动画= com.android. internal。r . style . animation _ Toast;
Java代码不能引用这个Style,不代表XML就不行。在此创建一个样式,并继承原生的吐司样式。我们可以在这里定制或者直接使用。为了方便和风格统一,直接在这里使用。
& ltstyle name = " to astanimation " parent = " @ Android:style/Animation。吐司" >;
& lt!-& lt;item name = " Android:WindowEntAnimation " >;@ anim/toast _ enter & lt;/item>。->;
& lt!-& lt;item name = " Android:windowExitAnimation " >;@ anim/toast _ exit & lt;/item>。->;
& lt/style>。
然后再次指定params . window动画来解决问题
params . window animations = r . style . to stanimation;
自建窗口管理器不会消失
首先,WindowManager不能像Toast一样显示后自动消失。如果像Toast一样容易自动消失,显示后定期发送任务关闭,那么问题来了。如何定义显示的时间?系统Toast显示的时间是多少?
首先,我们需要看看Toast提供的两个常量值
我们没有从这张照片中发现任何有价值的东西。让我们继续往下看,看看这些常数被引用的地方
通过查看源代码继续学习
但是通过测试,短吐司的显示时间是2-3秒,而长吐司的显示时间是3-4秒,所以这两个值都不是吐司显示时间的毫秒数,那么如何得到正确的毫秒数呢?这个问题留给大家思考,这里没有答案。
只能用当前活动创建窗口管理器缺陷
我发现一个问题,Activity和Application也是Context的子类。如果可以创建由活动获得的窗口管理器对象,但是如果由应用程序获得的窗口管理器对象报告错误。
Android . view . Window manager $ BadTokenException:无法添加窗口令牌无效的应用程序
错误报告已经明确了不能用Application对象创建WindowManager,也就是说只能用Activity对象创建WindowManager。
那么问题来了。每次播放自建的吐司,都需要当前的Activity对象。这个问题对于常年使用框架的同学来说是致命的。
在这里,以我的ToastUtils框架为例,它表明敬酒是这样被调用的:
ToastUtils.show("我是吐司");
如果想解决关闭通知栏后再次弹出toast的问题,需要改为
吐司面包。show(主要活动。这,“我是烤面包”);
先说这个问题的影响。我是框架的作者。对我来说,我只需要在ToastUtils中的show方法中再添加一个Activity参数。但是对于使用框架的人来说,在更新框架之后,整个项目中所有使用这个ToastUtils.show()方法的用户都会报错,需要多传一个Activity参数。我相信他们的心都快崩溃了。有没有好的办法解决这个问题?
application . registereactivitylifecycleallbacks(activitylifecycleallback);
这个API是Android 4.0以后才有的,现在大部分设备已经在Android 5.0及以上,所以这个API还是很有前景的。我们来看看ActivityLifecycleCallbacks的界面:
publicationinterfacactivitylifecycleallbacks {
创建的无效活动(活动活动,捆绑保存实例属性);
活动开始(活动活动);
无效活动恢复(活动活动);
空隙活动暂停(活动活动);
无效活动停止(活动活动);
无效活动保存实例状态(活动活动,捆绑未完成);
无效活动已销毁(活动活动);
}
看到这里,相信你已经知道真相了。此方法用于监控应用程序活动中的生命周期方法。
然后我们就可以通过这个API得到与用户交互的Activity对象,从而完成当前Activity对象对WindowManager的创建。
使用窗口管理器实现Toast有一些限制
当然,用WindowManager创建的视图必然会受到Activity的限制,因为它只能在这个Activity上显示,而不能在其他界面上显示,而系统的原Toast可以出现在其他界面上。有什么解决办法吗?
当没有浮动窗口权限时,窗口管理器只能显示附加到调用的活动。授予浮动窗口权限后,可以通过更改类型参数来更改窗口管理器的显示范围,以便窗口管理器可以显示在其他界面上,这样Toast就不会因为活动不可见而变得不可见。
//确定是Android 6.0以上,有浮动窗口权限
如果(构建。VERSION.SDK_INT >=构建。版本_代码。并购。& ampsettings . candrawoverlays(mToast . GetView()。getContext())) {
//解决了WindowManager创建的Toast只能在当前Activity中显示的问题
如果(构建。VERSION.SDK_INT >=构建。版本_代码。O) {
params.type = WindowManager。layout params . TYPE _ APPLICATION _ OVERLAY;
} else{
params.type = WindowManager。LayoutParams . TYPE _ PHONE
}
}
如何在本机Toast和自建窗口管理器之间进行选择
所以我们比较了一组数据:
类型显示范围需要参数兼容性效率通知栏权限悬浮窗权限原生 Toast所有界面Context子类高一般需要不需要WindowManager当前ActivityActivity子类一般高不需要不需要经过比较,原生Toast的优势还是大于WindowManager的。所以如果建议在通知栏有权限的前提下使用原生Toast,我们可以通过判断通知栏的权限是否关闭来判断是显示原生Toast还是自建WindowManager。方法代码如下:
/**
*检查通知栏权限是否打开
*/
publicationstationboolean is notification enabled(上下文上下文){
如果(构建。VERSION.SDK_INT >=构建。版本_代码。N) {
返回((通知管理器)上下文。NOTIFY _ ServiCe))。areNotificationsEnabled();
} elseif(Build。VERSION.SDK_INT >=构建。版本_代码。KITKAT) {
appOps manager appOps =(appOps manager)Context . GetSystemServiCe(上下文。APP _ OPS _ SERVICE);
application info appInfo = context . GetAPPlicationInfo();
string pkg = context . GetAPPlicationContext()。getPackageName();
intuid = appInfo.uid
尝试{
类别<。?>。appops class = class . for name(appops manager . class . GetName());
方法checkopnorthrowmethod = appopclass . getmethod(" checkopnorthrow "),整数。类型,整数。TYPE,string . class);
Field反对派NOTIFICATION value = appops class . getdeclaredfield(" OP _ POST _ NOTIFICATION ");
intvalue=(整数)反对派通知值。get(Integer . class);
return(Integer)CheckOpNorthRowMethod . invoke(appOps,value,uid,pkg)= 0;
} catch(NoSuchMethodException | NoSuchFieldException | InvocationTargetException | IllegalaccessException | RuntimeException | ClassNotFoundException被忽略){
returntrue
}
} else{
returntrue
}
}
详细的源地址请戳这里
https://github.com/getActivity/ToastUtils
作者技术讨论Q组:78797078
●编号390,直接输入该条的编号
1.《toast Toast 不显示了?》援引自互联网,旨在传递更多网络信息知识,仅代表作者本人观点,与本网站无关,侵删请联系页脚下方联系方式。
2.《toast Toast 不显示了?》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。
3.文章转载时请保留本站内容来源地址,https://www.lu-xu.com/fangchan/1610969.html