看到这篇启动优化,让你的App有顺滑无比的启动速度~~

11,038 阅读8分钟

为什么要做启动优化

1.APP的启动速度是直接影响用户体验的关键因素 2.随着APP的迭代和程序员的懈怠,三方库的依赖越来越多,自定义的category越来越多,重复的方法越来越多,会直接影响APP的启动时间

APP的启动时间指的是什么

TA(App总启动时间) = T1(main()之前的加载时间) + T2(main()之后的加载时间)

T1 = 系统dylib(动态链接库)和自身App可执行文件的加载

T2 = main方法执行之后到Delegate类中的didFinishLaunchingWithOptions方法执行结束前这段时间(Z这段时间主要是APP在构建第一个界面,并完成渲染展示)

你的APP启动时间合格吗?

使用砸壳和MokeyApp这种利器,我们先看几款APP的启动时间

Total pre-main time: 642.93 milliseconds (100.0%)
         dylib loading time: 201.45 milliseconds (31.3%)
        rebase/binding time: 131.51 milliseconds (20.4%)
            ObjC setup time:  67.38 milliseconds (10.4%)
           initializer time: 242.33 milliseconds (37.6%)
           slowest intializers :
             libSystem.B.dylib :   4.45 milliseconds (0.6%)
    libMainThreadChecker.dylib :  28.27 milliseconds (4.3%)
          libglInterpose.dylib :  63.57 milliseconds (9.8%)
         libMTLInterpose.dylib :  14.84 milliseconds (2.3%)
                  RevealServer :  48.39 milliseconds (7.5%)
        libdingdingDylib.dylib :  30.33 milliseconds (4.7%)
                      DT :  95.09 milliseconds (14.7%)

Total pre-main time: 1.1 seconds (100.0%)
         dylib loading time: 230.74 milliseconds (20.8%)
        rebase/binding time:  66.30 milliseconds (5.9%)
            ObjC setup time:  77.95 milliseconds (7.0%)
           initializer time: 732.20 milliseconds (66.1%)
           slowest intializers :
             libSystem.B.dylib :   7.86 milliseconds (0.7%)
    libMainThreadChecker.dylib :  25.69 milliseconds (2.3%)
          libglInterpose.dylib : 307.84 milliseconds (27.7%)
         libMTLInterpose.dylib :  77.95 milliseconds (7.0%)
  libViewDebuggerSupport.dylib :  22.51 milliseconds (2.0%)
                  RevealServer :  94.45 milliseconds (8.5%)
              libKKDylib.dylib :  38.06 milliseconds (3.4%)
                       K : 321.95 milliseconds (29.0%)
Total pre-main time: 873.18 milliseconds (100.0%)
         dylib loading time: 236.80 milliseconds (27.1%)
        rebase/binding time: 198.80 milliseconds (22.7%)
            ObjC setup time:  98.38 milliseconds (11.2%)
           initializer time: 338.96 milliseconds (38.8%)
           slowest intializers :
             libSystem.B.dylib :   9.01 milliseconds (1.0%)
    libMainThreadChecker.dylib :  26.69 milliseconds (3.0%)
          libglInterpose.dylib : 111.85 milliseconds (12.8%)
         libMTLInterpose.dylib :  42.68 milliseconds (4.8%)
  libViewDebuggerSupport.dylib :  19.92 milliseconds (2.2%)
             TBSharedFramework :  43.33 milliseconds (4.9%)
                  RevealServer :  40.34 milliseconds (4.6%)
              libTBDylib.dylib :  29.17 milliseconds (3.3%)
                            TB :  50.24 milliseconds (5.7%)

我们可以看到各种APP的启动时间千差万别,当启动时间大于n 秒的时候用户会感觉明显的等待。当然这个启动时间到底为多少合适因人而异,不过除了某些方面,APP冷启动速度这种东西当然是越快越好

如何查看自己APP的启动时间呢?

只需要在 Edit scheme -> Run -> Environment Variables 中将环境变量 DYLD_PRINT_STATISTICS 设为 1,就可以看到 main 之前各个阶段的时间消耗

