【问题分析】锁屏界面调起google语音助手后壁纸不可见【Android 14】

在这里插入图片描述

1 问题描述

为系统和锁屏分别设置两张不同的壁纸,然后在锁屏界面长按Power调起google语音助手后,有时候会出现壁纸不可见的情况,如以下截图所示:

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

有的时候又是正常的,但显示的也是系统壁纸,并非是锁屏壁纸。

后面我本地多次尝试,发现了一些规律:

1)、同时设置系统和锁屏壁纸为壁纸A,此时不会有问题。

2)、在第1步的基础上,单独将锁屏壁纸设置为壁纸B,此时也不会有问题。

3)、在第2步的基础上,单独将系统壁纸设置为壁纸C,出现问题。

添加了log后,大概知道问题出现的原因了,不过这里先分析一下WallpaperController中关于计算壁纸可见性的一些代码逻辑吧,之前零零散散看过一点,希望这次能够趁分析这个问题的机会,做一些总结。

2 WallpaperController代码分析

2.1 WallpaperController.adjustWallpaperWindows

更新壁纸的起点在WallpaperController.adjustWallpaperWindows:

在这里插入图片描述

1)、WallpaperController.findWallpaperTarget用来寻找壁纸的目标窗口,将寻找的结果放到成员变量WallpaperController.mFindResults中。

2)、WallpaperController.updateWallpaperWindowsTarget根据成员变量WallpaperController.mFindResults更新成员变量WallpaperController.mWallpaperTarget。

3)、最后如果WallpaperController.mWallpaperTarget不为空,那么认为壁纸可见,再调用WallpaperController.updateWallpaperTokens更新壁纸的可见性。

这里有两个成员变量mFindResults和mWallpaperTarget要先介绍一下。

首先是mWallpaperTarget,定义为:

在这里插入图片描述

白话点说就是壁纸的目标窗口,比如一个Activity的窗口在显示的时候被设置为支持壁纸显示,比如Launcher的窗口,那么这个窗口就可以作为壁纸的目标窗口。如果我们遍历所有的窗口后,找不到一个窗口可以作为壁纸的目标窗口,那么就说明所有的窗口都不支持壁纸显示,那壁纸也就会被设置为不可见。如果可以找到一个壁纸的目标窗口,那么这个目标窗口就会被保存到成员变量WallpaperController.mWallpaperTarget中。

接着是成员变量mFindResults,定义为:

在这里插入图片描述

它是一个FindWallpaperTargetResult类型的成员变量,这里则需要知道FindWallpaperTargetResult这个类的作用,它定义在WallpaperController里:

在这里插入图片描述

从类的注释上以及类名来看,这个类是用来保存寻找壁纸目标窗口操作的结果。

再看它的成员变量,首先是TopWallpaper类型的mTopWallpaper,它又是定义在FindWallpaperTargetResult中的内部类,只有两个成员变量,mTopHideWhenLockedWallpaper和mTopShowWhenLockedWallpaper,结合这里的注释以及我看了代码后的理解:

mTopHideWhenLockedWallpaper和mTopShowWhenLockedWallpaper都可以设置为壁纸窗口,即“Window{d837259 u0 com.android.systemui.wallpapers.ImageWallpaper}”,并且同一时间它们两个中间只能有一个被设置:

1)、如果mTopHideWhenLockedWallpaper被设置(即不为空),说明此时壁纸只能在Home界面可见,锁屏界面不可见。

2)、如果mTopShowWhenLockedWallpaper被设置(即不为空),说明此时壁纸在Home界面以及锁屏界面均可见。

后面分析到相关代码的时候就能了解上面的意义了。

然后再解释几个后续会分析到的成员变量:

1)、mNeedsShowWhenLockedWallpaper,如果在一个Activity界面可以在锁屏界面上显示,比如通话界面,如果这个Activity或者窗口不是全屏的,那么就会把mNeedsShowWhenLockedWallpaper的值设置为true。在这种场景下,即使我们没有为锁屏壁纸找到一个目标窗口,那么我们可能也是需要将锁屏壁纸显示出来的。

2)、useTopWallpaperAsTarget,结合上面第一点来说,如果后续经过我们遍历所有窗口后,我们找不到任何一个窗口可以作为壁纸的目标窗口,但是在一些特殊场景下,我们又是需要将壁纸显示出来的,那么我们就将这个值设置为true,表示我们将TopWallpaper中的mTopHideWhenLockedWallpaper或者mTopShowWhenLockedWallpaper保存的壁纸WindowState本身作为壁纸的目标窗口,至于从这两者中的哪个里面取,则是看当前是否处于锁屏。

