深夜暗坑 - iOS启动图异常修复方案

引言

你是否也碰到了启动图不更新、未加载等异常问题,今天就给大家带来一个终极解决方案。

Demo 地址:https://github.com/iversonxh/DynamicLaunchImage

效果图:

一、背景和问题

iOS 启动图相信大家都非常熟悉,版本迭代中不免会遇到更换启动图的需求,本以为这是件很简单的事情,但实际操作时却遇到了各种毫无头绪的异常问题,如启动图不更新、启动图未成功渲染等。

苹果曾在2019年WWDC上宣布自2020年4月起,提交审核的应用都必须使用 storyboard来配置启动图。而步入2020年以来,苹果也多次发布公告要求更换启动图配置方式:

具体可点击链接查看:https://developer.apple.com/news/?id=03262020b

在此背景下,百度App随即开展了相关的更换工作,具体的LaunchScreen.storyboard配置方式不再赘述,我们直接说配置后出现的问题:

  • 启动图未渲染成功,表现为每次启动均为白屏,并且线上也有复现,这是我们遇到的主要问题(该问题我们在某些知名App上也有复现);
  • 启动图未能更新,启动后仍展示旧启动图,这个问题相信有不少同学遇到。

二、问题分析定位

首先我们怀疑是配置方式有误、编译缓存等导致的问题,所以针对这些猜测我们做了以下测试:

  • 不同系统、不同机型测试,均有复现,排除该问题只发生在特定机型或系统上;
  • 清空编译缓存,仍旧复现,故排除编译缓存问题;
  • imageView添加背景色,启动时正常显示imageView的背景色,但图片内容未显示,故排除了布局问题;
  • 将图片从Assets中迁移至工程根目录下,出现空白启动图概率降低,但仍会偶现;
  • 修改图片名,前几次正常,之后依旧偶现;
  • 卸载应用重新安装,大概率恢复正常,仍复现;
  • LaunchScreen.storyboard文件复制到新建的空工程中,仍复现,此时猜测为系统缓存问题;
  • ……

经过一系列的测试,我们排除了人为因素、编译问题等可能出现问题的点,最终认定是系统问题导致。

接着我们想到当启动图出现问题时,系统是否会有一些辅助信息输出呢?果然通过 Mac 控制台应用,虽然没有找到明显的异常信息输出,但是我们从中发现了关于启动图生成的关键信息(以下测试基于iOS13系统,不同系统上表现存在差异)。

1.我们创建一个空工程,设备方向默认不更改,配置好启动图:


2.在【Edit Scheme】-【Run】-【Launch】,将其设置为【Wait for the executable to be launched】,接着运行工程,在控制台应用中搜索 SpringBoard 找到如下信息:

3.从日志中我们了解到,应用安装后,SpringBoard异步发起截图请求,接着由SplashBoard.framework生成截图,最后写入磁盘。

Demo 中共生成四张截图,分别为对应着浅色主题下竖屏启动图、浅色主题下横屏启动图、深色主题下竖屏启动图、深色主题下横屏启动图,竖/横屏截图是否生成由 info.plist 中所支持的设备方向决定。
如果在 info.plist 中未勾选任何方向,那么系统会输出“无法生成启动图,因为当前应用不支持任何有效的方向”,此种情况下系统生成启动图时机为首次启动应用时,大家可以自行实验下。
4.相信大家也注意到上图红框中的写入路径(路径较长截图中未能完全显示),查看完整输出如下:

  [baidu.TestLaunchScreen] Snapshot data for <XBApplicationSnapshot: 0x1098e10e0; …BEC9AEF7C41A> written to file: /private/var/mobile/Containers/Data/Application/573E7FE9-8A15-4E84-A562-F8C4A62EAFBC/Library/SplashBoard/Snapshots/baidu.TestLaunchScreen - {DEFAULT GROUP}/1FFD332B-EBA0-40C9-8EEE-BEC9AEF7C41A@3x.ktx