Total pre-main time: 537.96 milliseconds (100.0%)
         dylib loading time: 280.17 milliseconds (52.0%)
        rebase/binding time:  26.71 milliseconds (4.9%)
            ObjC setup time:  16.39 milliseconds (3.0%)
           initializer time: 214.18 milliseconds (39.8%)
           slowest intializers :
             libSystem.B.dylib :   3.62 milliseconds (0.6%)
    libMainThreadChecker.dylib :  21.47 milliseconds (3.9%)
          libglInterpose.dylib :  62.91 milliseconds (11.6%)
         libMTLInterpose.dylib :  30.22 milliseconds (5.6%)
                           Arm : 122.19 milliseconds (22.7%)

当然我们也可以获取更详细的时间,只需将环境变量 DYLD_PRINT_STATISTICS_DETAILS 设为 1 就可以

  total time: 5.2 seconds (100.0%)
  total images loaded:  467 (440 from dyld shared cache)
  total segments mapped: 73, into 6990 pages with 292 pages pre-fetched
  total images loading time: 4.7 seconds (91.4%)
  total load time in ObjC:  12.35 milliseconds (0.2%)
  total debugger pause time: 4.4 seconds (84.3%)
  total dtrace DOF registration time:   0.34 milliseconds (0.0%)
  total rebase fixups:  334,609
  total rebase fixups time:  25.78 milliseconds (0.4%)
  total binding fixups: 576,887
  total binding fixups time: 199.48 milliseconds (3.7%)
  total weak binding fixups time:   2.79 milliseconds (0.0%)
  total redo shared cached bindings time: 199.53 milliseconds (3.8%)
  total bindings lazily fixed up: 0 of 0
  total time in initializers and ObjC +load: 209.92 milliseconds (3.9%)
                         libSystem.B.dylib :   3.53 milliseconds (0.0%)
                libMainThreadChecker.dylib :  20.80 milliseconds (0.3%)
                      libglInterpose.dylib :  64.61 milliseconds (1.2%)
                     libMTLInterpose.dylib :  22.88 milliseconds (0.4%)
                                  linphone :  15.46 milliseconds (0.2%)
                                       Arm : 116.17 milliseconds (2.2%)
total symbol trie searches:    1429688
total symbol table binary searches:    0
total images defining weak symbols:  52
total images using weak symbols:  121

根据所见即所得的原则,我们可以看到APP的冷启动大概需要四个步骤: dylib loading、rebase/binding、ObjC setup、initializers,这四个步骤对应的是我们上边提到的T1时间(main()之前的加载时间) 那么我们可以认为将上边的四个步骤优化一下,我们就可以提高部分APP的启动速度了。那么这四步分别做了什么呢?这里让我们先盗个图...

提高main()函数之前的加载时间?

了解完毕mian()函数之前加载的步骤后,我们可以简单的分析出影响T1时间的各种因素:

1.动态库加载越多,启动越慢。
2.ObjC类,方法越多,启动越慢。
3.ObjC的+load越多,启动越慢。
4.C的constructor函数越多,启动越慢。
5.C++静态对象越多,启动越慢。

APP启动T1阶段对症下药

1.删除无用代码,合并一些同样功能的类

删除类的方法:ocjc_cover

通过对Mach-O文件的了解,可以知道__TEXT:__objc_methname:中包含了代码中的所有方法,而__DATA__objc_selrefs中则包含了所有被使用的方法的引用,通过取两个集合的差集就可以得到所有未被使用的代码

def referenced_selectors(path):
    re_sel = re.compile("__TEXT:__objc_methname:(.+)") //获取所有方法
    refs = set()
    lines = os.popen("/usr/bin/otool -v -s __DATA __objc_selrefs %s" % path).readlines() # ios & mac //真正被使用的方法
    for line in lines:
        results = re_sel.findall(line)
        if results:
            refs.add(results[0])
    return refs
}

对Mach-O文件的组成感兴趣的同学可以阅读以下这篇文章 Mach-O

2.+load优化

大部分APP因为业务需求或者一些奇淫巧技的关系,多多少少的回使用+load方法来执行一些操作,但是并不是每个方法都需要在+load那么早。部分操作可以延迟到+initialize中

3.减少framework的使用,动态链接比较耗时

4.尽量不要用C++虚函数(创建虚函数表有开销)

APP启动T2阶段

