面试合集iOS 开发优化点

iOS 性能调优之启动时间优化

2020-09-01  本文已影响0人  TIGER_XXXX

目的

从点击 App 图标到加载 App 闪屏之间会有个动画,我们希望 App 启动速度比这个动画更快。需要注意的是启动时间一旦超过 20s,系统会认为发生了死循环并杀掉 App 进程。当然启动时间最好以 App 所支持的最低配置设备为准。

启动时间的划分

启动时间可以拆分为两个部分

t1

  1. Run -> Arguments -> Enviroment Variables -> 添加字段{DYLD_PRINT_STATISTICS:Yes}
  1. 重新运行项目,会在控制台看到项目的per-main time

1. dylib loading time

这一阶段主要在load dylibs image(这里的image表示镜像)
在每一个动态库加载过程中dyld需要做下面几步

通常,一个App需要加载100到400个dylibs, 但是其中的系统库被优化,可以很快的加载.针对这一步骤的优化有:

2. rebase/binding time

由于ASLR(address space layout randomization)的存在,可执行文件和动态链接库在虚拟内存中的加载地址每次启动都不固定,所以需要这2步来修复镜像中的资源指针,来指向正确的地址. rebase修复的是指向当前镜像内部的资源指针, 而bind指向的是镜像外部的资源指针.
rebase步骤先进行,需要把镜像读入内存,并以page为单位进行加密验证,保证不会被篡改,所以这一步的瓶颈在IO.bind在其后进行,由于要查询符号表,来指向跨镜像的资源,加上在rebase阶段,镜像已被读入和加密验证,所以这一步的瓶颈在于CPU计算.
通过下面方法查看相关的资源指针

xcrun dyldinfo -rebase -bind -lazy_bind xxx.app/xxx

该阶段的优化关键在于减少__DATA指针的数量
可优化的操作有以下几点

3. Objc setup time

这一步主要做了以下操作

由于前面两步的处理,这里已经没有什么可以优化的了

4. initializer time

以上三步属于静态调整(fix-up),都是在修改__DATA segment中的内容,而这里则开始动态调整,开始在堆和堆栈中写入内容。 在这里的工作有以下几点:

针对per-main time优化的总结

t2

t2中所耗时间基本都是我们自己代码所造成的,所以优化的关键是找出每一个方法耗费的时间。
然后针对耗时的操作进行优化。
我们这里提供4种查看代码耗时的方法。

1. 使用Instruments

使用Instruments App Launch来抓取启动过程中各方法的耗时情况,具体可以参照下面的文章:
使用Instruments - App Launch查看启动问题
缺点:Instruments的功能很强大但是不够灵活。

2. 使用打点计时

此方法的原理为在你需要检测的方法前后做记录,计算前后两次打点间的时间间隔计算出方法耗时。
此方法可以参考这篇文章[贝聊科技]一次立竿见影的启动时间优化
缺点:此方法只能在方法调用的最外层获取到方法耗时,如果方法中调用了多个方法,要精确到具体哪个方法就需要多次设置打点的位置,使用也不是很方便。

3. 定时获取主线程中的调用栈

我们可以设置一个定时器,定时拉取主线程中调用栈信息,通过分析记录调用栈中各方法的入栈出栈时间,可以大致分析出各方法的耗时,这样就解决了方法嵌套时,无法定位耗时方法的问题。
调用栈分析器
调用栈分析器的使用方法可以看这篇文章iOS - 优化App冷启动速度
缺点:此方法存在两个问题,第一是拉去调用栈也会消耗时间,造成时间记录增加。第二就是定时器循环过程中会造成精度问题,例如调用时间小于定时器时间的方法不会被记录,调用时间恰好之比循环时间多了一点点就要增加整个循环时间间隔。

4. hook objc_msgSend 方法

使用fishhook钩住objc_msgSend,并在objc_msgSend前后插入我们的监听方法,为了不影响objc_msgSend的调用速度,插入的监听方法都需要使用汇编实现。
这里hook objc_msgSend的核心代码使用的是戴铭老师的。



使用时通过SMCallTrace类调用就可以了就可以了。
这里我们实现一个demo来验证一下
首先在didFinishLaunchingWithOptions方法中开始记录,并模拟一些耗时操作。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [SMCallTrace start];//开始记录
    [self test1];//耗时操作
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    self.window.backgroundColor = [UIColor whiteColor];
    ViewController *vc = [ViewController new];
    self.window.rootViewController = vc;
    [self.window makeKeyAndVisible];
    return YES;
}

在第一个显示出来的界面中模拟一个有层级的耗时方法,在viewDidAppear方法中结束记录,并打印个方法耗时。

- (void)viewDidLoad {
    [super viewDidLoad];
    [self test2];
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    [SMCallTrace stop];
    [SMCallTrace save];
}


- (void)test2 {
    sleep(0.79);
    [self test3];
}

- (void)test3 {
    sleep(1.77);
}

打印结果

 0| 2001.08|-[AppDelegate test1]
 path[AppDelegate test1]
 0|   1.67|-[UIWindow initWithFrame:]
 path[UIWindow initWithFrame:]
 0|   4.05|-[UIWindow setRootViewController:]
 path[UIWindow setRootViewController:]
 0| 1008.65|-[UIWindow makeKeyAndVisible]
 path[UIWindow makeKeyAndVisible]
 1| 1001.20|  -[ViewController test2]
 path[UIWindow makeKeyAndVisible] - [ViewController test2]
 2| 1001.15|    -[ViewController test3]
 path[UIWindow makeKeyAndVisible] - [ViewController test2] - [ViewController test3]

从打印结果可以看出耗时方法有test1, makeKeyAndVisible, test2, test3.
其中makeKeyAndVisible耗时是因为test2和test3在vc的viewDidLoad方法中调用的。
这样我们分析出具体的耗时方法就可以针对的调整调用时机,或者放到子线程调用了。

关注公众号回复476o7获取demo


参考文章:
https://www.jianshu.com/p/ce36d9efb6b0
https://www.jianshu.com/p/f26c4f16692a
https://www.jianshu.com/p/027a8abca64f
http://www.jianshu.com/p/d280feedbca0
http://www.cocoachina.com/ios/20161102/17931.html

上一篇下一篇

猜你喜欢

热点阅读