[baidu.TestLaunchScreen] Snapshot data for <XBApplicationSnapshot: 0x115a8e7e0; …AFBB52DBDDB3> written to file: /private/var/mobile/Containers/Data/Application/573E7FE9-8A15-4E84-A562-F8C4A62EAFBC/Library/SplashBoard/Snapshots/baidu.TestLaunchScreen - {DEFAULT GROUP}/96920D11-6312-4D69-BBDB-AFBB52DBDDB3@3x.ktx
[baidu.TestLaunchScreen] Snapshot data for <XBApplicationSnapshot: 0x108f97b60; …33E7BC4CFF46> written to file: /private/var/mobile/Containers/Data/Application/02CCE9FD-5F65-43F4-9D72-A5E0BA0C047E/Library/SplashBoard/Snapshots/baidu.TestLaunchScreen - {DEFAULT GROUP}/98F7B5B1-5B3B-478B-93A8-ED3DE6492AD1@3x.ktx
[baidu.TestLaunchScreen] Snapshot data for <XBApplicationSnapshot: 0x1159be260; …479CC9CC8BAD> written to file: /private/var/mobile/Containers/Data/Application/573E7FE9-8A15-4E84-A562-F8C4A62EAFBC/Library/SplashBoard/Snapshots/baidu.TestLaunchScreen - {DEFAULT GROUP}/D9D48845-8565-42CE-A834-479CC9CC8BAD@3x.ktx

5.此时看到写入路径正是我们所熟知的沙盒目录,接着我们将应用沙盒目录导出,查看Library目录结构如下

  ├── Caches
├── Preferences
└── SplashBoard
    └── Snapshots
        └── baidu.TestLaunchScreen\ -\ {DEFAULT\ GROUP}
            ├── 1FFD332B-EBA0-40C9-8EEE-BEC9AEF7C41A@3x.ktx
            ├── 96920D11-6312-4D69-BBDB-AFBB52DBDDB3@3x.ktx
            ├── 98F7B5B1-5B3B-478B-93A8-ED3DE6492AD1@3x.ktx
            └── D9D48845-8565-42CE-A834-479CC9CC8BAD@3x.ktx

果然,按照控制台中所输出的路径,我们找到了系统生成的启动图文件,其格式为KTX。

缓存启动图的文件名具有规则,但其规则我们不得而知。
6.接着我们点击应用图标启动应用,再次观察控制台应用中输出:


如图可知,点击应用图标后,SpringBoard找到了一个可用的启动图,无需预热SplashBoard,直接使用可用的启动图。
7.由以上分析我们知道系统启动应用时会检查当前是否有可用的启动图,所以我们猜想如果当前没有可用的启动图,那么应该会迫使系统重新生成。为此我们清空了缓存启动图,再次冷启应用,果然验证了我们的猜想:


上图中大致流程为,检测到无可用缓存启动图,预热SplashBoard,生成新的启动图,并缓存至沙盒目录,而我们在沙盒目录中也找到了新生成的启动图文件。

根据以上的分析结果,我们知道应用启动时加载启动图的大致流程:

  • 查找沙盒目录中是否存在可用的缓存启动图,如果有则直接使用,否则执行下一步;
  • 根据 LaunchScreen.storyboard 生成新的启动图,并将其缓存至沙盒目录/Library/SplashBoard/Snapshots/ - {DEFAULT GROUP}/。

但系统是如何生成的,调用了什么样的API,我们无法得知,并且其生成时机也早于我们应用代码可控制时机,也就意味着我们无法控制系统生成启动图的行为,换句话说就是即使我们的storyboard文件配置无误,但启动图出现异常可能是无法避免的,所以我们的想法是既然无法从根源上避免启动图异常问题,那么我们是否能够提供补救措施,让其自动恢复正常,下次冷启就显示我们期望的启动图,这样不至于一旦出现异常后后续冷启都异常,对于用户来说也可接受。

