跃迁引擎

空気を読んだ雨降らないでよ

iOS Research & Development


iOS 端启动页黑屏解决方案

问题背景

目前 iOS 端应用在安装后概率性会出现黑屏问题,主要表现为某次安装(有些设备是100%必现)后,每次启动时,在出现启动图之前都会出现一段持续 1 ~ 2s 的黑屏现象。这种现象并不是每个用户都会遇到,但是一旦在某次安装时出现,那么在未卸载的这段时间内,每次启动都会遇到该问题

分析报告

出现黑屏的原因

猜测

  • 启动阶段耗时任务导致阻塞了UI线程的加载
  • 系统对启动图片缓存导致
  • 启动图尺寸问题
  • 启动配置有误导致无法正常加载

排查

启动阶段耗时任务导致阻塞了UI线程的加载
  • 通过设置环境变量来查看App的启动时间, 在 Scheme 的参数中添加 DYLD_PRINT_STATISTICSDYLD_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
    40
    2021-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
        #import <UIKit/UIKit.h> 
        @interface UIApplication (LaunchScreen)
        - (void)clearLaunchScreenCache;
        @end

        #import "UIApplication+LaunchScreen.h"
        @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

        // 调用
        #import "UIApplication+LaunchScreen.h"
        [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 看谁能解释清楚,有结论后会更新一篇新文章来介绍。

最近的文章

基于 Clang 的 Xcode 编译器插件开发

LLVM &amp; Clang 官方文档 Clang 是作为常规 LLVM 版本的一部分发布的,你可以从 https://LLVM.org/releases/下载版本。 1.下载LLVM工程1git clone git@github.com:llvm/llvm-project.git 其中包含 …

, , , 开始阅读
更早的文章

单值二叉树

LeetCode 965. 单值二叉树 …

, , 开始阅读
comments powered by Disqus