这里我们再重提一下我们对于T2阶段的定义

T2 = main方法执行之后到Delegate类中的didFinishLaunchingWithOptions方法执行结束前这段时间(Z这段时间主要是APP在构建第一个界面,并完成渲染展示)

而在这个阶段随着业务的开发,我们可能会一股脑的把所有问题都堆在这里


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    [self setupNetwork];
    [self startTabbarControlService];
    [self startUserDefaultService];
    [self startPANEL];
    [self startUIToastService];
    [self startRootRouter];
    [self startBackgroundRefreshService];
    [self start3DTouchService:application];
    [self setUpNotification];
    [self startDebuggerService];
     return YES;
}

而过多无用的启动项很明显会拖累我们App的启动速度,而我们需要做的便是分析整个项目的业务需求以及架构设计,将所有的启动项分门别类,然后在不同的阶段分别启动

// AppDelegate
- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
    // 性能检测以及Crash
    [self setUPCrashAndPerformanceModule]
    // 统计信息上报
    [self setUPStatisticsInfoModule]
}
// AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 网络
    [self setupNetwork];
    // 基础信息
    [self setUPBaseInfo];
    // 基础依赖的SDK
    [self setUPBaseSDKMoudle];

    return YES;
}

// MainVC
- (void)viewDidLoad {
    [super viewDidLoad];
    // debug面板
    [self setUPDebuggerService];
    // 自定义配置信息
    [self setCustomConfiguration];
    // 等等
}

上面只是单纯罗列了一下具体操作的方法,但是App的业务千差万别,我们还需要具体问题具体分析。

APP启动T2阶段对症下药

1.项目自启动

这里可能有些同学要提问了,我们项目做了组件化,不同的组件可能在不同的项目里启动时间不同,这个问题怎么解决呢?如何做到组件可插拔呢?所以我们需要用到一个叫做"项目自启动"的技术,而这里核心用到的"_DATA"段,"_DATA"可以涵盖App启动的所有阶段(包括main函数之前)

具体的原理呢,大家可以直接阅读这篇文章 利用__attribute__((section()))构建初始化函数表,这里就不再做赘述了

这里为大家提供OC版本的自启动项工具ONLDynamicLoader

//
//  ONLDynamicLoader.h
//  ONLDynamicLoader
//
//  Created by only on 2019/10/14.
//  Copyright © 2019 only. All rights reserved.
//

#import <Foundation/Foundation.h>

static char * LEVEL_A = "LEVEL_A";
static char * LEVEL_B = "LEVEL_B";
static char * LEVEL_C = "LEVEL_C";

typedef void (*ONLDynamicLoaderInjectFunction)(void);

#define CRDYML_SEGMENTNAME "__DATA"
#define CRDYML_ATTRIBUTE(sectionName) __attribute((used, section(CRDYML_SEGMENTNAME "," #sectionName )))

#define CRDYML_FUNCTIONS_EXPORT_BEGIN(KEY) \
static void CRDYML_INJECT_##KEY##_FUNCTION(void){

#define CRDYML_FUNCTIONS_EXPORT_END(KEY) \
} \
static ONLDynamicLoaderInjectFunction dymlLoader##KEY##function CRDYML_ATTRIBUTE(KEY) = CRDYML_INJECT_##KEY##_FUNCTION;


NS_ASSUME_NONNULL_BEGIN

@interface ONLDynamicLoader : NSObject

+ (void)executeFunctionsForKey:(char *)key;

@end

NS_ASSUME_NONNULL_END

//
//  ONLDynamicLoader.m
//  ONLDynamicLoader
//
//  Created by only on 2019/10/14.
//  Copyright © 2019 only. All rights reserved.
//

#import "ONLDynamicLoader.h"
#include <mach-o/getsect.h>
#include <mach-o/loader.h>
#include <mach-o/dyld.h>
#include <dlfcn.h>

