启动阶段性能多维度分析
要优化,首先要做到的是对启动阶段的各个性能纬度做分析,包括主线程耗时、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 执行完,用户可见可操作。
也就是说对启动阶段的分析以 viewDidAppear 为截止。之前已经对 Application 初始化之前做过优化,效果并不明显,没有本质的提高,所以这次主要针对 Application 初始化到 viewDidAppear 这个阶段各个性能多纬度进行分析。
image.png延后任务管理
image.png经过前面所说的对主线程耗时方法和各个纬度性能分析后,对于那些分析出来没必要在启动阶段执行的方法,可以做成按需或延后执行。
任务延后的处理不能粗犷的一口气在启动完成后在主线程一起执行,那样用户仅仅只是看到了页面,依然没法响应操作。那该怎么做呢?套路一般是这样,创建四个队列,分别是:
- 异步串行队列
- 异步并行队列
- 闲时主线程串行队列
- 闲时异步串行队列
有依赖关系的任务可以放到异步串行队列中执行。异步并行队列可以分组执行,比如使用 dispatch_group,然后对每组任务数量进行限制,避免 CPU、线程和内存瞬时激增影响主线程用户操作,定义有限数量的串行队列,每个串行队列做特定的事情,这样也能够避免性能消耗短时间突然暴涨引起无法响应用户操作。
使用 dispatch_semaphore_t 在信号量阻塞主队列时容易出现优先级反转,需要减少使用,确保 QoS 传播。可以用 dispatch group 替代,性能一样,功能不差。
闲时队列实现方式是监听主线程 runloop 状态,在 kCFRunLoopBeforeWaiting 时开始执行闲时队列里的任务,在 kCFRunLoopAfterWaiting 时停止。
对于main()调用之前的耗时我们可以优化的点有:
- 减少不必要的framework,因为动态链接比较耗时
- check framework应当设为optional和required,如果该framework在当前App支持的所有iOS系统版本都存在,那么就设为required,否则就设为optional,因为optional会有些额外的检查
- 合并或者删减一些OC类,关于清理项目中没用到的类,使用工具AppCode代码检查功能,查到当前项目中没有用到的类如下:
- 删减一些无用的静态变量
- 删减没有被调用到或者已经废弃的方法
方法见:
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
- 将不必须在+load方法中做的事情延迟到+initialize中
- 尽量不要用C++虚函数(创建虚函数表有开销)
对于main()函数调用之前我们可以优化的点有:
- 不使用xib,直接视用代码加载首页视图
- NSUserDefaults实际上是在Library文件夹下会生产一个plist文件,如果文件太大的话一次能读取到内存中可能很耗时,这个影响需要评估,如果耗时很大的话需要拆分(需考虑老版本覆盖安装兼容问题)
- 每次用NSLog方式打印会隐式的创建一个Calendar,因此需要删减启动时各业务方打的log,或者仅仅针对内测版输出log
- 梳理应用启动时发送的所有网络请求,是否可以统一在异步线程请求
APP瘦身
1. LSUnusedResources对无用的图片进行查找删除
image.png注意:如果项目中有UIImage*image=[UIImage imageNamed:[NSString stringWithFormat:@”TabImage_index%d.png”,i]];
这种使用方式的话,就不要勾选上图标记的Ignore similar name了。
2. Link-Map分析并删除无用三方库
-
在XCode中开启编译选项Write Link Map File \n
XCode -> Project -> Build Settings -> 把Write Link Map File选项设为yes,并指定好linkMap的存储位置 -
工程编译完成后,在编译目录里找到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:
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里通常会关注下主线程是否在频繁的上下文切换
网友评论