所以接下来我们做了一些尝试来验证是否能够修复我们所遇到的问题:

  1. 清空启动图缓存目录,迫使系统重新生成启动图文件,但仍出现白屏问题,方案无效;
  2. 是否可以我们自己生成启动图放至缓存目录,让系统认为存在可用的缓存启动图:
    a. 清空缓存目录,直接放入随意命名的图片,验证无效,系统会在应用下次启动时或应用挂起时,根据应用支持的界面方向及设备当前的方向重新生成对应的启动图;
    b. 替换缓存启动图文件,即保证该目录下所有子文件名不变,但文件内容全部替换,验证方案有效

替换后冷启效果:


3. 接着我们又做了多次测试,得出了以下结论:

a.替换的图片名需与对应的缓存图完全一致,包括文件扩展名,但实际其内容格式可以为 PNGJPEG
b.替换的图片大小需与当前屏幕大小一致(图片宽高等于屏幕宽高或高宽),如果不一致,系统会重新生成缓存启动图。

经过深度调研及不断地分析测试,我们终于得出一个可行方案,那就是替换系统生成的缓存启动图。

三、解决方案

最终我们决定直接摒弃系统缓存的启动图,完全替换为我们自己生成的启动图。

即用户安装应用后,系统会自动生成启动图并缓存至沙盒目录,接着用户启动应用时,我们通过代码将沙盒目录下缓存的启动图文件全部替换为我们通过代码生成的启动图。

3.1 生成启动图

对 LaunchScreen.storyboard 的初始视图控制器进行截图,参考以下代码:

NSString *launchScreenName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"UILaunchStoryboardName"];
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:launchScreenName bundle:nil];
UIViewController *vc = storyboard.instantiateInitialViewController;
UIGraphicsBeginImageContextWithOptions([UIScreen mainScreen].bounds.size, NO, [UIScreen mainScreen].scale);
[vc.view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();

3.2 替换启动图

前面提到替换图片时,需保持缓存目录下文件名不变,所以这里最简单的办法就是遍历缓存目录下的文件名,接着以这些文件名直接写入替换的图片。

然而当我们按照以上方案初步开发完成,进行多系统验证时,遇到了一个棘手的问题,测试发现方案在iOS10.0及以上工作正常替换成功,但是在iOS9.x及以下系统方案无效。通过断点调试发现调用NSFileManager接口获取缓存目录下的文件名列表为空,再通过观察控制台应用中的输出,发现根本原因是无读取权限:

Sandbox: TestLaunchScreen(403) deny(1) file-read-data /private/var/mobile/Containers/Data/Application/E7CB1946-1CB2-48FF-9193-88FCF7848323/Library/Caches/Snapshots/baidu.TestLaunchScreen

接着我们又测试往缓存目录写入文件,发现也无写入权限:

Sandbox: TestLaunchScreen(630) deny(1) file-write-create /private/var/mobile/Containers/Data/Application/1C4B15FB-6AE4-444F-96FA-9FC3B84622CD/Library/Caches/Snapshots/baidu.TestLaunchScreen1/test.png

顺带测试了下在iOS9.x上删除该缓存目录,发现同样无权限。

这里也是经过不断调试,找到了如下 <span style="font-size: 17px;">API 变相地实现了操作缓存目录,大家可以查看 Demo 体会其作用:

- (BOOL)moveItemAtPath:(NSString *)srcPath 
                toPath:(NSString *)dstPath 
                 error:(NSError **)error API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

3.3 横竖屏问题

大部分非游戏类应用,支持的界面方向都只有竖屏(Portrait),即应用页面不会跟随设备方向旋转,始终以竖屏方向显示。但实际开发时,由于某些特殊需求,我们可能会勾选上横屏(LandScape Left / LandScape Right),虽然我们可以通过代码控制页面不跟随设备方向旋转,但是这会导致系统为应用分别生成横屏和竖屏的启动图,从而导致一个问题:

若用户未开启系统旋转锁定,且横置手机启动应用,这会使得应用启动时显示横屏方向的启动图,而部分应用并未考虑适配横屏场景启动图,从而可能导致该场景下启动图拉伸或压缩等显示异常,比如在LaunchScreen.storyboard中仅添加一张背景图,给其设置约束铺满全屏,竖屏时正常显示,但横屏时就异常了。(ps:大家可以关闭系统旋转锁定,参考横屏冷启淘宝及微信的解决方案)
有一种解决方案是 info.plist 中 Supported interface orientations 置空,但这解决不了启动图不更新或无法渲染问题。

百度App正如上面所描述,我们的产品页面在iPhone上不会跟随设备方向旋转,但iPad上是需要支持设备方向旋转,所以我们的处理是:

针对 iPhone上,我们通过代码仅生成竖屏启动图,然后直接替换全部的缓存启动图,即启动时不管设备方向如何,展示的始终为竖屏启动图;

而针对 iPad上,我们通过代码同时生成竖屏及横屏启动图,接着分别使用这两张图进行替换,同时在替换时进行校验,只有当替换的启动图与缓存启动图宽高一致时才执行,即竖屏只替换竖屏、横屏只替换横屏。

注意 iPad上的方案涉及到图片宽高获取,而相信大家阅读到这里也知道了缓存图格式有KTX,但该图片无法直接使用UIImage接口进行加载,这里我们通过多机型、多系统地查看了KTX图片的元数据,发现总结其中的规则,通过取固定段的字节计算其宽高,或直接使用ImageIO相关的接口可以获取其宽高,参考:

+ (CGSize)getImageSize:(NSData *)imageData {
    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);
    CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, 0, NULL);

    CGFloat width = CGImageGetWidth(imageRef);
    CGFloat height = CGImageGetHeight(imageRef);

    CFRelease(imageRef);
    CFRelease(source);

    return CGSizeMake(width, height);
}

