iOS启动速度优化

10/31/2018 iOS

有一段时间没有关注公司App的启动状况了,今天公司PM反馈App启动速度非常慢,让帮忙协助排查下问题。这里记录下整个过程。并涉及到一些大致的优化方法。

以下所有数据均采用iPhone 6 plus测试,性能好的手机会更快,比如iPHone X MAX

# 应用启动流程

iOS应用的启动可分为pre-main阶段和main()阶段,其中系统做的事情依次是:

  1. pre-main阶段

    1.1. 加载应用的可执行文件
    1.2. 加载动态链接库加载器dyld(dynamic loader)
    1.3. dyld递归加载应用所有依赖的dylib(dynamic library 动态链接库)

  2. main()阶段

    2.1. dyld调用main()
    2.2. 调用UIApplicationMain()
    2.3. 调用applicationWillFinishLaunching
    2.4. 调用didFinishLaunchingWithOptions

# 启动时间计算

App总启动时间 = pre-main()加载时间 + main()加载时间

# 启动耗时的测量

# pre-main

对于pre-main阶段,Apple提供了一种测量方法,在 Xcode 中 Edit scheme -> Run -> Auguments 将环境变量DYLD_PRINT_STATISTICS 设为1 :

DYLD_PRINT_STATISTICS

第一次冷启动

Total pre-main time: 4.4 seconds (100.0%)
         dylib loading time: 3.0 seconds (68.5%)
        rebase/binding time: 269.59 milliseconds (6.1%)
            ObjC setup time: 644.33 milliseconds (14.6%)
           initializer time: 473.18 milliseconds (10.7%)
           slowest intializers :
             libSystem.B.dylib :  32.77 milliseconds (0.7%)
    libMainThreadChecker.dylib : 134.16 milliseconds (3.0%)
1
2
3
4
5
6
7
8

第二次热启动

 Total pre-main time: 4.1 seconds (100.0%)
         dylib loading time: 2.9 seconds (72.5%)
        rebase/binding time: 253.23 milliseconds (6.1%)
            ObjC setup time: 430.37 milliseconds (10.4%)
           initializer time: 447.22 milliseconds (10.8%)
           slowest intializers :
             libSystem.B.dylib :  33.03 milliseconds (0.8%)
    libMainThreadChecker.dylib :  84.15 milliseconds (2.0%)
                  AFNetworking : 106.04 milliseconds (2.5%)
1
2
3
4
5
6
7
8
9

第三次热启动

Total pre-main time: 3.7 seconds (100.0%)
         dylib loading time: 2.9 seconds (78.2%)
        rebase/binding time: 232.80 milliseconds (6.1%)
            ObjC setup time: 204.22 milliseconds (5.4%)
           initializer time: 379.50 milliseconds (10.1%)
           slowest intializers :
             libSystem.B.dylib :  27.56 milliseconds (0.7%)
                  AFNetworking : 122.75 milliseconds (3.2%)
1
2
3
4
5
6
7
8

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

# main

对于main阶段的启动时间测量,可以在Application组件中新建一个AppStartConstants.h文件声明全局变量CFAbsoluteTime MainTime;(也可以在其他地方,只要能够被引入即可)。

  1. 在main.m文件中引入并获取当前时间:

    extern CFAbsoluteTime MainTime;
    
    int main(int argc, char * argv[])
    {
        @autoreleasepool {
            MainTime = CFAbsoluteTimeGetCurrent();
    
    
    1
    2
    3
    4
    5
    6
    7
  2. - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions函数最后获取当前时间并计算差值

    extern CFAbsoluteTime MainTime;
    
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
    	// 其他代码
        double launchTime = (CFAbsoluteTimeGetCurrent() - MainTime);
        NSLog(@"Total main time: %.2f second", launchTime);
        return YES;
    }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    第一次冷启动获取到的启动时间:Total main time: 7.32 second
    第二次热启动获取到的启动时间:Total main time: 6.52 second
    第三次热启动获取到的启动时间:Total main time: 6.28 second

简单写了个记录时间的工具,后面所有时间都是基于这个工具记录和输出的,代码如下:

//
//  AppStartTime.h
//
//  Created by wanyakun on 2018/11/2.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface AppStartTime : NSObject

@property (nonatomic, assign) CFAbsoluteTime mainTime;

+ (instancetype)shareInstance;

+ (void)logTimeWithMessage:(NSString *)message;

+ (void)printLogMessage;

@end

NS_ASSUME_NONNULL_END
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//
//  AppStartTime.m
//
//  Created by wanyakun on 2018/11/2.
//

#import "AppStartTime.h"

@interface AppStartTime ()

@property (nonatomic, assign) CFAbsoluteTime lastTime;

@property (nonatomic, strong) NSMutableArray *messageArray;

@end

@implementation AppStartTime

+ (instancetype)shareInstance {
    static AppStartTime *instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[AppStartTime alloc] init];
    });
    return instance;
}