3)、wallpaperTarget,这个没什么好说的,在寻找壁纸的目标窗口阶段,我们将寻找的结果保存在FindWallpaperTargetResult.wallpaperTarget中,后续在WallpaperController.updateWallpaperWindowsTarget中,我们就从FindWallpaperTargetResult.wallpaperTarget中拿寻找的结果。

接下来看寻找壁纸的目标窗口的代码,WallpaperController.findWallpaperTarget。

2.2 WallpaperController.findWallpaperTarget

这个方法用来寻找壁纸的目标窗口,是我们本篇文章的分析重点。

在这里插入图片描述

2.2.1 FindWallpaperTargetResult.reset

调用FindWallpaperTargetResult.reset重置FindWallpaperTargetResult的保存的所有信息:

在这里插入图片描述

2.2.2 Freeform的情况

如果当前有WINDOWING_MODE_FREEFORM类型的App显示:

在这里插入图片描述

那么就设置FindWallpaperTargetResult.useTopWallpaperAsTarget为true:

在这里插入图片描述

即在Freeform的场景下直接将壁纸进行显示,不需要再额外找一个目标窗口了,这算是一种对需要显示壁纸的特殊场景的处理。

2.2.3 WallpaperController.mFindWallpapers

接着对所有的窗口进行第一次遍历:

在这里插入图片描述

如果这个窗口是壁纸类型的,那么继续判断,如果WallpaperWindowToken.canShowWhenLocked返回true,说明此时壁纸是允许在锁屏界面显示的,那么就将这个壁纸窗口保存在FindWallpaperTargetResult.mTopShowWhenLockedWallpaper中,后续如果我们检测到FindWallpaperTargetResult.mTopShowWhenLockedWallpaper不为空,那么就说明当前壁纸是允许在锁屏界面显示的。否则就将这个壁纸窗口保存在FindWallpaperTargetResult.mTopHideWhenLockedWallpaper中,后续如果我们检测到FindWallpaperTargetResult.mTopHideWhenLockedWallpaper不为空,那么就说明当前壁纸是只能在Home界面显示的。

另外根据我本地的测试,当同时设置系统壁纸和锁屏壁纸时,WallpaperWindowToken.setShowWhenLocked这个方法会被调用,设置WallpaperWindowToken.mShowWhenLocked为true,调用堆栈为:

在这里插入图片描述

2.2.4 WallpaperController.mFindWallpaperTargetFunction

对所有的窗口进行第二次遍历:

在这里插入图片描述

这里的逻辑也比较复杂,省略不太重要的部分,首先看一种特殊的情况:

如果在锁屏状态,并且此时正在遍历的这个窗口盖在锁屏界面,那么继续判断:

1)、如果现在锁屏的状态为“occluded”。

或者

2)、当前该在锁屏界面上的那个窗口处于Transition,那一般就是open或者close。

该窗口或者该窗口对应的ActivityRecord是否是全屏的,如果不是,那么将mNeedsShowWhenLockedWallpaper设置为true。很好理解,如果是一个非全屏的窗口盖在锁屏界面上,如果不显示锁屏壁纸,那么屏幕上没有被这个非全屏窗口覆盖的部分就会由于没有内容显示从而黑屏。

这里稍微提一下这个对窗口是否处于Transitiond的判断,如果只是判断锁屏的状态为“occluded”,那么可能会出现锁屏状态切换为“occluded”不够及时,从而出现短暂黑屏的现象,就比如我这里长按Power键唤起google语音助手的情况:

在这里插入图片描述

所以我们需要加上对窗口是否处于Transition的判断,确保壁纸在Transition早期阶段就显示。

判断过这种特殊场景后,接着对这个正在遍历的窗口进行判断,如果这个窗口在屏幕上,并且已经绘制完成了,那么调用WindowState.hasWallpaper方法去判断该窗口是否支持显示壁纸(这里就不分析动画过程中显示壁纸的情况了):

在这里插入图片描述

这是更一般的情况。

涉及LetterBox的情况比较少见,最常见的还是通过检查窗口是否配置了FLAG_SHOW_WALLPAPER这个窗口标志位来判断这个窗口是否支持显示壁纸,就比如Launcher。