3.4 细节优化

在初步走通了流程,验证了方案的可行性后,我们开始完善设计整套流程,并且测试其性能消耗。如测试发现从storyboard生成截图较为耗时,为此我们做了一个缓存策略,避免每次都去截图。

优化后完整流程图如下:

3.5 方案小结

经历了整个方案从调研到开发完成,以及多机型多系统的测试,我们对缓存启动图在不同系统版本上的表现差异性做了个简单归纳:

  1. 缓存路径:
    iOS13.0 及以上:Library/SplashBoard/Snapshots/${PRODUCT_BUNDLE_IDENTIFIER} - {DEFAULT GROUP};
    iOS13.0 以下:Library/Caches/Snapshots/${PRODUCT_BUNDLE_IDENTIFIER};
  2. 图片格式:
    iOS10.0 及以上:KTX
    iOS10.0 以下:PNG
  3. 系统缓存图目录读写权限:
    iOS10.0 及以上:有权限;
    iOS10.0 以下:无权限。

四、总结

本方案主要用于解决启动图无法渲染、不更新等异常问题,能够让应用自动恢复正常的启动图,从用户角度来说最坏的情况是首次启动时展示了异常的启动图,但下次冷启时即可展示正常的启动图了,保证了用户体验。

理论上在本方案基础之上还可升级添加更多产品策略,但这里也忠告大家请勿滥用,并且未来苹果可能会修改该系统机制。

希望本文能够对碰到此类问题的同学们有所帮助,也欢迎大家对本文指正不足。

最后给大家奉上苹果爸爸关于启动图的官方文档,其中一段:

呃。。。还是希望苹果爸爸能够“完善”这个问题。


https://mp.weixin.qq.com/s/PXKJ0_ETGtw_m0BmD7sZ4A

iOS APP 图标版本化

在我们的项目开发过程中,需要频繁打包给测试人员去测试,有时候我们都不知道测试机上安装的版本是否是最新的,这样会造成很多不必要的麻烦和成本。因此我们需要将buildNumber以水印的方式打在APPIcon上,可以很直观的知道当前是哪一个版本。