- (instancetype)init {
    if (self = [super init]) {
        _mainTime = CFAbsoluteTimeGetCurrent();
        _lastTime = _mainTime;
        _messageArray = [NSMutableArray arrayWithCapacity:5];
    }
    return self;
}

+ (void)logTimeWithMessage:(NSString *)message {
    [[AppStartTime shareInstance] logTimeWithMessage:message];
}

- (void)logTimeWithMessage:(NSString *)message {
    CFAbsoluteTime current = CFAbsoluteTimeGetCurrent();

    CFTimeInterval duration = current - _lastTime;
    NSString *startMessage = nil;
    if (duration >= 1) {
        startMessage = [NSString stringWithFormat:@"%@ time: %.1f seconds", message, duration];
    } else {
        startMessage = [NSString stringWithFormat:@"%@ time: %.2f milliseconds", message, duration*1000];
    }
    [_messageArray addObject:startMessage];
    _lastTime = current;
}

+ (void)printLogMessage {
    [[AppStartTime shareInstance] printLogMessage];
}

- (void)printLogMessage {
    
    double launchTime = (CFAbsoluteTimeGetCurrent() - self.mainTime);
    NSLog(@"Total main time: %.4f second", launchTime);
    
    [_messageArray enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSLog(@"%@", obj);
    }];  
}

@end
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

# 启动时间优化

# pre-main优化

看第一次冷启动的时间如下:

Total pre-main time: 4.4 seconds (100.0%)
         dylib loading time: 3.0 seconds (68.5%)
        rebase/binding time: 269.59 milliseconds (6.1%)
            ObjC setup time: 644.33 milliseconds (14.6%)
           initializer time: 473.18 milliseconds (10.7%)
           slowest intializers :
             libSystem.B.dylib :  32.77 milliseconds (0.7%)
    libMainThreadChecker.dylib : 134.16 milliseconds (3.0%)
