Toast与Snackbar的那点事

背景

Toast是Android平台上的常用技术。从用户角度来看,Toast是用户与App交互最基本的提示控件;从开发者角度来看,Toast是开发过程中常用的调试手段之一。此外,Toast语法也非常简单,仅需一行代码。基于简单易用的优点,Toast在Android开发过程中被广泛使用。

但是,Toast是系统层面提供的,不依赖于前台页面,存在滥用的风险。为了规避这些风险,Google在Android系统版本的迭代过程中,不断进行了优化和限制。这些限制不可避免的影响到了正常的业务逻辑,在迭代过程中,我们遇到过以下几个问题:

  1. 设置中关闭某个App的【显示通知】开关,Toast不再弹出,极大的影响了用户体验。
  2. Toast在Android 7.1.2(API25)以下会发生BadTokenException异常,导致App崩溃。
  3. 自定义TYPE_TOAST类型的Window,在Android 7.1.1、7.1.2发生token null is not valid异常,导致App崩溃。

与Toast斗争

在美团平台的业务中,Toast被用作主流程交互的提示控件,比如在完成下单、评价、分享后进行各种提示。Toast被限制之后会给用户带来误解。为了解决正常的业务Toast被系统限制误伤的问题,我们与Toast展开了一系列的斗争。

斗争一:Toast不弹出

举个案例:某个用户投诉美团App在分享朋友圈后没有任何提示,不知道是否分享成功。具体原因是用户在设置里关闭了美团App的【显示通知】开关,导致通知权限无法获取,这极大的影响了用户体验。然而,在Android 4.4(API19)以下系统中,这个开关的打开状态,也就是通知权限是否开启的状态我们是无法判断的,因此我们也无法感知Toast弹出与否,为了解决这个问题,需要从Toast的源码入手,最后源码总结步骤如下:

  1. Toast#show()源码中,Toast的展示并非自己控制,而是通过AIDL使用INotificationManager获取到NotificationManagerService(NMS)这个远程服务。
  2. 调用service.enqueueToast(pkg, tn, mDuration)将当前Toast的显示加入到通知队列,并传递了一个tn对象,这个对象就是NMS用作回传Toast的显示状态。
  3. 在tn的回调方法中,使用WindowManager将构造的Toast添加到当前的window中,需要注意的是这个window的type类型是TYPE_TOAST

Toast不弹出原因分析

那么为什么禁掉通知权限会导致Toast不再弹出呢?

通过以上分析,Toast的展示是由NMS服务控制的,NMS服务会做一些权限、token等的校验,当通知权限一旦关闭,Toast将不再弹出。

可行性方案调研

如果能够绕过NMS服务的校验那么就可以达到我们的诉求,绕过的方法是按照Toast的源码,实现我们自己的MToast,并将NMS替换成自己的ToastManager,如下图:

方案定了后,需要做的事情就是代码替换。作为平台型App,美团App大量使用了Toast,人工替换肯定会出现遗漏的地方,为了能用更少的人力来解决这个问题,我们采用了如下方案。

解决方案

美团App在早期就因业务需要接入了AspectJ,AspectJ是Java中做AOP编程的利器,基本原理就是在代码编译期对切面的代码进行修改,插入我们预先写好的逻辑或者直接替换当前方法的实现。美团App的做法就是借用AspectJ,从源头拦截并替换Toast的调用实现。

关键代码如下:

@Aspect
public class ToastAspect {@Pointcut("call(* android.widget.Toast+.show(..))")public void toastShow() {}@Around("toastShow()")public void toastShow(ProceedingJoinPoint point) {Toast toast = (Toast) point.getTarget();Context context = (Context) ReflectUtils.getValue(toast, "mContext");if (Build.VERSION.SDK_INT >= 19 && NotificationManagerCompat.from(context).areNotificationsEnabled()) {point.proceed(point.getArgs());} else {floatToastShow(toast, context);}}private static void floatToastShow(Toast toast, Context context) {...new MToast(context).setDuration(mDuration).setView(mNextView).setGravity(mGravity, mX, mY).setMargin(mHorizontalMargin, mVerticalMargin).show();}
}

其中MToast是TYPE_TOAST类型的的Window,这样即使禁掉通知权限,业务代码也可以不作任何修改,继续弹出Toast。而底层已经被无感知的替换成自己的MToast了,以最小的成本达到了目标。

斗争二:BadTokenException