发布于:8天以前  |  21次阅读  |  详细内容 »

如何实现一个HTTP请求库——axios源码阅读与分析

在前端开发过程中,我们经常会遇到需要发送异步请求的情况。而使用一个功能齐全,接口完善的HTTP请求库,能够在很大程度上减少我们的开发成本,提高我们的开发效率。

发布于:9天以前  |  23次阅读  |  详细内容 »

老司机 iOS 周报 #144 | 2021-01-14

发布于:12天以前  |  44次阅读  |  详细内容 »

快手,快影 iOS App反调试

发布于:19天以前  |  65次阅读  |  详细内容 »

优酷iOS插件化页面架构方法

随着业务不停地迭代,优酷 APP 用于分发视频资源的 UI 控件越写越多,也越来越复杂,并且同时相似相近的代码也非常多。

发布于:2月以前  |  180次阅读  |  详细内容 »

iOS中的内嵌汇编

写一篇在iOS上使用汇编的文章的想法在脑袋里面停留了很久了,但是迟迟没有动手。虽然早前在做启动耗时优化的工作中,也做过通过拦截objc_msgSend并插入汇编指令来统计方法调用耗时的工作,但也只仅此而已。刚好最近的时间项目在做安全加固,需要写更多的汇编来提高安全性(文章内汇编使用指令集为ARM64),也就有了本文

发布于:2月以前  |  182次阅读  |  详细内容 »

77.9K 的 Axios 项目有哪些值得借鉴的地方

Axios 是一个基于 Promise 的 HTTP 客户端,同时支持浏览器和 Node.js 环境。它是一个优秀的 HTTP 客户端,被广泛地应用在大量的 Web 项目中。

发布于:2月以前  |  166次阅读  |  详细内容 »

不会吧,这也行?iOS后台锁屏监听摇一摇

一般情况下,出于省电、权限、合理性等因素考虑,给人的感觉是很多奇怪的需求安卓可以实现,但是iOS就无法实现!今天要介绍的需求也有这种感觉,就是“当 APP 处于后台或锁屏状态时,依旧可以监听到摇一摇,进而触发某些功能,比如:语音播报”。

发布于:3月以前  |  306次阅读  |  详细内容 »

iOS 稳定性:App 被终止的原因

本次 session 主要内容如下: 介绍了后台应用终止的常见原因,并提供了一些优化建议 介绍了 MetricsKit 提供的在代码中获取诊断和性能数据的方法 介绍了 Xcode Metrics Ogranizer 提供的关于线上用户性能数据的可视化报告

发布于:3月以前  |  421次阅读  |  详细内容 »

优酷iOS插件化页面架构方法

随着业务不停地迭代,优酷 APP 用于分发视频资源的 UI 控件越写越多,也越来越复杂,并且同时相似相近的代码也非常多。

发布于:3月以前  |  318次阅读  |  详细内容 »

Vue中Axios的封装和API接口的管理

在vue项目中,和后台交互获取数据这块,我们通常使用的是axios库,它是基于promise的http库,可运行在浏览器端和node.js中。他有很多优秀的特性,例如拦截请求和响应、取消请求、转换json、客户端防御XSRF等。所以我们的尤大大也是果断放弃了对其官方库vue-resource的维护,直接推荐我们使用axios库。如果还对axios不了解的,可以移步axios文档。

发布于:3月以前  |  292次阅读  |  详细内容 »

iOS 持续集成:更完备的 App Store Connect API

时隔两年 App Store Connect API 有了更新,WWDC 2018 推出了 App Store Connect API ,用于自动化一些 App Store Connect 后台操作。这次更新包含了 app 元数据相关的API,补上了原来缺失的重要一环, 使得几乎可以通过 App Store Connect API 完成 App Store Connect 上的所有操作。今后开发、证书配置、用户管理、测试、发布全流程都可以通过 API 完成。