1
2
3
4
5
6
7
8
  1. dylib loading

    这一阶段dyld会分析应用依赖的dylib,找到其mach-o文件,打开和读取这些文件并验证其有效性,接着会找到代码签名注册到内核,最后对dylib的每一个segment调用mmap()。一般情况下,iOS应用会加载100-400个dylibs,其中大部分是系统库,这部分dylib的加载系统已经做了优化。

    所以,依赖的dylib越少越好。在这一步,我们可以做的优化有:

    • 尽量不使用内嵌(embedded)的dylib,加载内嵌dylib性能开销较大
    • 合并已有的dylib和使用静态库(static archives),减少dylib的使用个数

    由于App中有一百多个组件,而且都是动态Framework,所以会出现 dylib loading time: 3.0 seconds (68.5%) 所以尽量不使用内嵌的dylib是不太现实的。我们能做的是对现有组件进行分析,尽可能减少组件个数。

    • 去掉没有使用的系统lib
    • 去掉已经废弃的组件、类库
    • 功能重复的组件,只保留一个,比如json等第三方组件(swift和Objective-C的)
    • 合并功能类似的组件、分类,比如Utils等
    • 优化现有的组件,比如RN只有一个风险测评在使用,如果未来一段时间不打算使用,可以降级到H5,RN组件依赖的部分也会相应减少,考虑MVVM用到的RX组件是否继续使用
  2. rebase/binding

    在dylib的加载过程中,系统为了安全考虑,引入了ASLR(Address Space Layout Randomization)技术和代码签名。由于ASLR的存在,镜像(Image,包括可执行文件、dylib和bundle)会在随机的地址上加载,和之前指针指向的地址(preferred_address)会有一个偏差(slide),dyld需要修正这个偏差,来指向正确的地址。Rebase在前,Bind在后,Rebase做的是将镜像读入内存,修正镜像内部的指针,性能消耗主要在IO。Bind做的是查询符号表,设置指向镜像外部的指针,性能消耗主要在CPU计算。

    所以,指针数量越少越好。在这一步,我们可以做的优化有:

    • 减少ObjC类(class)、方法(selector)、分类(category)的数量
    • 减少C++虚函数的的数量(创建虚函数表有开销)
    • 使用Swift structs(内部做了优化,符号数量更少)
  3. ObjC setup

    大部分ObjC初始化工作已经在Rebase/Bind阶段做完了,这一步dyld会注册所有声明过的ObjC类,将分类插入到类的方法列表里,再检查每个selector的唯一性。

    在这一步倒没什么优化可做的,Rebase/Bind阶段优化好了,这一步的耗时也会减少。

  4. initializer

    到了这一阶段,dyld开始运行程序的初始化函数,调用每个Objc类和分类的+load方法,调用C/C++ 中的构造器函数(用__attribute__((constructor))修饰的函数),和创建非基本类型的C++静态全局变量。Initializers阶段执行完后,dyld开始调用main()函数。

    在这一步,我们可以做的优化有:

    • 少在类的+load方法里做事情,尽量把这些事情推迟到+initiailize (目前有容器类防护)
    • 减少构造器函数个数,在构造器函数里少做些事情 (目前注册路由、Handler和神策埋点)
    • 减少C++静态全局变量的个数

总结执行方法:

  • 排查无用的dylib,移除不再使用的lib
  • 删除无用的组件、类库,合并重复的文件、分类,移除功能重复的第三方库或者通用业务组
  • 梳理各个类的+load方法,将多个类中+load方法做的事延迟到+initiailize里去做。

TODO: 记录优化后的耗时,做对比

# main优化

这一阶段的优化主要是减少didFinishLaunchingWithOptions方法里的工作,在didFinishLaunchingWithOptions方法里,我们会创建应用的window,指定其rootViewController,调用window的makeKeyAndVisible方法让其可见。由于业务需要,我们会初始化各个二方/三方库,检查是否需要显示引导页、是否需要登录、是否有新版本等,由于历史原因,这里的代码容易变得比较庞大,启动耗时难以控制。

所以,满足业务需要的前提下,didFinishLaunchingWithOptions在主线程里做的事情越少越好。在这一步,我们可以做的优化有:

  • 梳理各个二方/三方库,找到可以延迟加载的库,做延迟加载处理,比如放到MainTabBarController的viewDidAppear方法里。
  • 梳理业务逻辑,把可以延迟执行的逻辑,做延迟执行处理。比如检查新版本、注册推送通知等逻辑。
  • 避免复杂/多余的计算。
  • 避免在首页控制器的viewDidLoad和viewWillAppear做太多事情,这2个方法执行完,首页控制器才能显示,部分可以延迟创建的视图应做延迟创建/懒加载处理。

# 实践

App完整的启动时间(main阶段时间为总耗时,每一步的耗时通过减去上一步就可以算出)

	Total pre-main time: 4.0 seconds (100.0%)
         dylib loading time: 3.1 seconds (77.7%)
        rebase/binding time: 244.99 milliseconds (6.0%)
            ObjC setup time: 231.28 milliseconds (5.7%)
           initializer time: 422.69 milliseconds (10.4%)