美团App在线上经常会上报BadTokenExceptionCrash,而且集中在Android 5.0 - Android 7.1.2的机型上。具体Crash堆栈如下:

android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@6caa743 is not valid; is your activity running?at android.view.ViewRootImpl.setView(ViewRootImpl.java:607)at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:341)at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:106)at android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3242)`BadTokenException`at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2544)at android.app.ActivityThread.access$900(ActivityThread.java:168)at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1378)at android.os.Handler.dispatchMessage(Handler.java:102)at android.os.Looper.loop(Looper.java:150)at android.app.ActivityThread.main(ActivityThread.java:5665)at java.lang.reflect.Method.invoke(Native Method)at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:822)at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:712)

BadTokenException原因分析

我们知道在Android上,任何视图的显示都要依赖于一个视图窗口Window,同样Toast的显示也需要一个窗口,前文已经分析了这个窗口的类型就是TYPE_TOAST,是一个系统窗口,这个窗口最终会被WindowManagerService(WMS)标记管理。但是我们的普通应用程序怎么能拥有添加系统窗口的权限呢?查看源码后发现需要以下几个步骤:

  1. 当显示一个Toast时,NMS会生成一个token,而NMS本身就是一个系统级的服务,所以由它生成的token必然拥有权限添加系统窗口。
  2. NMS通过ITransientNotification也就是tn对象,将生成的token回传到我们自己的应用程序进程中。
  3. 应用程序调用handleShow方法,去向WindowManager添加窗口。
  4. WindowManager检查当前窗口的token是否有效,如果有效,则添加窗口展示Toast;如果无效,则抛出上述异常,Crash发生。

详细的原理图如下:

在Android 7.1.1的NMS源码中,关键代码如下:

void showNextToastLocked() {ToastRecord record = mToastQueue.get(0);while (record != null) {try {// 调用tn对象的show方法展示toast,并回传tokenrecord.callback.show(record.token);// 超时处理scheduleTimeoutLocked(record);return;} catch (RemoteException e) {...}}
}private void scheduleTimeoutLocked(ToastRecord r)
{mHandler.removeCallbacksAndMessages(r);Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;// 根据toast显示的时长,延迟触发消息,最终调用下面的方法mHandler.sendMessageDelayed(m, delay);
}private void handleTimeout(ToastRecord record)
{synchronized (mToastQueue) {int index = indexOfToastLocked(record.pkg, record.callback);if (index >= 0) {cancelToastLocked(index);}}
}void cancelToastLocked(int index) {ToastRecord record = mToastQueue.get(index);try {// 调用tn对象的hide方法隐藏toastrecord.callback.hide();} catch (RemoteException e) {...}ToastRecord lastToast = mToastQueue.remove(index);// 移除当前的toast的token,token就此失效mWindowManagerInternal.removeWindowToken(lastToast.token, true, DEFAULT_DISPLAY);...
}

问题验证

通过以上分析showNextToastLocked()被调用后,如果此时主线程由于其它原因被阻塞导致handleShow()不能及时调用,从而触发超时逻辑导致token失效。主线程阻塞结束后,继续执行Toast的show方法时,发现token已经失效了,于是抛出BadTokenException异常从而导致上述Crash。

可以使用以下的代码验证此异常:

Toast.makeText(this, "测试Crash", Toast.LENGTH_SHORT).show();
try {Thread.sleep(5000);
} catch (InterruptedException e) {e.printStackTrace();
}

解决方案

那么如何解决这个异常呢?首先想到就是对Toast加上try-catch,但是发现不起作用,原因是这个异常并非在当前线程中立即被抛出的,而是添加到了消息队列中,等待消息真正执行时才会被抛出。Google在Android 8.0的代码提交中修复了这个问题,把8.0的源码和前一版本对比可以发现,如同我们的分析,Google在消息执行处将异常catch住了。那么针对8.0之前的版本发生的Crash怎么办呢?美团平台使用了一个类似代理反射的通用解决方案,结构如下图:

基本原理:使用我们自己实现的ToastHandler替换Toast内部的Handler,ToastHandler作用就是把异常catch住,这种修改思路和Android 8.0修复思路保持一致,只不过一个是在系统层面解决,一个是在用户层面解决。

斗争三:token null is not valid

在Android 7.1.1、7.1.2和去年8月发布的Android 8.0系统中,我们的方案出现了另一个异常token null is not valid,这个异常堆栈如下:

android.view.WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?at android.view.ViewRootImpl.setView(ViewRootImpl.java:683)at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:342)at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:94)

token null is not valid原因分析

这个异常其实并非是Toast的异常,而是Google对WindowManage的一些限制导致的。Android从7.1.1版本开始,对WindowManager做了一些限制和修改,特别是TYPE_TOAST类型的窗口,必须要传递一个token用于权限校验才允许添加。Toast源码在7.1.1及以上也有了变化,Toast的WindowManager.LayoutParams参数额外添加了一个token属性,这个属性的来源就已经在上文分析过了,它是在NMS中被初始化的,用于对添加的窗口类型进行校验。当用户禁掉通知权限时,由于AspectJ的存在,最终会调用我们封装的MToast,但是MToast没有经过NMS,因此无法获取到这个属性,另外就算我们按照NMS的方法自己生成一个token,这个token也是没有添加TYPE_TOAST权限的,最终还是无法避免这个异常的发生。

源码中关键代码如下:

// 方法签名多了一个IBinder类型的token,它是在NMS中创建的
public void handleShow(IBinder windowToken) {...if (mView != mNextView) {...mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);mParams.x = mX;mParams.y = mY;mParams.verticalMargin = mVerticalMargin;mParams.horizontalMargin = mHorizontalMargin;mParams.packageName = packageName;mParams.hideTimeoutMilliseconds = mDuration == Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;// 这里添加了tokenmParams.token = windowToken;if (mView.getParent() != null) {if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);mWM.removeView(mView);}...try {// 8.0版本的系统,将这里的异常catch住了mWM.addView(mView, mParams);trySendAccessibilityEvent();} catch (WindowManager.BadTokenException e) {/* ignore */}}
}

解决方案

经过调研,发现Google对WindowManager的限制,让我们不得不放弃使用TYPE_TOAST类型的窗口替代Toast,也代表了我们上述使用WindowManager方案的终结。

斗争总结

我们的核心目标只是希望在用户关闭通知消息开关的情况下,能继续看到通知,所以我们使用了WindowManager添加自定义window的方式来替换Toast,但是在替换的过程中遇到了一些Toast的Crash异常,为了解决这些Crash,我们提出了使用自定义ToastHandler的方式来catch住异常,确保app正常运行。在方案推广上,为了能用更少的人力,更高的效率完成替换,我们使用了AspectJ的方案。最后,在Android 7.1.1版本开始,由于Google对WindowManager的限制,导致这种使用自定义window的替换Toast的方式不再可行,我们便开始寻找替换Toast的其它可行方案。

替换Toast的可行方案

为了继续能让用户在禁掉通知权限的情况下,也能看到通知以及屏蔽上述Toast带来的Crash,我们经过调研、分析并尝试了以下几种方案。

  1. 在7.1.1以上系统中继续使用WindowManager方式,只不过需要把type改为TYPE_PHONE等悬浮窗权限。
  2. 使用Dialog、DialogFragment、PopupWindow等弹窗控件来实现一个通知。
  3. 按照Snackbar的实现方式,找到一个可以添加布局的父布局,采用addView的方式添加通知。

以上几种方案的共同点是为了绕过通知权限的检查,即使用户禁掉了通知权限,我们自定义的通知依然可以不受影响的弹出来,但是也有很明显的缺陷,如下图:

经过对比,我们也采用了Snackbar替换Toast的方案,原因是Snackbar是Android自5.0系统推出MaterialDesign后官方推荐的控件,在交互友好性方面比Toast要好,例如:支持手势操作,支持与CoordinatorLayout联动等,Snackbar作为提示控件目前在市面上也被广泛使用,而其它方案有明显的缺陷如下:

首先,使用WindowManager添加悬浮窗的方式,虽然这种方式能和原生的Toast保持完美的一致性,但是需要的权限太高,坑也太多。TYPE_PHONE的权限要比TYPE_TOAST权限敏感太多,而且在Android 8.0系统上必须使用TYPE_APPLICATION_OVERLAY这个type,并且要申请以下两个权限,这两个权限不仅需要在清单文件中声明,而且绝大部分手机默认是关闭状态,需要我们引导用户开启,如果用户选择不开启,那么Toast还是不能弹出。同时还需要适配众多定制化ROM的国产机型。绕过了通知权限的坑,又跳入了悬浮窗权限的坑,这是不可取的。

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW"/>

其次,使用Dialog方式也有明显的缺陷,Dialog、DialogFragment、PopupWindow都严重依赖于Activity,没有Activity作为上下文时,它们是无法创建和显示的,并且简单的通知使用这种控件过重。此外,在UI展示和API一致性上,几乎和Toast没有什么关系,需要额外做封装的成本比较大。

遇到问题

我们在使用Snackbar替换Toast时遇到了以下两个问题:

  1. Snackbar弹出的时候,被Dialog,PopupWindow等控件遮住。
  2. Snackbar无法进行跨页面展示,这是Snackbar实现原理决定的。

解决方案

首先,为了满足自身业务的扩展性、灵活性,我们参照系统Snackbar的源码,进行了按需定制,比如多样化的样式扩展、进入进出的动画扩展、支持自定义布局的扩展等,接口更加丰富。一方面是为了解决以上遇到的问题,另一方面也是为了在业务的迭代过程中能快速开发和适配。以下是基本的类图依赖关系:

问题一解决

针对Snackbar弹出的时候,被Dialog,PopupWindow等控件遮住的问题,原因在于Snackbar依赖于View,当把Activity布局的View传给Snackbar做为Snackbar展示依赖的父View时,后面再弹Dialog,PopupWindow等控件,Snackbar就会被控件遮挡。正确的做法是直接把PopupWindow和Dialog所依赖的View传给Snackbar。那么我们定制化的Snackbar不仅支持传递这个View,也支持直接传递PopupWindow和Dialog的实例,上图中SnackbarBuilder的方法反应了这个改动。

问题二解决

比较复杂的问题是Snackbar不支持跨页面展示,我们在项目中有大量这样的代码:

Toast.makeText(this, "弹出消息", Toast.LENGTH_SHORT).show();
finish();

当直接把Toast替换成Snackbar后,这个消息会一闪而过,用户来不及查看,因为Snackbar依赖的Activity被销毁了,为了解决这个问题,我们一共探讨了三种方案:

方案一:

使用startActivityForResult替换所有跨页面展示的通知,也就是在A页面使用startActivityForResult跳转到B页面,把原本在B页面弹出Toast的逻辑,改写到A页面自己弹出Snackbar。

这种方案:优点在于责任清晰明确,页面被finish后应该展示什么通知以及应该由谁触发这个通知的展示,这个责任本身就在调用方;缺点在于代码改动比较大。因此我们舍弃了这种方案。

方案二: 使用Application.ActivityLifecycleCallbacks全局监听Activity的生命周期,当一个页面关闭的时候,记录下Snackbar剩余需要展示的时间,在进入下一个Activity后,让没有展示完的Snackbar继续展示。

这种方案:优点在于代码改动量小;缺点在于在页面切换过程中,如果Snackbar没有展示结束,会出现一次闪烁。虽然在技术上这种方案很好,代码的侵入性极低,但是这个闪烁对于产品来说无法接受,因此这种方案也不做考虑。

方案三:

使用本地广播进行跨页面展示,这也是美团最终使用的解决方案,具体原理如下

  1. 在A页面跳转B页面前,使用当前传入的Context注册一个广播。
  2. 在B页面finish之前,发送A在跳转前注册的广播,并把需要展示的消息使用Intent返回。
  3. 在广播中获取A页面的实例,使用Snackbar展示B页面回传的消息,并把当前广播unRegister反注册掉。

这是方案一的自动化版本,为了达到自动化的效果和对原有代码的最小侵入性,我们设计了一个辅助类,就是上图中的SnackbarHelper,原理图如下:

SnackbarHelper提供统一的入口,接入成本低,只需要将原有使用context.startActivity()、context.startActivityForResult()、context.finish()的地方改成SnackBarHelper下面的同名方法即可。这样通过广播的方法完成了Snackbar的跨页面展示,业务方的代码修改量仅仅是改一下调用方式,改动极小。

结语

目前这套解决方案在美团业务中被广泛使用,能覆盖到绝大部分场景。通知的展现形式基本与Toast没有区别,不仅解决了用户在禁掉通知的情况下无法看到通知的困境,也降低了客诉率。

作者简介

  • 子尧,美团高级工程师,2017年加入美团,负责平台搜索、平台首页等研发工作。
  • 腾飞,美团资深工程师,2015年加入美团,平台基础业务组负责人,负责平台业务的迭代。

招聘

美团平台客户端技术团队长期招聘技术专家,有兴趣的同学可以发送简历到:fangjintao#meituan.com。

详情请点击:详细JD。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/477133.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LintCode 1683. 杀怪兽(队列)

1. 题目 有 n 只怪兽和一个奥特曼&#xff0c;奥特曼和怪兽都有5个属性值。 当且仅当奥特曼的5个属性值都不小于怪兽时&#xff0c;奥特曼可以杀死怪兽。 当一个怪兽被杀掉时&#xff0c;这个怪兽的5个属性会增加到奥特曼身上。 请问奥特曼最多可以杀死多少怪兽? 样例 1: 输…

聊聊大火的多模态

多模态机器学习&#xff0c;英文全称 MultiModal Machine Learning (MMML)&#xff0c;旨在通过机器学习的方法实现处理和理解多源模态信息的能力。每一种信息的来源或者形式&#xff0c;都可以称为一种模态。例如&#xff0c;人有触觉&#xff0c;听觉&#xff0c;视觉&#x…

2018开春大礼:750页电子书 + 33场技术沙龙资料 + 17场线上课程分享

2017年&#xff0c;美团成长为中国领先的生活服务电子商务平台&#xff0c;在吃喝玩乐住行等200多个品类&#xff0c;2800多个城区县&#xff0c;服务了亿万消费者、数百万商家&#xff0c;日订单数超过2200万&#xff0c;年度交易总额达到了3600亿。2017年10月&#xff0c;美团…

LintCode 1677. 石头(自定义优先队列)

1. 题目 给定数组 p 代表 n 个石头的位置和数组 d 代表这 n 块石头能够扔的距离。 从左(0位置)往右走。当你第 k 次碰到一个石头时&#xff0c; 如果 k 是奇数&#xff0c; 把这个石头往右扔&#xff1b; 如果 k 是偶数&#xff0c;跳过这个石头。 返回不再会碰到石头时&…

手机上也能训练BERT和ResNet了?!

源 | 机器之心研究者表示&#xff0c;他们将边缘训练看作一个优化问题&#xff0c;从而发现了在给定内存预算下实现最小能耗的最优调度。目前&#xff0c;智能手机和嵌入式平台等边缘设备上已经广泛部署深度学习模型来进行推理。其中&#xff0c;训练仍然主要是在具有 GPU 等高…

LintCode 125. 背包问题 II(DP)

1. 题目 有 n 个物品和一个大小为 m 的背包. 给定数组 A 表示每个物品的大小 数组 V 表示每个物品的价值. 问最多能装入背包的总价值是多大? 样例 1: 输入: m 10, A [2, 3, 5, 7], V [1, 5, 2, 4] 输出: 9 解释: 装入 A[1] 和 A[3] 可以得到最大价值, V[1] V[3] 9 样例…

大众点评App的短视频耗电量优化实战

前言 美团测试团队负责App的质量保证工作&#xff0c;日常除了App的功能测试以外&#xff0c;还会重点关注App的性能测试。现在大家对手机越来越依赖&#xff0c;而上面各App的耗电量&#xff0c;直接影响了手机的待机时间&#xff0c;是用户非常关心的一点。本文主要通过一个典…

解决CNN固有缺陷!通用 CNN 架构CCNN来了| ICML2022

文 | David W. Romero等源丨机器之心在 VGG、U-Net、TCN 网络中... CNN 虽然功能强大&#xff0c;但必须针对特定问题、数据类型、长度和分辨率进行定制&#xff0c;才能发挥其作用。我们不禁会问&#xff0c;可以设计出一个在所有这些网络中都运行良好的单一 CNN 吗&#xff1…

境外业务性能优化实践

本文根据第16期美团技术线上沙龙OnLine演讲内容整理而成。 前言 性能问题简介 应用性能是产品用户体验的基石&#xff0c;性能优化的终极目标是优化用户体验。当我们谈及性能&#xff0c;最直观能想到的一个词是“快”&#xff0c;Strangeloop在对众多的网站做性能分析之后得出…

LeetCode 第 21 场双周赛(779/1913,前40.7%)

文章目录1. 比赛结果2. 题目LeetCode 5336. 上升下降字符串 easyLeetCode 5337. 每个元音包含偶数次的最长子字符串 mediumLeetCode 5338. 二叉树中的最长交错路径 mediumLeetCode 5339. 二叉搜索子树的最大键值和 hard1. 比赛结果 只做出来了第1题&#xff0c;第3题有一个例子…

算法工程师的三观测试

文 | 小戏编 | 小轶如果我在谷歌输入“How to improve my machine learning models”&#xff0c;我会得到形形色色花样繁多的提升模型性能的方法。从调参到特征工程&#xff0c;从集成模型到数据增强&#xff0c;琳琅满目&#xff0c;不胜枚举。可是如果我在这个问题上加一点限…

领域驱动设计在互联网业务开发中的实践

至少30年以前&#xff0c;一些软件设计人员就已经意识到领域建模和设计的重要性&#xff0c;并形成一种思潮&#xff0c;Eric Evans将其定义为领域驱动设计&#xff08;Domain-Driven Design&#xff0c;简称DDD&#xff09;。在互联网开发“小步快跑&#xff0c;迭代试错”的大…

LeetCode 1377. T 秒后青蛙的位置(BFS)

1. 题目 给你一棵由 n 个顶点组成的无向树&#xff0c;顶点编号从 1 到 n。青蛙从 顶点 1 开始起跳。规则如下&#xff1a; 在一秒内&#xff0c;青蛙从它所在的当前顶点跳到另一个 未访问 过的顶点&#xff08;如果它们直接相连&#xff09;。青蛙无法跳回已经访问过的顶点。…

已删除

7.12更新:部分基金赎回的钱已经到账小金库了&#xff0c;今日从小金库提现时又提示银行卡已删除&#xff0c;不过这次可以点击重新绑定&#xff0c;重新绑定时会报错“绑定的卡与原卡一致”&#xff0c;流程没法走完。但这时候再退回去却发现“银行卡已删除”的提示没有了。于是…

即时配送的ETA问题之亿级样本特征构造实践

ETA&#xff08;Estimated time of Arrival&#xff0c;预计送达时间&#xff09;是外卖配送场景中最重要的变量之一&#xff08;如图1&#xff09;。 我们对ETA预估的准确度和合理度会对上亿外卖用户的订单体验造成深远影响&#xff0c;这关系到用户的后续行为和留存&#xff…

LeetCode 1376. 通知所有员工所需的时间(DFS)

1. 题目 公司里有 n 名员工&#xff0c;每个员工的 ID 都是独一无二的&#xff0c;编号从 0 到 n - 1。公司的总负责人通过 headID 进行标识。 在 manager 数组中&#xff0c;每个员工都有一个直属负责人&#xff0c;其中 manager[i] 是第 i 名员工的直属负责人。对于总负责人…

华为天才少年稚晖君做了一把模块化机械键盘,引起极客圈地震,网友:这才是真正的客制化...

作者 | 王玥、李梅&#xff0c;陈彩娴&#xff08;编辑&#xff09;来源 | AI科技评论他来了他来了&#xff0c;一人顶一个团队的稚晖君又一次带着一项硬核&#xff08;虽然他强调是“软核”&#xff09;黑科技来了&#xff01;稚晖君&#xff0c;真名彭志辉&#xff0c;三次元…

iPhone X 刘海打理指北

iPhone X 刘海机于9月13日发布&#xff0c;给科技小春晚带来一波高潮。作为开发人员却多出来一份忧虑&#xff0c;iPhone X 怎么适配&#xff1f;我们 App 的脑袋会不会也长一刘海出来&#xff1f;Tabbar 会不会被圆角&#xff1f;先来看一下美团 App 的表现&#xff1a; 图 1.…

大厂们终于无法忍受“加一秒”了,微软谷歌Meta等公司提议废除闰秒

文 | 萧箫 发自 凹非寺源 | 量子位 , 公众号 QbitAI大厂们再也无法忍受闰秒带来的一堆bug了。现在&#xff0c;谷歌Meta微软亚马逊等一众科技巨头发起了一项倡议&#xff1a;废除闰秒&#xff01;闰秒这玩意&#xff0c;说白了就是通过给“世界标准时间”加_&#xff08;或减&a…

LeetCode 1374. 生成每种字符都是奇数个的字符串

1. 题目 给你一个整数 n&#xff0c;请你返回一个含 n 个字符的字符串&#xff0c;其中每种字符在该字符串中都恰好出现 奇数次 。 返回的字符串必须只含小写英文字母。如果存在多个满足题目要求的字符串&#xff0c;则返回其中任意一个即可。 示例 1&#xff1a; 输入&…