深夜暗坑 - 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 启动速度

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

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

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

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

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

探秘 iOS 14 的 WidgetKit

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

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

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

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

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

OCRunner:完全体的iOS热修复方案

为了能够实现一篇文章的思路:Objective-C源码 -> 二进制补丁文件 ->热更新(具体是哪篇我忘了)。当时刚好开始了oc2mango翻译器的漫漫长路(顺带为了学习编译原理,嘻嘻),等基本完成以后,就开始肝OCRunner:完全兼容struct,enum,系统C函数调用,魔改libffi,生成补丁文件等,尽可能兼容Objective-C,为了做一个直接运行OC的快乐人。

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

iOS APP图标版本化

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

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

百度App iOS工程化实践: EasyBox破冰之旅

百度App从单一的搜索工具发展到今天以搜索和Feed流为双引擎的综合性内容消费服务平台,其复杂程度已然不可同日而语矣。 作为一个日活过亿的超级App,业务规模庞大,相关技术人员超过千人,客户端支持主流的移动技术,涉及近百业务方,技术形态复杂,各种组件近三百个,代码百万量级,由此带来的工程化问题是技术团队的一个极大挑战。

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

百度APP iOS暗黑模式适配的完美解决方案

在2019WWDC的开场演讲中,苹果公布了即将推出的iOS13 DarkMode的新特性。此新特性不仅可以在夜晚保护视力,而且对于使用OLED的最新一代设备而言,也可以帮助用户节省电量消耗。不过此特性只支持iOS13以上的系统,为了给全系统所有用户最好的体验,研发出了一套皮肤主题框架,不仅可以全系统支持DarkMode,还可以扩展多套皮肤主题;

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

iOS 14 - 使用 PHPicker 选择照片和视频

绝大多数的 App 都要和相册打交道,选择照片或者视频,要么用来发个朋友圈,要么是放到什么地方做个背景。从 AssertLibrary 到 Photos 框架,苹果已经在多年之前就给相册相关的 API 做过一次大升级了。

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

iOS开发体验优化方案

随着Flutter等跨端框架的出现,业务开发同学经常需要在Android/iOS上跨端进行业务开发,问题定位等。新的不熟悉的环境的搭建总会遇到各种各样的问题,导致搭建失败,特别是iOS开发环境,是最复杂的,不仅环境搭建繁琐,而且切分支后的打包速度很慢,所以我们设计实现了两个工具,用于优化闲鱼iOS开发体验。

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

iOS14:再见了,“流氓”APP!

最近和苹果有关的重大消息可能就是从8月1日开始,AppStore中国区火速下架未获版号的游戏APP,数量超过30000款,之前小智就和大家说过,这未必不是一件好事,众多低质和“流氓”APP将被最大限度隔绝在iOS系统之外。

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

iOS 14 苹果对 Objective-C Runtime 的优化

Objective-C 是一门古老的语言,诞生于 1984 年,跟随 Apple 一路浮沉,见证了乔布斯创建了 NeXT,也见证了乔布斯重回 Apple 重创辉煌,它用它特立独行的语法,堆砌了 UIKit,AppKit, Foundation 等一个个基石,时间来到 2020 年,面对汹涌的"后浪" Swift,"老前辈" Objective-C 也在发挥着自己的余热,即使面对越来越多阵地失守,唯有“老兵不死,只会慢慢凋亡"才能体现的悲壮。今年,Apple 给 Objective-C Runtime 带来了新的优化,接下来,让我们深入理解这些变化。

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

iOS14 隐私适配及部分解决方案

在刚刚结束的线上 WWDC 2020 发布会上苹果向我们展示了新的 iOS14 系统。iOS14 的适配,很重要的一环就集中在用户隐私和安全方面。 在 iOS13 及以前,当用户首次访问应用程序时,会被要求开放大量权限,比如相册、定位、联系人,实际上该应用可能仅仅需要一个选择图片功能,却被要求开放整个照片库的权限,这确实是不合理的。对于相册,在 iOS14 中引入了 “LimitedPhotos Library” 的概念,用户可以授予应用访问其一部分的照片,对于应用来说,仅能读取到用户选择让应用来读取的照片,让我们看到了 Apple 对于用户隐私的尊重。这仅仅是一部分,在iOS14 中,可以看到诸多类似的保护用户隐私的措施,也需要我们升级适配。 最近在调研 iOS14的适配方案,本文主要分享一下 iOS14 上对于隐私授权的变更和部分适配方案,欢迎补充指正。

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

Metal新特性:大幅度提升iOS端性能

Metal 是一个和 OpenGL ES 类似的面向底层的图形编程接口,通过使用相关的 api 可以直接操作 GPU ,最早在 2014 年的 WWDC 的时候发布。Metal 是 iOS 平台独有的,意味着它不能像 OpenGL ES 那样支持跨平台,但是它能最大的挖掘苹果移动设备的 GPU 能力,进行复杂的运算,像 Unity 等游戏引擎都通过 Metal 对 3D 能力进行了优化, App Store 还有相应的运用 Metal 技术的游戏专题。 闲鱼团队是比较早在客户端侧选择Flutter方案的技术团队,当前的闲鱼工程里也是一个较为复杂的Native-Flutter混合工程。作为一个2C的应用,性能和用户体验一直是闲鱼技术团队在开发中比较关注的点。而Metal这样的直接操作GPU的底层接口无疑会给闲鱼技术团队突破性能瓶颈提供一些新的思路。 下面会详细阐述一下这次大会Metal相关的新特性,以及对于闲鱼技术和整个淘系技术来说,这些新特性带来了哪些技术启发与思考。

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

最多阅读

快速配置 Sign In with Apple 1年以前  |  3180次阅读
给数组NSMutableArray排序 1年以前  |  2325次阅读
开篇 关于iOS越狱开发 1年以前  |  2322次阅读
APP适配iOS11 1年以前  |  2245次阅读
在越狱的iPhone设置上使用lldb调试 1年以前  |  2228次阅读
UITableViewCell高亮效果实现 1年以前  |  2159次阅读
App Store 审核指南[2017年最新版本] 1年以前  |  2075次阅读
使用 GPUImage 实现一个简单相机 1年以前  |  2042次阅读
所有iPhone设备尺寸汇总 1年以前  |  2011次阅读
关于Xcode不能打印崩溃日志 1年以前  |  1930次阅读
使用ssh访问越狱iPhone的两种方式 1年以前  |  1926次阅读
使用ssh 访问越狱iPhone的两种方式 1年以前  |  1795次阅读
UIDevice的简单使用 1年以前  |  1658次阅读
为对象添加一个释放时触发的block 1年以前  |  1620次阅读
使用最高权限操作iPhone手机 1年以前  |  1561次阅读