App启动速度的优化与监控

App的启动分为冷启动和热启动两种方式

  • 冷启动: App 点击启动前,它的进程不在系统里,需要系统新创建一个进程分配给他启动的的情况, 这是一次完整的启动过程
  • 热启动: App冷启动后, 将App进入后台. 此时App的进程还在系统中, 用户重新进入App, 这个过程做的事情非常少, 称之为热启动

App启动速度的优化主要是针对冷启动时的优化. 用户感知到启动速度慢主要是因为主线程的卡顿, 在主线程中进行了大量文件读写操作, 或者屏幕渲染周期中进行了大量的计算等等. 然而, 即便处理了首屏之前所有的主线程操作, 有时候App的启动仍然慢于其他竞品App, 这个时候我们就需要对App首屏启动前的流程进行一个分析, 找出可以被优化的方向.

一般而言, App的启动时间是指, 从用户点击App开始, 到用户看到App首屏画面中间的这段时间, 这期间主要包括三个阶段:

  1. main() 函数执行前
  2. main() 函数执行后
  3. 首屏渲染完成后

在main() 函数执行前, 系统会做的几件事包括:

  1. 加载可执行文件(.o)文件(编译时, 先将文件预编译, 替换宏, 删除注释, 展开头文件等生成预编译.i文件, 然后编译生成.s文件, 后将.s文件编译成机器可以识别的.o文件)
  2. 加载动态链接库, rebase指针调整, bind符号绑定
  3. Objc运行时的初始处理, 包括相关类的注册, category的注册, selector的唯一性检查等等

在这个时期, 我们可以做的对启动速度的优化操作包括:

  • 减少不必要的动态库的加载, 尽量将多个动态库进行合并. 数量上, 苹果公司最多支持6个非系统动态库合并为一个
  • 减少在启动后不回去使用的类或者方法
  • +load()方法里的内容可以等到首屏渲染以后再去执行, 或者使用initialize() 方法去替换, 因为在每个+ load()方法中, 进行运行时方法替换都会带来4毫秒的消耗, 积少成多, 大量的+load方法对启动会起到很大的影响
  • 控制C++全局变量的数量

在main()函数执行后(即main()函数开始执行到appDelegate的didFinishlaunchingWithOpitons方法里首屏渲染相关方法执行完成)首页的业务代码都是在这个阶段, 也就是首屏渲染完成前执行的, 主要包括

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

在这个时期应该确定, 那些初始化方法需要在这个阶段执行, 那些方法可以再首屏渲染完成后执行, 梳理完后将初始化功能放到合适的阶段

首屏渲染完成后, 此时主要完成的就是:

  • 其他非首屏业务模块的初始化
  • 监听注册, 配置文件的读取

此时用户已经可以看到App首页的信息了, 从代码上看则是以首屏渲染完成后到didFinishLaunchingWithOptions方法作用域结束.

功能级别的启动优化就要从main()函数后的阶段入手.

  1. 通过监听runloop根据时间间隔判断卡顿:

ObserverMonitor.h

//
//  ObserverMonitor.h
//  StackCatch
//
//  Created by 阮皓 on 2019/3/14.
//  Copyright © 2019 阮皓. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface ObserverMonitor : NSObject

+ (instancetype)monitor;

+ (void)start;

@end

ObserverMonitor.m

//
//  ObserverMonitor.m
//  StackCatch
//
//  Created by 阮皓 on 2019/3/14.
//  Copyright © 2019 阮皓. All rights reserved.
//

#import "ObserverMonitor.h"
#include <libkern/OSAtomic.h>
#include <execinfo.h>

@interface ObserverMonitor () {
    
    CFRunLoopObserverRef _observer;
    dispatch_semaphore_t _semaphore;
    CFRunLoopActivity _activity;
}



@end

@implementation ObserverMonitor

+ (instancetype)monitor {
    
    static ObserverMonitor *monitor;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        monitor = [[ObserverMonitor alloc] init];
    });
    return monitor;
}

+ (void)start {
    
    [[ObserverMonitor monitor] startObserver];
}

- (void)startObserver {
    
    CFRunLoopObserverContext context = {
        
        0,
        (__bridge void *)(self),
        &CFRetain,
        &CFRelease,
        NULL
    };
    _observer = CFRunLoopObserverCreate(NULL, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallBack, &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
    
    _semaphore = dispatch_semaphore_create(0);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        while (YES) {
            long result = dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, 50 * NSEC_PER_MSEC));
            // Returns zero on success, or non-zero if the timeout occurred.
            if (result != 0) {
                // 说明一个runloop超时
                if (self->_activity == kCFRunLoopBeforeSources || self->_activity == kCFRunLoopAfterWaiting) {
                    // 说明这个超时的loop不是沉睡状态
                    [self logStack];
                }
            }
        }
    });
}

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    
    ObserverMonitor *monitor = (__bridge ObserverMonitor *)info;
    monitor->_activity = activity;
    dispatch_semaphore_signal(monitor->_semaphore);
}

- (void)logStack {
    
    void *callstack[128];
    int frames = backtrace(callstack, 128);
    char **strs = backtrace_symbols(callstack, frames);
    int i;
    NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];
    for (i = 0; i < 4; i++) {
        
        [backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
    }
    free(strs);
    NSLog(@"====================堆栈\n %@ \n",backtrace);
}

@end

然而, 这种方式可以检测出runloop的卡顿, 却不能精确地指出到底是哪一个方法导致了runloop的卡顿. 这时候可以使用对objc_msgSend方法进行hook来掌握所有方法的执行耗时.

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 爱情就是死循环,一旦执行就陷进去了;爱上一个人,就是内存泄漏–你永远释放不了;真正爱上一个人的时候,那就是常量限定...
    PetitBread阅读 4,836评论 0 2
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 13,917评论 1 32
  • 背景 一个项目做的时间长了,启动流程往往容易杂乱,库也用的越来越多,APP的启动时间也会慢慢变长。本次将针对iOS...
    酱油瓶2阅读 8,886评论 0 12
  • 武陵春 春晚 李清照 风住尘香花已尽,日晚倦梳头。物是人非事事休,欲语泪先流。 闻说双溪春尚好,也拟泛轻舟,只恐双...
    胡诹阅读 3,258评论 0 3
  • 一切都应该尽可能地简单,但不要太简单。——阿尔伯特·爱因斯坦 这句话体现出来的是一种化繁为简的能力,把复杂的系统通...
    刻意练习社区阅读 3,081评论 1 0

友情链接更多精彩内容