2.2.5 FindWallpaperTargetResult.setUseTopWallpaperAsTarget

回到WallpaperController.findWallpaperTarget方法:

在这里插入图片描述

这里承接第4步,如果在第4步我们发现一个非全屏的窗口盖在了锁屏界面上,那么就会将FindWallpaperTargetResult.mNeedsShowWhenLockedWallpaper设置为true。

接着在这里,就调用FindWallpaperTargetResult.setUseTopWallpaperAsTarget设置FindWallpaperTargetResult.useTopWallpaperAsTarget为true:

在这里插入图片描述

来保证后续壁纸可以显示,这个场景和Freeform出现的场景处理方式一致,即对需要显示壁纸的特殊场景的一种处理。

2.2.6 FindWallpaperTargetResult.setWallpaperTarget

来看最后的一点内容:

在这里插入图片描述

如果FindWallpaperTargetResult.wallpaperTarget为空,说明通过两次遍历我们没有找到壁纸的目标窗口,但是FindWallpaperTargetResult.useTopWallpaperAsTarget为true,又说明我们的确想显示壁纸,那么就调用FindWallpaperTargetResult.getTopWallpaper看看能不能返回一个壁纸窗口,如果可以,那么就调用FindWallpaperTargetResult.setWallpaperTarget将壁纸的目标窗口设置为壁纸本身。但是也可能会返回null,看下FindWallpaperTargetResult.getTopWallpaper的内容:

在这里插入图片描述

前面说过了,如果将壁纸窗口保存在mTopHideWhenLockedWallpaper中,说明当前壁纸只能在Home界面显示,不能在锁屏界面显示。如果将壁纸窗口保存在mTopShowWhenLockedWallpaper中,说明当前壁纸可以在Home界面以及锁屏界面显示。这里的代码大概就是这种逻辑,比较简单,不再赘述。

需要注意的是这里的返回值可能为空,我们分析的这个问题就是因为这里返回空所以出现了壁纸不可见的情况导致了黑屏,我们后续分析问题产生的原因。

2.3 WallpaperController.updateWallpaperWindowsTarget

在这里插入图片描述

WallpaperController.updateWallpaperWindowsTarget这个方法,我看了下好像没有太多可以说的,就是把上一步寻找壁纸的目标窗口的结果保存到WallpaperController的成员变量mWallpaperTarget中。

2.4 WallpaperController.updateWallpaperTokens

回到WallpaperController.adjustWallpaperWindows中,如果WallpaperController.mWallpaperTarget不为空,那么调用WallpaperController.updateWallpaperTokens设置壁纸的可见性:

在这里插入图片描述

这里需要注意的一点就是,及时这里的传参visibility是true,后续可能也无法将壁纸变为可见,因为这里还有额外的判断。

首先这里的成员变量mWallpaperTokens定义为:

在这里插入图片描述

是一个WallpaperWindowToken的队列,在WallpaperWindowToken创建的时候,会把它加入到mWallpaperTokens中。

接着这里会调用FindWallpaperTargetResult.getTopWallpaper来获取当前的壁纸窗口,只有这个壁纸窗口不为空,并且在mWallpaperTokens中,我们才能将壁纸的可见性设置为true。

后续的WallpaperWindowToken.updateWallpaperWindows就不分析了。

3 问题分析

代码分析完了,现在分析问题。

根据我之前本地操作的结果:

1)、同时设置系统和锁屏壁纸为壁纸A,此时不会有问题。

2)、在第1步的基础上,单独将锁屏壁纸设置为壁纸B,此时也不会有问题。

3)、在第2步的基础上,单独将系统壁纸设置为壁纸C,出现问题。

3.1 同时设置系统和锁屏壁纸为壁纸A

这个路径下,会WallpaperWindowToken.setShowWhenLocked这个方法会被调用,设置WallpaperWindowToken.mShowWhenLocked为true,调用堆栈为:

在这里插入图片描述

对所有窗口进行mFindWallpapers遍历时,由于WallpaperWindowToken.mShowWhenLocked为true,因此会设置mTopShowWhenLockedWallpaper为壁纸窗口。

对所有窗口进行mFindWallpaperTargetFunction遍历时,由于google语音助手这个显示在锁屏界面之上的界面对应的ActivityRecord是非全屏的,因此会设置FindWallpaperTargetResult.mNeedsShowWhenLockedWallpaper为true:

在这里插入图片描述

并且由于所有窗口都不满足作为壁纸的目标窗口的条件,因此这一步没有找到目标窗口。

后续再回到WallpaperController.findWallpaperTarget:

在这里插入图片描述

现在FindWallpaperTargetResult.mNeedsShowWhenLockedWallpaper为true,所以这里会调用FindWallpaperTargetResult.setUseTopWallpaperAsTarget来将FindWallpaperTargetResult.useTopWallpaperAsTarget设置为true,那么接着就会调用FindWallpaperTargetResult.getTopWallpaper尝试获取壁纸窗口,并且将返回的结果作为目标窗口:

在这里插入图片描述

这里由于我们处于锁屏,因此会返回TopWallpaper.mTopShowWhenLockedWallpaper,并且根据我们的分析,因为WallpaperWindowToken.mShowWhenLocked为true,因此之前我们的确是将壁纸窗口保存在了TopWallpaper.mTopShowWhenLockedWallpaper中的,因此这里就可以返回壁纸窗口,并且将其设置为壁纸的目标窗口。

这种情况最终是会找到一个壁纸的目标窗口的,因此壁纸是可见的。

3.2 在第1步的基础上,单独将锁屏壁纸设置为壁纸B

这种情况下,和3.1节的分析内容不会有区别,所以也不会有什么问题。

唯一有点奇怪的是,此时显示的是系统壁纸,而非锁屏壁纸。也不能说奇怪,毕竟系统壁纸才是真正的壁纸,是有一个专门的壁纸窗口对应,而锁屏壁纸,应该只是锁屏界面为自己设置的一张背景图。

3.3 在第2步的基础上,单独将系统壁纸设置为壁纸C

这种情况下,就会出现问题,原因出在哪儿呢?

看了下log,发现此时WallpaperWindowToken.mShowWhenLocked变成了false,那么对所有窗口进行mFindWallpapers遍历时,由于WallpaperWindowToken.mShowWhenLocked为false,因此会设置TopWallpaper.mTopHideWhenLockedWallpaper为壁纸窗口。

而在后续调用FindWallpaperTargetResult.getTopWallpaper尝试获取壁纸窗口时,由于此时处于锁屏,因此返回的仍然是TopWallpaper.mTopShowWhenLockedWallpaper。这就是问题的原因所在了,我们将壁纸窗口保存在了TopWallpaper.mTopHideWhenLockedWallpaper中,那么TopWallpaper.mTopShowWhenLockedWallpaper就是空的,因此调用FindWallpaperTargetResult.getTopWallpaper返回的就是空的,最终结果就是没有为壁纸找到一个目标窗口,壁纸在锁屏状态下变为不可见。

那么为什么WallpaperWindowToken.mShowWhenLocked变成了false呢,我也没看到WallpaperWindowToken.setShowWhenLocked方法有调用将WallpaperWindowToken.mShowWhenLocked设置为false啊?原来是设置系统壁纸的时候,直接重新创建了一个新的WallpaperWindowToken对象:

在这里插入图片描述

WallpaperWindowToken.mShowWhenLocked默认是false,并且后续没有再调用WallpaperWindowToken.setShowWhenLocked将WallpaperWindowToken.mShowWhenLocked设置为true,所以就出现了问题。

最后再看下同时设置系统壁纸和锁屏壁纸的情况吧,同时设置了壁纸后,会重新创建一个WallpaperWindowToken对象,接着就是调用WallpaperWindowToken.setShowWhenLocked设置WallpaperWindowToken.mShowWhenLocked:

在这里插入图片描述

看调用堆栈,都在WallpaperManagerService$DisplayConnector.connectLocked方法中:

在这里插入图片描述

看到这里需要更正上面的一个说法,就是单独设置了系统壁纸的时候,其实也是调用了WallpaperWindowToken.setShowWhenLocked了,但是设置的是false,因为只是针对系统壁纸生效,而本来WallpaperWindowToken.mShowWhenLocked默认的就是false,所以我之前添加的log没有打印…

用白话总结一下这个问题,给我个人的感觉就是:

1)、setShowWhenLocked这个属性表示的壁纸自己支持不支持在锁屏界面显示,是壁纸自己决定的,或者说是Launcher在设置壁纸的时候决定的。

2)、WallpaperController用来决策壁纸是否需要在锁屏界面上显示。

