启动优化

作者: 雷霸龙 | 来源:发表于2021-03-15 10:29 被阅读0次

    启动阶段性能多维度分析

    要优化,首先要做到的是对启动阶段的各个性能纬度做分析,包括主线程耗时、CPU、内存、I/O、网络。这样才能更加全面的掌握启动阶段的开销,找出不合理的方法调用。

    启动越快,更多的方法调用就应该做成按需执行,将启动压力分摊,只留下那些启动后方法都会依赖的方法和库的初始化,比如网络库、Crash库等。而剩下那些需要预加载的功能可以放到启动阶段后再执行。

    简单来说 iOS 启动分为加载 Mach-O 和运行时初始化过程.

    Mach-O 主要分为:

    • 中间对象文件(MH_OBJECT)
    • 可执行二进制(MH_EXECUTE)
    • VM 共享库文件(MH_FVMLIB)
    • Crash 产生的 Core 文件(MH_CORE)
    • preload(MH_PRELOAD)
    • 动态共享库(MH_DYLIB)
    • 动态链接器(MH_DYLINKER)
    • 静态链接文件(MH_DYLIB_STUB)符号文件和调试信息(MH_DSYM)这几种。

    运行时初始化过程分为:

    • 加载类扩展。
    • 加载 C++静态对象。
    • 调用+load 函数。
    • 执行 main 函数。
    • Application 初始化,到 applicationDidFinishLaunchingWithOptions 执行完。
    • 初始化帧渲染,到 viewDidAppear 执行完,用户可见可操作。
    image.png

    也就是说对启动阶段的分析以 viewDidAppear 为截止。之前已经对 Application 初始化之前做过优化,效果并不明显,没有本质的提高,所以这次主要针对 Application 初始化到 viewDidAppear 这个阶段各个性能多纬度进行分析。

    image.png

    延后任务管理

    image.png

    经过前面所说的对主线程耗时方法和各个纬度性能分析后,对于那些分析出来没必要在启动阶段执行的方法,可以做成按需或延后执行。

    任务延后的处理不能粗犷的一口气在启动完成后在主线程一起执行,那样用户仅仅只是看到了页面,依然没法响应操作。那该怎么做呢?套路一般是这样,创建四个队列,分别是:

    • 异步串行队列
    • 异步并行队列
    • 闲时主线程串行队列
    • 闲时异步串行队列

    有依赖关系的任务可以放到异步串行队列中执行。异步并行队列可以分组执行,比如使用 dispatch_group,然后对每组任务数量进行限制,避免 CPU、线程和内存瞬时激增影响主线程用户操作,定义有限数量的串行队列,每个串行队列做特定的事情,这样也能够避免性能消耗短时间突然暴涨引起无法响应用户操作。

    使用 dispatch_semaphore_t 在信号量阻塞主队列时容易出现优先级反转,需要减少使用,确保 QoS 传播。可以用 dispatch group 替代,性能一样,功能不差。

    闲时队列实现方式是监听主线程 runloop 状态,在 kCFRunLoopBeforeWaiting 时开始执行闲时队列里的任务,在 kCFRunLoopAfterWaiting 时停止。

    对于main()调用之前的耗时我们可以优化的点有:

    1. 减少不必要的framework,因为动态链接比较耗时
    2. check framework应当设为optional和required,如果该framework在当前App支持的所有iOS系统版本都存在,那么就设为required,否则就设为optional,因为optional会有些额外的检查
    3. 合并或者删减一些OC类,关于清理项目中没用到的类,使用工具AppCode代码检查功能,查到当前项目中没有用到的类如下:
    image.png
    1. 删减一些无用的静态变量
    2. 删减没有被调用到或者已经废弃的方法
    方法见:
    http://stackoverflow.com/questions/35233564/how-to-find-unused-code-in-xcode-7
    https://developer.Apple.com/library/ios/documentation/ToolsLanguages/Conceptual/Xcode_Overview/CheckingCodeCoverage.html
    
    1. 将不必须在+load方法中做的事情延迟到+initialize中
    2. 尽量不要用C++虚函数(创建虚函数表有开销)

    对于main()函数调用之前我们可以优化的点有:

    1. 不使用xib,直接视用代码加载首页视图
    2. NSUserDefaults实际上是在Library文件夹下会生产一个plist文件,如果文件太大的话一次能读取到内存中可能很耗时,这个影响需要评估,如果耗时很大的话需要拆分(需考虑老版本覆盖安装兼容问题)
    3. 每次用NSLog方式打印会隐式的创建一个Calendar,因此需要删减启动时各业务方打的log,或者仅仅针对内测版输出log
    4. 梳理应用启动时发送的所有网络请求,是否可以统一在异步线程请求

    APP瘦身

    1. LSUnusedResources对无用的图片进行查找删除
    image.png

    注意:如果项目中有UIImage*image=[UIImage imageNamed:[NSString stringWithFormat:@”TabImage_index%d.png”,i]];这种使用方式的话,就不要勾选上图标记的Ignore similar name了。

    2. Link-Map分析并删除无用三方库
    1. 在XCode中开启编译选项Write Link Map File \n
      XCode -> Project -> Build Settings -> 把Write Link Map File选项设为yes,并指定好linkMap的存储位置

    2. 工程编译完成后,在编译目录里找到Link Map文件(txt类型) 默认的文件地址:~/Library/Developer/Xcode/DerivedData/XXX-xxxxxxxxxxxxx/Build/Intermediates/XXX.build/Debug-iphoneos/XXX.build/ \n\

    3. AppCode删除无用的类

    通过APPCode 打开对应的工程文件 选择 Code - > inspect Code 分析代码,去掉无用的引用及代码。包括无用的类、函数、宏定义、value、属性等,而safe delete功能使得删除一些由于runtime被调用到的代码时更加安全智能。当然还是要人工校准过再确认删除。

    image.png

    打印APP启动时间

    对于iOS应用查看启动时间,添加环境变量 DYLD_PRINT_STATISTICS值等于1:

    image.png

    System Trace

    1、System Trace一般可以用来分析哪些问题呢?
    • 锁的互斥,主要是主线程等子线程释放锁
    • 线程优先级,抢占和高优线程超过CPU核心数量
    • 虚拟内存,Page Fault的代价其实不小
    • 系统调用,了解性能瓶系统正在做什么
    2、Point of Interest

    有时候我们只关心某一段小段时间的性能,如何把时间段和System Trace对应起来呢?

    可以通过kdebug_signpost相关的接口相关的接口来打一些点,这些点会在Point of Interest区域中显示:

    kdebug_signpost_start(10, 0, 0, 0, 0);
    kdebug_signpost_end(10, 0, 0, 0, 0);
    
    3、Thread State Trace

    System Trace一个很重要的特性就是能看到线程不同的状态,以及状态之间切换的原因,通常我们会选择一个时间段,然后汇总观察结果

    几个线程状态说明:

    • Running,线程在CPU上运行
    • Blocked,线程被挂起,原因有很多,比如等待锁,sleep,File Backed Page In等等。
    • Runnable,线程处于可执行状态,等CPU空闲的时候,就可以运行
    • Interrupted,被打断,通常是因为一些系统事件,一般不需要关注
    • Preempted,被抢占,优先级更高的线程进入了Runnable状态

    Blocked和Preempted是优化的时候需要比较关注的两个状态,分析的时候通常需要知道切换到这两个状态的原因,这时候要切换到Events: Thread State模式,然后查看状态切换的前一个和后一个事件,往往能找到状态切换的原因。

    除了Thread State Event比较有用,另外一个比较有用的是Narrative,这里会把所有的事件,包括下文的虚拟内存等按照时间轴的方式汇总

    4、Virtual Memory Trace

    内存分为物理内存和虚拟内存,二者按照Page的方式进行映射。

    可执行文件,也就是Mach-O本质上是通过mmap相关API映射到虚拟内存中的,这时候只分配了虚拟内存,并没有分配物理内存。如果访问一个虚拟内存地址,而物理内存中不存在的时候,会怎么样呢?会触发一个File Backed Page In,分配物理内存,并把文件中的内容拷贝到物理内存里,如果在操作系统的物理内存里有缓存,则会触发一个Page Cache Hit,后者是比较快的,这也是热启动比冷启动快的原因之一。

    这种刚刚读入没有被修改的页都是Clean Page,是可以在多个进程之间共享的。所以像__TEXT段这种只读的段,映射的都是Clearn Page。

    _DATA段是可读写的,当_DATA段中的页没有被修改的时候,同样也可以在两个进程共享。但一个进程要写入,就会触发一次Copy On Write,把页复制一份,重新分配物理内存。这样被写入的页称为Dirty Page,无法在进程之间共享。像全局变量这种初始值都是零的,对应的页在读入后会触发一次内存写入零的操作,称作Zero Fill。

    iOS不支持内存Swapping out即把内存交换到磁盘,但却支持内存压缩(Compress memory),对应被压缩的内存访问的时候就需要解压缩(Decompress memory),所以在Virtial Memroy Trace里偶尔能看到内存解压缩的耗时。

    5、System Load

    以10ms为纬度,统计活跃的高优线程数量和CPU核心数对比,如果高于核心数量会显示成黄色,小于等于核心数量会是绿色。这个工具是用来帮助调试线程的优先级的

    线程的优先级可以通过QoS来指定,比如GCD在创建Queue的时候指定,NSOperationQueue通过属性指定:

    //GCD
    dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, -1);
    dispatch_queue_t queue = dispatch_queue_create("com.custom.utility.queue", attr);
    //NSOperationQueue
    operationQueue.qualityOfService = NSQualityOfServiceUtility
    

    选择合适的优先级,避免优先级反转,影响线程的执行效率,尤其是别让后台线程抢占主线程的时间。

    6、Thermal State

    一个大家不怎么关注,但其实挺重要的性能指标是发热状态,因为发热后系统会限制CPU/GPU/IO等使用。System Trace也提供了对应的分析工具

    iOS 11之后,可以通过NSProcesssInfo的相关API来获取当前发热状态:

    NSProcessInfo.processInfo.thermalState
    

    一共有四种状态,正常的状态是Nominal,后面逐级严重:

    • Nominal
    • Fair
    • Serious
    • Critical
    7、System Call & Context Switch

    操作系统为了安全考虑,把文件读写(open/close/write/read),锁(ulock_wait/ulock_wake)等核心操作封装到了内核里,用户态必须调用内核提供的接口才能完成对应的操作,这样的调用称作系统调用System Call。System Trace里提供了系统调用相关的Event

    线程/进程需要轮流到CPU上执行,在切换的时候,必须把线程/进程状态保存下来,之后才能恢复,这种保存/恢复的过程称作上下文切换Context Switch,在System Trace里通常会关注下主线程是否在频繁的上下文切换

    相关文章

      网友评论

        本文标题:启动优化

        本文链接:https://www.haomeiwen.com/subject/zkghcltx.html