发布于:3月以前  |  354次阅读  |  详细内容 »

iOS 性能优化:优化 App 启动速度

苹果是一家特别注重用户体验的公司,过去几年一直在优化 App 的启动时间,特别是去年的 WWDC 2019 keynote[1] 上提到,在过去一年苹果开发团队对启动时间提升了 200%

发布于:3月以前  |  327次阅读  |  详细内容 »

iOS圆角的离屏渲染,你真的弄明白了吗

发布于:3月以前  |  306次阅读  |  详细内容 »

iOS导航栏整体滑动解决方案(类似淘宝)

发布于:3月以前  |  359次阅读  |  详细内容 »

让你的应用远离越狱:iOS 14 App Attest 防护功能

当越狱在 iOS 设备第一次流行起来时,iOS 开发人员会尝试各种方法来保护自己的应用程序,以让应用免受盗版等不确定因素的困扰。有许多方法可以做到这一点,包括检查 Cydia 是否存在、检测应用程序是否可读取自身沙箱之外的文件、在检测到调试器时让应用程序崩溃等等。

发布于:3月以前  |  346次阅读  |  详细内容 »

探秘 iOS 14 的 WidgetKit

Widget Extension 提供了 small, medium, large 三个尺寸,不同尺寸可以展示不同的数据、不同的界面,开发者也可以锁定自己APP的 Widget 只有某类尺寸,相同的widget也能重复添加。作为添加在主屏幕上的控件,苹果用了 “At a glance” 来形容 widget ,所以 widget extension 是无法交互的,它能做的只有展示一些信息与点击两个作用,点击后就会引导至app,同时为了性能与耗电量的考虑,Widget extension 也不能展示视频和动态图像。

发布于:3月以前  |  383次阅读  |  详细内容 »

iOS 的自动构建流程

发布于:3月以前  |  410次阅读  |  详细内容 »

iOS14 Widget 万字指北,先人一步获得顶级流量

2020 年 6 月 22 日,苹果召开了第一次线上的开发者大会 - WWDC20。这次发布会上宣布了ARM架构Mac芯片(拳打Intel)、iOS 14 ATT(脚踢Facebook),可谓是一次载入史册(我是爸爸)的发布会了,当然还发布了被称为下一个顶级流量入口的Widget。踩着八月的尾巴,本次我们就来探究一下Widget。本文会从Widget初窥和Widget开发两个维度和章节来探究一下Widget, 其中初窥章节会带您简单的了解一下Widget,适合应用决策者阅读; 开发章节会带着您一步一步的完成设计开发Widget,适合程序员阅读。

发布于:3月以前  |  523次阅读  |  详细内容 »

iOS 性能优化 - Allocations分析内存分配

发布于:3月以前  |  375次阅读  |  详细内容 »

最多阅读

快速配置 Sign In with Apple 1年以前  |  3784次阅读
开篇 关于iOS越狱开发 1年以前  |  2565次阅读
APP适配iOS11 1年以前  |  2526次阅读
使用 GPUImage 实现一个简单相机 1年以前  |  2509次阅读
给数组NSMutableArray排序 1年以前  |  2487次阅读
在越狱的iPhone设置上使用lldb调试 1年以前  |  2440次阅读
App Store 审核指南[2017年最新版本] 1年以前  |  2336次阅读
UITableViewCell高亮效果实现 1年以前  |  2316次阅读
所有iPhone设备尺寸汇总 1年以前  |  2258次阅读
使用ssh访问越狱iPhone的两种方式 1年以前  |  2193次阅读
关于Xcode不能打印崩溃日志 1年以前  |  2117次阅读
使用ssh 访问越狱iPhone的两种方式 1年以前  |  2014次阅读
UIDevice的简单使用 1年以前  |  1805次阅读
为对象添加一个释放时触发的block 1年以前  |  1783次阅读
使用最高权限操作iPhone手机 1年以前  |  1749次阅读