
问题背景
目前 iOS 端应用在安装后概率性会出现黑屏问题,主要表现为某次安装(有些设备是100%必现)后,每次启动时,在出现启动图之前都会出现一段持续 1 ~ 2s 的黑屏现象。这种现象并不是每个用户都会遇到,但是一旦在某次安装时出现,那么在未卸载的这段时间内,每次启动都会遇到该问题
分析报告
出现黑屏的原因
猜测
- 启动阶段耗时任务导致阻塞了UI线程的加载
- 系统对启动图片缓存导致
- 启动图尺寸问题
- 启动配置有误导致无法正常加载
排查
启动阶段耗时任务导致阻塞了UI线程的加载
通过设置环境变量来查看App的启动时间, 在 Scheme 的参数中添加
DYLD_PRINT_STATISTICS
和DYLD_PRINT_STATISTICS_DETAILS
查看,未发现明显异常情况,排除启动阶段过长导致的加载延迟 ❌1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
402021-03-03 12:30:29.872999+0800 DataTrack-Debug[13475:3206592] WF: _WebFilterIsActive returning: NO
Total pre-main time: 1.5 seconds (100.0%)
dylib loading time: 118.27 milliseconds (7.7%)
rebase/binding time: 121.23 milliseconds (7.9%)
ObjC setup time: 43.85 milliseconds (2.8%)
initializer time: 1.2 seconds (81.5%)
slowest intializers :
libSystem.B.dylib : 8.82 milliseconds (0.5%)
libMainThreadChecker.dylib : 35.91 milliseconds (2.3%)
libglInterpose.dylib : 477.57 milliseconds (31.1%)
DataTrack-Debug : 1.2 seconds (82.5%)
total time: 3.0 seconds (100.0%)
total images loaded: 522 (507 from dyld shared cache)
total segments mapped: 55, into 2872 pages
total images loading time: 1.5 seconds (51.1%)
total load time in ObjC: 43.85 milliseconds (1.4%)
total debugger pause time: 1.4 seconds (47.3%)
total dtrace DOF registration time: 0.00 milliseconds (0.0%)
total rebase fixups: 624,012
total rebase fixups time: 56.78 milliseconds (1.8%)
total binding fixups: 108,035
total binding fixups time: 68.05 milliseconds (2.2%)
total weak binding fixups time: 65.72 milliseconds (2.1%)
total redo shared cached bindings time: 69.34 milliseconds (2.2%)
total bindings lazily fixed up: 0 of 0
total time in initializers and ObjC +load: 1.2 seconds (41.0%)
libSystem.B.dylib : 8.82 milliseconds (0.2%)
libBacktraceRecording.dylib : 7.47 milliseconds (0.2%)
libobjc.A.dylib : 3.27 milliseconds (0.1%)
libMainThreadChecker.dylib : 35.91 milliseconds (1.1%)
libglInterpose.dylib : 477.57 milliseconds (15.7%)
libMTLCapture.dylib : 15.86 milliseconds (0.5%)
libViewDebuggerSupport.dylib : 16.14 milliseconds (0.5%)
LookinServer : 7.18 milliseconds (0.2%)
DataTrack-Debug : 1.2 seconds (41.6%)
total symbol trie searches: 369778
total symbol table binary searches: 0
total images defining weak symbols: 55
total images using weak symbols: 130屏蔽 didFinishLaunchingWithOptions 中除设置根视图以外的代码,运行后发现无变化,依旧存在黑屏,排除启动阶段,由于代码执行逻辑阻塞UI线程导致的黑屏问题 ❌
- 此时就非常令人感到疑惑了,既然没有阻塞UI线程的代码逻辑,为何还会出现加载延迟的现象呢?
- 尝试屏蔽了 didFinishLaunchingWithOptions 中的所有代码逻辑(理论上只会影响进入落地页,而不会影响启动图),黑屏问题依旧存在,说明也不是根视图设置有问题❌
- 那么,有没有可能是在 didFinishLaunchingWithOptions 执行前做了什么,导致加载被阻塞了呢?
断点停留在 didFinishLaunchingWithOptions 函数第一行,查看调用链以及启动图变化,发现果然存在一个针对 didFinishLaunchingWithOptions 函数的 hook 操作,会不会就是它呢?
- 找到这个函数后,发现是在 +load 函数内进行的方法交换,以此来做 Crash 防护的若干工作,于是屏蔽了这段代码,发现黑屏问题依旧存在,根本就不是因为启动阶段的代码导致的❌
- 不甘心,又用 Instruments 详细查看了启动阶段各个函数的耗时,并且将耗时较多的任务逐一屏蔽,毫无帮助❌
- 此时问题就来了,既然不是我们自身的代码导致,是否有可能是启动阶段的优化,如二进制重拍而导致的问题呢?但是鉴于这个黑屏问题的表现,这种可能性我认为不大,如果是因为启动优化造成的,那么这个问题必然是100%出现在每个用户的设备商,所以排除❌
至此,基本排除了因为启动阶段耗时任务导致阻塞了UI线程的加载的因素。
启动配置有误导致无法正常加载
检查过后发现配置毫无问题
系统对启动图片缓存导致
排除了代码问题,接下来很可能就是由于启动图缓存导致的黑屏了
每次启动,都进行清除缓存的操作,并无效果,排除设备缓存问题 ❌
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@interface UIApplication (LaunchScreen)
- (void)clearLaunchScreenCache;
@end
@implementation UIApplication (LaunchScreen)
- (void)clearLaunchScreenCache {
NSError *error;
[NSFileManager.defaultManager removeItemAtPath:[NSString stringWithFormat:@"%@/Library/Caches/Snapshots",NSHomeDirectory()] error:&error];
if (error) {
NSLog(@"Failed to delete launch screen cache: %@",error);
}
}
@end
// 调用
[UIApplication.sharedApplication clearLaunchScreenCache];诡异的现象
怀疑可能根据图片名做了缓存,于是修改启动图片名称,发现更改名称后首次启动有效,启动图立即展示,但随后的启动再次回到之前的黑屏状态,虽然表现非常奇怪,但是也侧面证明了和加载的图片路径有关,因为每次替换图片名,也就相当于替换了图片的绝对路径,但首次有效,第二次无效这个问题,多半是对路径下的同名图片做了缓存,但为何一读缓存就加载失败,这显然非常的诡异
因为实在太诡异了,所以我把启动图放到一个新建的demo工程里,发现一切正常,启动的一瞬间就加载出来了,直到这个时候,我才意识到此前忽略的一点:启动图应当出现在 didFinishLaunchingWithOptions 之前才对,而先前打断点的时候明显感知到,在 didFinishLaunchingWithOptions 完全执行完毕后,才开始加载启动图,我觉得有些不对劲,打开工程目录发现同时存在着一个 strongBoard 和 xib 的同名 LaunchScreen 文件
我查看了工程内的引用,发现有一段代码引用这xib文件,并且有一段注释写道: 这段代码主要是为了解决,启动页后的白屏问题,恍惚间我意识到什么,用一个新的 UIView 替换了这个 xib,再次运行,果然,启动图全部消失,一片黑屏后,进入了首页,这说明 stroyBoard 的启动图压根就没有加载成功,先前看到的启动图实际上是这段 xib!
1
2// 这段代码主要是为了解决,启动页后的白屏问题
UIView*lanuchView= [[[NSBundlemainBundle] loadNibNamed:@"LaunchScreen"owner:niloptions:nil] lastObject];重新设置 storyBoard
进入 stroyBoard 后,看不出什么明显的问题,突然想到有没有可能是图片尺寸问题导致加载不出来,毕竟之前看到过类似的文章介绍,但转念一想,demo一模一样的图片就没问题,所以不太可能是图片本身的问题,那就可能是其他原因导致的,于是做出如下尝试
- 替换图片,随便替换了一张工程内的图片,发现依旧黑屏❌
- 换了图还不行,怀疑是 storyBoard 作为入口失效了,可能整体就没加载出来,于是删除所有控件的图片,更换背景色,背景色的启动页面加载成功!✅
- 说明 storyBoard 本身的加载没问题,是显示的内容出了问题,于是加上了一个 UILabel 控件,随便填了点内容,发现启动页面和 UILabel 控件加载正常!✅
- 控件也没问题?那有没有可能是拉的约束出了问题,毕竟 storyBoard 玩的溜的人不多,约束出问题也是有可能的(但如果真的这个问题造成的,黑屏就应该是100%出现的),删除所有 AutoLayout 约束,并填上之前的图片,依旧黑屏,不是约束的问题❌
- 难道这个 UIImageView 控件有问题,没法加载?于是删掉此前的 UIImageView,重新添加新的 UIImageView,再填充启动图片,依旧黑屏❌ 我可要爆粗口了啊
- 会不会是图片太大了?(但是demo咋没问题)不甘心的我又试了试,换了张很小的按钮的切图,加载成功!✅
- 难道是图片大小的关系?不应该啊,于是仔细观察加载成功和不成功的图片区别,发现:加载成功的图片都是存放在 Images.xcassets 中的,不成功的都是放在其他目录的!于是把启动图放到 Images.xcassets 中,再次 UIImageView 中填入启动图,加载成功!✅
结论
目前可以暂时得出一个简易的结论:必须放到 Images.xcassets 中的资源文件才可以被 storyBoard 准确加载,有个别文章还专门强调要放到根路径下,不知误导了多少同学。不过目前具体原理暂时不清楚,待我搜寻一番官方文档或 WWDC 看谁能解释清楚,有结论后会更新一篇新文章来介绍。