这个问题很明显就是这两者冲突了,WallpaperController的逻辑觉得这个时候壁纸应该在锁屏界面显示,但是还是需要看看在Launcher设置壁纸的时候,设定壁纸是否可以在锁屏界面显示。如果壁纸不支持在锁屏界面显示,那么WallpaperController也不能强行让壁纸在锁屏界面上显示。

4 解决方案

分析到这里感觉这应该是google的原生问题,但是pixel却没有问题,不过发现了一个区别,就是将系统壁纸和锁屏壁纸分别设置为不同的壁纸图片后,在锁屏界面长按Power唤起语音助手时,发现pixel显示的是壁纸是锁屏壁纸,而我们的机器要么显示的是系统壁纸,要么就不显示。

dump信息看了下,原来pixel的机器有两个WallpaperWindowToken,分别管理系统壁纸和锁屏壁纸:

在这里插入图片描述

而我们的机器只有一个,是系统壁纸:

在这里插入图片描述

锁屏壁纸只是NotificationShade为自己设置的一张背景。

跟SystemUI的同事沟通了一下,得知pixel用的似乎不是aosp里的SystemUI,而是自己另外一套的SystemUI,并且将aosp的SystemUI推到手机里也有问题,那这个问题无法参考pixel进行修改了。

回顾一下问题发生的原因,即WallpaperController判断出锁屏界面需要显示壁纸,壁纸却又说我的出厂设定就是只能在Home界面显示,我就不显示,WallpaperController拗不过壁纸,所以出现了黑屏。

如果要解决这个黑屏问题,我目前能想到的就是围绕FindWallpaperTargetResult.mNeedsShowWhenLockedWallpaper这个变量做文章, 既然这个变量被设置为true了,就说明当下的确需要壁纸去显示了,不管壁纸它支持不支持在锁屏界面上显示,它都得直楞起来,先给我显示了再说。

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

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

相关文章

Android system property运作流程源码分析

一.序 前文分析了build.prop这个系统属性文件的生成,每个属性都有一个名称和值,他们都是字符串格式。属性被大量使用在Android系统中,用来记录系统设置或进程之间的信息交换。属性是在整个系统中全局可见的。每个进程可以get/set属性&#x…

傻傻分不清楚:JDK/JRE/JVM的区别和联系

在Java开发的世界里,JDK、JRE和JVM是三个经常听到的术语。 对于初学者来说,它们的概念和区别可能会让人感到困惑。 这篇文章详细解释下三个组件的含义、它们之间的区别和联系。 一,JDK:Java Development Kit JDK是Java开发工具…

MOS管栅极驱动自举电路设计

自举式驱动电路工作原理 自举式电路在高电压栅极驱动电路中是很有用的,其工作原理如下: 当 VS 降低到 IC 电源电压 VDD以下(至少要比VDD低一个二极管压降) 或下拉至地时 (低端开关导通,高端开关关断),电源 VDD 通过自举电阻RBOOT,和自举二极管DBOOT,对自举电容CBOOT…

一套pacs医学影像存档与通讯系统源码 PACS系统的起源、趋势、工作流程

PACS系统的起源 医学影像信息系统最初是从处理放射科的数字图像发展起来的。医学影像信息系统的前身是医学影像存档与通信系统(PACS,Picture Archiving & Communication System),最先推动PACS发展的动力来自于传统的相机厂家…

【已解决】QT C++中QLineEdit不可粘贴输入

本博文源于生产实际,主要解决LineEdit不可粘贴输入的情况。下面将进行具体分析 问题来源 输入框只能一个个输入,不可复制粘贴。 分析 给QLineEdit装一个监听事件,监听它的事件即可。 问题解决步骤 问题一共分为三步: 书写监…

C++反汇编——多态,面试题01

文章目录 1.C的三大特性1.1封装1.2继承1.3多态1.3.1 虚函数1.3.2 多态代码反汇编分析。反汇编分析1——基类指针指向子类对象,构造过程。反汇编分析2——基类指针指向子类对象,调用虚函数getPrice()过程。反汇编分析3——基类对象,调用虚函数…

electron-vite工具打包后通过内置配置文件动态修改接口地址实现方法

系列文章目录 electronvitevue3 快速入门教程 文章目录 系列文章目录前言一、实现过程二、代码演示1.resources/env.json2.App.vue3.main/index.js4.request.js5.安装后修改 前言 使用electron-vite 工具开发项目打包完后每次要改接口地址都要重新打包,对于多环境…

