Tencent / QMUI_iOS

QMUI iOS——致力于提高项目 UI 开发效率的解决方案
http://qmuiteam.com/ios
Other
7.07k stars 1.37k forks source link

[UIKit Bug] App 处于后台时修改 UIAppearance 里 UIImage 类型的属性很大几率导致第三方键盘 crash #1281

Closed MoLice closed 2 years ago

MoLice commented 3 years ago

Bug 表现

  1. 系统设置里只保留一个键盘,且为第三方键盘,如搜狗、百度(这一步非必须,只是为了便于观察现象)。
  2. 在使用 QMUI 的 App 里(例如 QMUI Demo、微信读书、企业微信等大型应用)唤起该第三方键盘。
  3. 在另一个没使用 QMUI 的 App 里(例如系统的 App)唤起该第三方键盘。
  4. 不断重复在两个 App 之间切换,过一会即可看到第三方键盘无法显示,被一个系统默认键盘代替(意味着此时第三方键盘被杀掉,且系统尝试重新创建它时无法在足够快的时间内完成,于是用系统默认键盘代替),如以下录屏所示:

预期的表现 不管进行多少次 App 间的切换,都不应该导致第三方输入法频繁被杀掉。

其他信息

jiasongs commented 3 years ago

我也遇到了这个问题,我屏蔽了[在后台切换主题]的代码之后就没有用户反馈过了。不知是不是同样的问题。

MoLice commented 3 years ago

连接真机,通过系统控制台可得知在出现 bug 现象时,第三方输入法会收到内存警告,在没使用 QMUI 的 App 之间互相切换不会引起这个内存警告,证明是 QMUI 里的某些代码引起该问题。

自己创建一个 Custom Keyboard Extension,按照 issue 的重现步骤操作后,我们的输入法确实遇到了系统内存限制的报错(上限 66MB,不同的 iOS 版本、iPhone 设备,该上限不同,所以老旧机器更容易触发该现象)。

在不断重复的测试过程中可看到,宿主应用(Host App,也即当前正在使用键盘的 App)更新 UIAppearance 的行为会通信给第三方键盘:

进而对 UIAppearance 中 UIImage 类型的对象进行反序列化:

通过在第三方键盘 Target 里 hook -[UIImage initWithCoder:] 可知在 Host App 退到后台时,第三方键盘里进行了上千次的图片生成操作,而且每次都比上一次的次数要翻很多倍。

而且按地址过滤后可看到其中有大量重复的 image 操作,这现象明显是异常的。

以 QMUI Demo 为例,通过这些图片的 size(width = 4, height = 88) 可知是 UINavigationBarbackgroundImageshadowImage 等图片,也即业务 App 自己设置的图片。而 QMUI Demo 里是在配置表里设置这些图片的,当系统的 DarkMode 发生切换时,会重新应用配置表,将图片设置到 UINavigationBar.appearance 的同时,遍历当前界面里的所有 UINavigationBar 实例,修改它们的样式。

结合上述的 crash 堆栈,尝试将上面截图里的①注释掉,发现问题得到解决。

将 backgroundImage 以 UINavigationBar.appearance 的方式使用是一个常规的操作,本身应该没问题,而问题出现在切换 App 时,此时 QMUI 会重新应用配置表,再走一次 UIAppearance 的更新,猜测可能是时机问题引起的。于是新建一个空项目,在 - [UIViewController traitCollectionDidChange:] 里设置 UIAppearance,当切换 App 时,系统会将 App 快速切换到 Light、Dark,分别对 App 截两张图,此时会触发 traitCollectionDidChange:,进而触发 UIAppearance 的更新,然后成功重现该问题。点击下载测试 Demo

到这里该 Bug 可以定性为系统 Bug:在 App 处于后台时修改 UIAppearance 里的 UIImage 类型的属性,会导致第三方键盘在短时间内异常进行大量 UIImage 的反序列化操作,最终触达系统对输入法分配的内存上限,输入法 crash。

进一步测试可知几个信息点:

  1. 通过 UIAppearance 设置的任意 image 都会在第三方输入法的第二次及后续的启动时反序列化(-[UIImage initWithCoder:]),设置多少次 appearance 的 image 就会调用多少次 initWithCoder。即便对同一个 UIAppearance 设置相同属性的 image,也不是覆盖性质,而是增加,每设置一次都会产生一次 initWithCoder:。
  2. 第一次显示键盘不会触发,第二次开始及后续都会触发。如果键盘正在显示时切换 App(也即把之前的 App 推到后台),那么重新唤醒该 App 时,键盘视为一次新的显示。
  3. 正常情况下使用 UIAppearance 也会触发第三方键盘调用 initWithCoder:,但设置多少次就会调用多少次,这通常不会导致 OOM,也不会像 issue 里描述的那样以几倍的规模扩大,且倍数还越来越大。

至此该系统 Bug 的解决方式就很清晰了:在设置 UIAppearance 前判断当前 App 是否在后台(建议监听 UIApplicationDidFinishLaunchingNotification、UIApplicationWillEnterForegroundNotification 等通知,而非判断 UIApplicationState,因为当 App 启动时走到某些类的 +load 方法时,UIApplicationState 为 Active,但走到 AppDelegate 的 didFinishLaunching 时又变成 Inactive 了),是则不设置。 由于该问题没有合适的统一修复的方法,因此 QMUI 仅修复自身的场景,不提供通用的方案。业务项目里非 QMUI 的代码如果也有这种操作,也需自行判断。

MoLice commented 2 years ago

已发布 4.3.0 修复该问题。