2018-10-31 20:20:57.832445+0800 Lender[16596:1060700] didFinishLaunch time: 0.31 second
2018-10-31 20:20:57.834161+0800 Lender[16596:1060700] window create time: 0.31 second
2018-10-31 20:20:57.844925+0800 Lender[16596:1060700] AppConfigBusiness time: 0.32 second
2018-10-31 20:20:57.896577+0800 Lender[16596:1060700] AppConfigNetworking time: 0.37 second
2018-10-31 20:20:57.901165+0800 Lender[16596:1060700] AppConfigBootingProtection time: 0.38 second
[16596:1060700] AppConfigRouter time: 0.38 second
2018-10-31 20:20:57.963292+0800 Lender[16596:1060700] LoadLocalToken time: 0.44 second
2018-10-31 20:20:57.963436+0800 Lender[16596:1060700] DataCompatibility time: 0.44 second
2018-10-31 20:20:58.320243+0800 Lender[16596:1060700] AppDelegateNotificationsManager time: 0.80 second
2018-10-31 20:20:58.334090+0800 Lender[16596:1060700] BIZConfigCenter checkUpdate time: 0.81 second
2018-10-31 20:20:58.338521+0800 Lender[16596:1060700] BLLBSManager time: 0.81 second
2018-10-31 20:20:58.340121+0800 Lender[16596:1060700] LCLocationManager time: 0.82 second
2018-10-31 20:20:58.648829+0800 Lender[16596:1060700] AppConfigShenCe time: 1.12 second
2018-10-31 20:20:58.846310+0800 Lender[16596:1060700] AppConfigUMTG time: 1.32 second
2018-10-31 20:21:03.565177+0800 Lender[16596:1060700] UMShareTool configUMShare time: 6.04 second
2018-10-31 20:21:03.566030+0800 Lender[16596:1060700] BIZPush start time: 6.04 second
2018-10-31 20:21:03.566481+0800 Lender[16596:1060700] AppConfigTingYun time: 6.04 second
2018-10-31 20:21:03.587389+0800 Lender[16596:1060700] ConfigAPM time: 6.06 second
2018-10-31 20:21:03.587538+0800 Lender[16596:1060700] AppConfigBanner time: 6.06 second
2018-10-31 20:21:03.612676+0800 Lender[16596:1060700] SaveUserAgent time: 6.09 second
2018-10-31 20:21:03.616083+0800 Lender[16596:1060700] Config BeeHive time: 6.09 second
2018-10-31 20:21:03.619002+0800 Lender[16596:1060700] super didFinishLaunchingWithOptions time: 6.09 second
2018-10-31 20:21:03.781076+0800 Lender[16596:1060700] config root vc time: 6.26 second
2018-10-31 20:21:03.794603+0800 Lender[16596:1060700] ShowAdvertisement time: 6.27 second
2018-10-31 20:21:03.797242+0800 Lender[16596:1060700] AppConfigInfoCollect time: 6.27 second
2018-10-31 20:21:03.803755+0800 Lender[16596:1060700] UIWebView thread time: 6.28 second
2018-10-31 20:21:03.803853+0800 Lender[16596:1060700] Total main time: 6.28 second
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

# pre-main阶段优化

  1. 要开发人员配合,主要靠减少组件、合并功能重复代码,删除无用代码来做。
  2. 所有Framework动态库转static library静态库。dylib loading time: 3.1 seconds (77.7%) 这一部分基本上可以完全优化掉
  3. 二进制重排。目的是减少Page Fault, 做法就是通过Clang插桩获取到启动到首页加载完成所执行的函数,然后写入到order文件,最后壳工程配置下order文件。具体做法详见:ios启动优化:二进制重排 (opens new window)

下面为优化后的效果:

pre-main

# main阶段优化

从中可以看出UMShareTool configUMShare time占用了将近5s的时间,而且是非启动必须配置的业务,而main函数中Debug组件启动时间也较长大概0.3s。超过100ms的配置都需要考虑优化。

我们建立一个队列,将一些不需要必须在主线程中启动的配置放到队列中中去配置(比如BootingProtection、友盟、TalkingData、友盟分享、设置UserAgent)。

