APP启动过程监控与优化

4,322 阅读8分钟

前言

过长的启动时间对于用户体验是非常的不好的,苹果建议的启动时间不要超过400ms,大约0.4秒,并且启动时间超过20s将被系统直接杀死,所以优化启动时间是极其必要的。

这里有篇写的不错: www.jianshu.com/p/db493e529… 美团: tech.meituan.com/2018/12/06/…

App的两种启动方式:冷/热启动App完整启动流程以及优化思路

但是,一般提到启动优化和监控,基本上都是指的是:冷启动。所以下面的介绍,也是冷启动优化和监控。

一、冷启动热启动

  • 冷启动:App点击启动前,此时App的进程还不在系统里。需要系统新创建一个进程分配给App。
  • 热启动:App在冷启动后用户将App退回后台,此时App的进程还在系统里。用户重新返回App的过程。

主要区别:

名称区别
冷启动启动时,App的进程不在系统里,需要开启新进程。
热启动启动时,App的进程还在系统里,不需要开启新进程。

热启动流程:

image.png image.png

二、启动耗时统计

用户能感知到的启动慢,其实都发生在主线程上。 而主线程慢的原因有很多,比如在主线程上执行了耗时的IO读写操作、在渲染周期中执行了大量计算等。 我们一般统计 App 的启动耗时是从点击 App 开始首屏渲染完成的过程所消耗的时间。 总结来说,App 的启动主要包括三个阶段:

  • main() 函数执行前;即操作系统加载App可执行文件到内存,然后执行一系列的加载&链接等工作,最后执行至App的main()函数
  • main() 函数执行后;即从main()开始,到appDelegatedidFinishLaunchingWithOptions方法执行完毕.
  • 首屏渲染完成后;即 didFinishLaunchingWithOptions 方法作用域内执行首屏渲染之后的所有方法执行完成

启动耗时: t(App总启动时间) = t1(main()之前的加载时间) + t2(main()之后到didFinishLaunchingWithOptions的加载时间) + t3首屏渲染完成

注释:pre-main阶段优化是相对比较复杂的,主要优化还是在main()之后

1,main 函数之前称为 pre_main 阶段,main函数执行前,系统会做的事:

Main a, # 1a IF A, Appes e Thin #!.png

  • App启动后,首先,系统内核Kernel)创建一个进程。 加载可执行文件。(App里的所有.o文件
  • 加载动态链接库(dyld是苹果的动态链接器),进行rebase指针调整和bind符号绑定。
  • ObjCruntime初始化。包括:ObjC相关Class的注册category注册selector唯一性检查等。
  • 初始化。包括:执行+load()方法、用attribute((constructor))修饰的函数的调用、创建C++静态全局变量等。

此过程pre_main(main 函数之前)我们可以使用如下方法去查看时长: 打开需要检测耗时的项目,Xcode -> Product -> Scheme -> Edit Scheme,新增: DYLD_PRINT_STATISTICS 设置值为1

Navigate.png

image.png

如上图设置完成,运行项目,然后可以看到下面表格数据 :

加载过程加载时长
Total pre-main time:460.33 milliseconds (100.0%) 总耗时大概:0.46秒
dylib loading time:197.97 milliseconds (43.0%)加载动态库 (可以删除不必要的动态库,动态库合并,或将动态库变成静态库)。
rebase/binding time:85.82 milliseconds (18.6%) 指针重定位(进行rebase指针调整和bind符号绑定,ASLR随机分配的内存地址+文件偏移地址,用于找到外部方法)(外部方法:可执行文件外的方法)。
ObjC setup time:49.00 milliseconds (10.6%) ObjC类初始化(包括ObjC相关Class的注册、category注册、selector唯一性检查等,每20000个类大概增加耗时800ms删除僵尸类。)
initializer time:127.53 milliseconds (27.7%) 其它初始化(调用每个ObjC类与分类的+load方法,创建C++静态全局变量)。
slowest intializers :比较耗时的动态库,根据上面可对pre-main阶段进行优化,同时对比swift和OC,因为swift是静态语言,相对耗时会更少。
libSystem.B.dylib :4.10 milliseconds (0.8%) 一个系统的动态库。
libc++.1.dylib :13.71 milliseconds (2.9%)
libMainThreadChecker.dylib :31.80 milliseconds (6.9%)

还有一个方法获取更详细的时间,只需将环境变量 DYLD_PRINT_STATISTICS_DETAILS 设为 1就可以:

  total time: 1.0 seconds (100.0%)
  total images loaded:  243 (0 from dyld shared cache)
  total segments mapped: 721, into 93608 pages with 6173 pages pre-fetched
  total images loading time: 817.51 milliseconds (78.3%)
  total load time in ObjC:  63.02 milliseconds (6.0%)
  total debugger pause time: 683.67 milliseconds (65.5%)
  total dtrace DOF registration time:   0.07 milliseconds (0.0%)
  total rebase fixups:  2,131,938
  total rebase fixups time:  37.54 milliseconds (3.5%)
  total binding fixups: 243,422
  total binding fixups time:  29.60 milliseconds (2.8%)
  total weak binding fixups time:   1.75 milliseconds (0.1%)
  total redo shared cached bindings time:  29.32 milliseconds (2.8%)
  total bindings lazily fixed up: 0 of 0
  total time in initializers and ObjC +load:  93.76 milliseconds (8.9%)
                           libSystem.dylib :   2.58 milliseconds (0.2%)
               libBacktraceRecording.dylib :   3.06 milliseconds (0.2%)
                            CoreFoundation :   1.85 milliseconds (0.1%)
                                Foundation :   2.61 milliseconds (0.2%)
                libMainThreadChecker.dylib :  42.73 milliseconds (4.0%)
                                   ModelIO :   1.93 milliseconds (0.1%)
                              AFNetworking :  18.76 milliseconds (1.7%)
                                    LDXLog :   9.46 milliseconds (0.9%)
                        libswiftCore.dylib :   1.16 milliseconds (0.1%)
                   libswiftCoreImage.dylib :   1.51 milliseconds (0.1%)
                                    Bigger :   3.91 milliseconds (0.3%)
                              Reachability :   1.48 milliseconds (0.1%)
                             ReactiveCocoa :   1.56 milliseconds (0.1%)
                                SDWebImage :   1.41 milliseconds (0.1%)
                             SVProgressHUD :   1.23 milliseconds (0.1%)
total symbol trie searches:    133246
total symbol table binary searches:    0
total images defining weak symbols:  30
total images using weak symbols:  69

那么针对此过程pre_main(main 函数之前)的优化方案:

  • (1)减少使用 +load() 方法里做事情,尽量把这些事情推迟到+initiailize 方案①:如果可能的话,将+load中的内容,放到渲染完成后做。 方案②:使用+initialize()的方法代替+load(),注意把逻辑移动到+initialize()时,要注意避免+initialize()的重复调用问题,可以使用dispatch_once()让逻辑只执行一次。
  • (2)合并多个动态库 苹果公司建议使用更少的动态库,并且建议在使用动态库的数量较多时,尽量将多个动态库进行合并。数量上,苹果公司最多可以支持6个非系统动态库合并为一个。 除了我们App本身的可行性文件,系统中所有的framework比如UIKit、Foundation等都是以动态链接库的方式集成进App中的。 系统使用动态链接有几点好处代码共用:很多程序都动态链接了这些 lib,但它们在内存和磁盘中只有一份。 易于维护:由于被依赖的 lib 是程序执行时才链接的,所以这些 lib 很容易做更新,比如libSystem.dylib 是 libSystem.B.dylib 的替身,哪天想升级直接换成libSystem.C.dylib 然后再替换替身就行了。 减少可执行文件体积:相比静态链接,动态链接在编译时不需要打进去,所以可执行文件的体积要小很多。
  • (3)优化类、方法、全局变量 减少ObjC类(class)、方法(selector)、分类(category)的数量;少用C++全局变量;main函数执行后,减少加载启动后不会去使用的类或者方法。
  • (4)优化首屏渲染前的功能初始化 main函数执行后到首屏渲染完成前,只处理首屏渲染相关业务。首屏渲染外的其他功能放到首屏渲染完成后去初始化。
  • (5)优化主线程耗时操作,防止屏幕卡顿 首先检查首屏渲染前,主线程上的耗时操作。将耗时操作滞后或异步处理。通常的耗时操作有:网络加载、编辑、存储图片和文件等资源。针对耗时操作做相对应的优化即可。
  • (6)通过重排 Mach-O中的二进制,减少启动流程中的缺页中断次数(Page Fault)

小知识点:+load()+initialize()两者的区别?

+load()方法会在main()函数调用前就调用,而+initialize()是在类第一次使用时才会调用。 +load方法的调用优先级: 父类 > 子类 > 分类,并且不会被覆盖,均会调用。 +load方法是在main() 函数之前调用,所有的类文件都会加载,包括分类也会加载。+initialize方法的调用优先级:分类 > 子类,父类 > 子类。(父类的分类重写了+initialize方法会覆盖父类的+initialize方法)

2,main函数执行后: main函数执行后的阶段,指的是:从 main 函数执行开始,到 appDelegate didFinishLaunchingWithOptions方法里首屏渲染相关方法执行完成。即: 从main函数执行到设置self.window.rootViewController执行完成的阶段

  • 首屏初始化所需配置文件的读写操作;
  • 首屏列表大数据的读取;
  • 首屏渲染的大量计算;

iOs App Launch Sequence.png

此过程,时间统计可以如下: 在main函数文件下:

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

CFAbsoluteTime startTime;

int main(int argc, char * argv[]) {
   startTime = CFAbsoluteTimeGetCurrent();
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

AppDelegate.h里面

#import <UIKit/UIKit.h>
extern CFAbsoluteTime startTime;
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@end

AppDelegate.m里面

#import "AppDelegate.h"
@interface AppDelegate ()
@end

@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    double launchTime =  CFAbsoluteTimeGetCurrent() - startTime;
    NSLog(@"main()阶段测量时间launchTime-----:  %.2f秒",launchTime);
    return YES;
}

最终打印:

main()阶段测量时间launchTime-----: 0.06

那么针对此过程main() 函数执行后的优化项的优化方案:

  • 减少在主线程中执行IO读写操作.
  • 将各种SDK(二方、三方)初使化工作放到子线程处理.
  • 减少首屏渲染的大量计算.

首屏渲染完成后的的优化方案: 这个阶段用户已经能够看到 App 的首页信息了,所以优化的优先级排在最后。但是,那些会卡住主线程的方法还是需要最优先处理的,不然还是会影响到用户后面的交互操作。