思维导图如何用AI生成?借助这几款工具

思维导图如何用AI生成?在数字化时代,思维导图作为一种高效的信息组织与展示工具,被广泛应用于学习、工作和项目管理中。随着人工智能技术的飞速发展,AI生成思维导图已成为现实,极大地提升了创建思维导图的效率和创意。…

翻译《The Old New Thing》 - Restating the obvious about the WM_COMMAND message

Restating the obvious about the WM_COMMAND message - The Old New Thing (microsoft.com)https://devblogs.microsoft.com/oldnewthing/20060302-10/?p32093 Raymond Chen 2006年03月02日 关于 WM_COMMAND 消息的显而易见的知识点补充 简要 本文详细解释了 WM_COMMAND 消息…

Django 静态文件管理与部署指南

title: Django 静态文件管理与部署指南 date: 2024/5/10 17:38:36 updated: 2024/5/10 17:38:36 categories: 后端开发 tags: WebOptCDN加速DjangoCompressWebpackStaticDeployCICD-ToolsSecStatic 第一章:介绍 Django 静态文件的概念和重要性 在 Web 开发中&a…

Linux diff命令(比较两个文件或目录的内容差异)

文章目录 Linux diff 命令详解教程基本用法比较文件输出解释 递归比较(-r)示例代码 控制输出格式统一格式(-u)上下文格式(-c) 高级选项忽略所有空白差异(-w)仅报告文件是否不同 Linu…

纯 CSS 实现标签自动显示超出数量

现代 CSS 强大的令人难以置信。 这次我们来用 CSS 实现这样一个功能:有多个宽度不同的标签水平排列,当外层宽度不足时,会提示超出的数量,演示效果如下 如果让我用 JavaScript来实现估计都有点折腾,毕竟宽度都是动态的…

有什么方便实用的黏土特效教程?6个软件教你快速进行特效制作

有什么方便实用的黏土特效教程?6个软件教你快速进行特效制作 作为时尚小达人,你自己是否想要制作出属于自己的黏土特效照片呢?比如下面几种。 看到这些黏土特效软件有没有心动,下面我也为大家详细的介绍一下可以制作出对应特效的…

Java 集合-List

集合主要分为两组(单列集合, 双列集合) Connection 接口有两个重要的子接口LIst 和 Set, 它们的实现子类都是单列集合, Map 接口的实现子类是双列集合, 存放的是 K-V Connection 接口 Collection 接口和常用方法 下面以 ArrayList 演示一下 add: 添加单个元素remove: 删除指…

Socket学习记录

本次学习Socket的编程开发,该技术在一些通讯软件,比如说微信,QQ等有广泛应用。 网络结构 这些都是计算机网络中的内容,我们在这里简单回顾一下: UDP(User Datagram Protocol):用户数据报协议;TCP(Transmission Contr…

这是一关于DSC相关的文档

这是一关于DSC相关的文档 上面这幅图清晰的展示了somewhat flat的像素图示

机器学习算法应用——关联规则分析(4-4)

关联规则分析(4-4) 关联规则分析(Association Rule Mining)是一种基于频繁项集的分析方法,它以最常出现在一起的元素之间的关系作为分析对象,主要用于发掘大数据中隐藏的关联规则,是数据挖掘技术…

小众行业风口:Q1季度擦窗机器人行业线上市场销售数据分析

今天给大家分享一个2024年的小众行业增长风口——擦窗机器人。 作为家居自动化里的重要一员,擦窗机器人可以简称为擦窗神器,是为了解决大户型家庭的外窗清洁痛点而存在。而目前,擦窗机器人行业正在走向成熟,且市场需求量居高不下…

苹果公司因iPad广告争议而道歉,承认“未达标”|TodayAI

周二,苹果公司发布了一则新的iPad Pro广告,引起了广泛争议,该公司随后发表道歉声明,承认这则广告“未达标”。这则名为“压碎!”的广告意图展示全新的M4芯片iPad Pro的创意潜力,但却因其表现方式而备受批评…

设计模式学习笔记 - 回顾总结:在实际软件开发中常用的设计思想、原则和模式

概述 本章,先来回顾下整个专栏的知识体系,主要包括面向对象、设计原则、编码规范、重构技巧、设计模式五个部分。 面向对象 相对于面向过程、函数式编程,面向对象是现在最主流的编程范式。纯面向过程的编程方法,现在已经不多见了…