另外删除webkit启动的代码,因为在设置UserAgent中已经创建UIWebView实例,启动了webkit

注意:听云要去必须在主线程中启动,神策目前不确定是否需要放到队列中(暂时先放到了队列中)

main时间初步优化后的结果(优化了时间输出格式,每一步为实际用时):

2018-10-31 17:59:27.987630+0800 Lender[19074:1207484] Total main time: 1.02 second
2018-10-31 17:59:27.988138+0800 Lender[19074:1207484] didFinishLaunch time: 108.54 milliseconds
2018-10-31 17:59:27.988450+0800 Lender[19074:1207484] window create time: 1.07 milliseconds
2018-10-31 17:59:27.988836+0800 Lender[19074:1207484] AppConfigBusiness time: 12.09 milliseconds
2018-10-31 17:59:27.989111+0800 Lender[19074:1207484] AppConfigNetworking time: 78.91 milliseconds
2018-10-31 17:59:27.989376+0800 Lender[19074:1207484] AppConfigBootingProtection time: 0.04 milliseconds
2018-10-31 17:59:27.989648+0800 Lender[19074:1207484] AppConfigRouter time: 0.41 milliseconds
2018-10-31 17:59:27.989894+0800 Lender[19074:1207484] LoadLocalToken time: 1.23 milliseconds
2018-10-31 17:59:27.990151+0800 Lender[19074:1207484] DataCompatibility time: 0.05 milliseconds
2018-10-31 17:59:27.990419+0800 Lender[19074:1207484] AppConfigTingYun time: 392.52 milliseconds
2018-10-31 17:59:27.990666+0800 Lender[19074:1207484] AppConfigShenCe time: 0.31 milliseconds
2018-10-31 17:59:27.990908+0800 Lender[19074:1207484] NotificationsManager time: 44.76 milliseconds
2018-10-31 17:59:27.991193+0800 Lender[19074:1207484] BIZConfigCenter time: 19.47 milliseconds
2018-10-31 17:59:27.991453+0800 Lender[19074:1207484] BLLBSManager time: 3.44 milliseconds
2018-10-31 17:59:27.991708+0800 Lender[19074:1207484] LCLocationManager time: 1.47 milliseconds
2018-10-31 17:59:27.991961+0800 Lender[19074:1207484] AppConfigUMTG time: 8.83 milliseconds
2018-10-31 17:59:27.992201+0800 Lender[19074:1207484] ConfigUMShare time: 0.04 milliseconds
2018-10-31 17:59:27.992409+0800 Lender[19074:1207484] BIZPush start time: 0.82 milliseconds
2018-10-31 17:59:27.992463+0800 Lender[19074:1207484] ConfigAPM time: 28.12 milliseconds
2018-10-31 17:59:27.992495+0800 Lender[19074:1207484] AppConfigBanner time: 0.18 milliseconds
2018-10-31 17:59:27.992596+0800 Lender[19074:1207484] Config BeeHive time: 2.78 milliseconds
2018-10-31 17:59:27.992894+0800 Lender[19074:1207484] Super finishLaunching time: 1.40 milliseconds
2018-10-31 17:59:27.992949+0800 Lender[19074:1207484] config root vc time: 268.16 milliseconds
2018-10-31 17:59:27.993012+0800 Lender[19074:1207484] ShowAdvertisement time: 47.51 milliseconds
2018-10-31 17:59:27.993153+0800 Lender[19074:1207484] AppConfigInfoCollect time: 1.32 milliseconds
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

目前main阶段只有听云和RootViewController 的创建用时比较长,听云必须在主线程启动这个没办法优化, RootViewController 要到对应的组件再做优化。可以看到热启动main阶段已经做到1秒左右。
其实main阶段还有很多业务组件的初始化可以优化,需要深入到每个业务组件中去优化,后续可以建立组件准入机制,将其影响到App启动的时间作为一项衡量指标,超过100毫秒就需要在集成的时候做告警。

至此App整体启动速度pre-main+main阶段在1.6s左右

Last Updated: 1/15/2023, 2:48:14 PM