static void ONLDynamicLoader_invoke_method(void *key){
    Dl_info info;
    int ret = dladdr(ONLDynamicLoader_invoke_method, &info);
    if(ret == 0){
        // fatal error
    }
#ifndef __LP64__
    const struct mach_header *mhp = (struct mach_header*)info.dli_fbase;
    unsigned long size = 0;
    uint32_t *memory = (uint32_t*)getsectiondata(mhp, QWLoadableSegmentName, QWLoadableSectionName, & size);
#else /* defined(__LP64__) */
    const struct mach_header_64 *mhp = (struct mach_header_64*)info.dli_fbase;
    unsigned long size = 0;
    uint64_t *memory = (uint64_t*)getsectiondata(mhp, CRDYML_SEGMENTNAME, key, &size);
#endif /* defined(__LP64__) */
    
    for(int idx = 0; idx < size/sizeof(void*); ++idx){
        ONLDynamicLoaderInjectFunction func = (ONLDynamicLoaderInjectFunction)memory[idx];
        func(); //crash tofix
    }
}


@implementation ONLDynamicLoader

+ (void)executeFunctionsForKey:(char *)key
{
    ONLDynamicLoader_invoke_method(key);
}
// 示例 编译阶段注册的启动项
CRDYML_FUNCTIONS_EXPORT_BEGIN(LEVEL_A)
NSLog(@"=====LEVEL_A==========");
CRDYML_FUNCTIONS_EXPORT_END(LEVEL_A)

CRDYML_FUNCTIONS_EXPORT_BEGIN(LEVEL_B)
NSLog(@"=====LEVEL_B==========");
CRDYML_FUNCTIONS_EXPORT_END(LEVEL_B)

CRDYML_FUNCTIONS_EXPORT_BEGIN(LEVEL_C)
NSLog(@"=====LEVEL_C==========");
CRDYML_FUNCTIONS_EXPORT_END(LEVEL_C)

@end

利用这个工具我们可以很轻松的做到在合适的阶段声明启动项的启动阶段

// AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 所有注册到A的启动项将会在这个阶段启动
    [ONLDynamicLoader executeFunctionsForKey:LEVEL_A];
    return YES;
}
// MainVC
- (void)viewDidLoad {
    [super viewDidLoad];
    // 所有注册到B的启动项将会在这个阶段启动
    [ONLDynamicLoader executeFunctionsForKey:LEVEL_B];
    
}

其他的组件在内部注册自己需要的启动阶段

//
//  Created by only on 2019/10/14.
//  Copyright © 2019 only. All rights reserved.
//

#import "ONLMoudleA.h"
#import "ONLDynamicLoader.h"


@implementation ONLMoudleA

+ (instancetype)shareMoudeleA {
    static id shareMoudeleA = nil;
    static dispatch_once_t onceToken = 0;
    dispatch_once(&onceToken, ^{
        // alloc & init work
        shareMoudeleA = [[self alloc]init];
    });
    
    return shareMoudeleA;
}

- (void)setUP{
    NSLog(@"ONLMoudleA 启动了");
}
// 根据实际的项目需求做到可插拔、解耦合、可复用等等等的问题
CRDYML_FUNCTIONS_EXPORT_BEGIN(LEVEL_B)
[[ONLMoudleA shareMoudeleA]setUP];
CRDYML_FUNCTIONS_EXPORT_END(LEVEL_B)

@end

而到这里App的启动优化其实已经完成了2/3,有些同学可能会问了WTF说好的

TA(App总启动时间) = T1(main()之前的加载时间) + T2(main()之后的加载时间)

怎么还有1/3没有完成呢???其实呢,还有一个T3时间无论是闪屏页和首页的网络请求还是数据加载都是耗时操作,只有用户眼睛能够看到界面并且开始操作才算是真正的完成App启动。即:

TA(App总启动时间) = T1(main()之前的加载时间) + T2(main()之后的加载时间) +T3(首页数据加载+闪屏页数据同步)

针对T3的优化较为复杂了,需要针对接口UI等等方面去考虑,有兴趣的同学可以参考分析支付宝的做法,嗯,后期会补上。(一个巨型的App竟然有着鬼一样的启动速度!!)

嗯,这篇博客就暂到这里,写的比较快,有错误的地方或者理解错误的地方希望大家指正,一起进步一起学习!

参考资料

1.iOS App冷启动治理:来自美团外卖的实践

2.今日头条iOS客户端启动速度优化

3.廖威雄: 利用__attribute__((section()))构建初始化函数表与Linux内核init的实现