iOS启动时间的一些分析

前言

最近在作一些App品质提高,启动时间优化是其中很重要的一项,本文围绕启动时间作一个深刻了解。xcode

正文

什么是启动时间?

启动时间能够理解为从用户点击App的Icon到用户看到App真正画面而且能够进行交互的时间。这段时间还能够为两部分:iOS系统启动App的时间 和 App初始化应用内部逻辑和界面的时间。缓存

1、App产生

在探究iOS系统如何处理App启动以前,咱们须要先了解下一个App是如何产生的:
一、编译:咱们打开一个xcode工程,会看到若干个.h/.m组成;当咱们进行编译时,编译器会分别对每一个.m文件进行编译,获得对应的.o文件;
sass

二、连接:将编译产生的多个.o文件结合静态库、动态库进行连接,获得一个可执行文件,也叫Mach-O文件;​ Mach-O里的部分信息会被行裁剪(strip),好比说调试符号、行号等信息;为了方便调试,会把这些信息放到一个dsym文件; ​markdown

三、签名&打包&上传:将裁剪后的Mach-O与资源文件(storyboard、asset)等一块儿打包成.app文件,再进行签名,最后上传到AppStore后台;app

2、iOS如何启动App

WWDC视频中对启动过程作了一些介绍,先看iOS 13之前用dyld2是如何启动App:ide

一、解析Mach-O文件的头部,找到​LC_LOAD_DYLINKER,定位到dyld的路径,将dyld加载到内存中;函数

二、解析动态库的依赖,好比说咱们工程中这部分依赖;工具

三、分别将动态库mmap到内存中,一个App运行过程当中会依赖不少动态库;​oop

四、符号查找定位,下图是咱们工程依赖的GLKit.framework,可是点开framework的所在文件夹,会发现只有头文件和一个tbd文件;tbd是text-based stub library的简称,为xcode连接过程提供符号;App真正运行的时候,还须要加载动态库,进行真正的连接;(动态连接的了解能够看前文)布局

五、符号绑定和重定向,动态连接与静态连接同样,符号最终都须要转换为运行时的内存地址;动态库的符号须要运行时,才能肯定全部符号的具体位置;还有另一个影响的因素是iOS的ASLR(进程地址空间布局随机化)也须要在运行时加上偏移;

六、静态初始化,包括咱们经常使用​+load方法,以及其余静态初始化的方法;

dyld3如何进行优化? iOS 13以后,系统提供的dyld3将启动过程的解析Mach-O文件的头部解析动态库的依赖符号查找定位的结果作了一个缓存,写到是disk中。在启动时候,就直接读取缓存并校验是否有效,再进行后续的动态库加载符号绑定和重定向以及静态初始化

这个缓存存储在沙盒的tmp/com.apple.dyld目录(tmp目录不能再整个清除),缓存会在手机系统升级或者更新App时从新建立。

3、开发时如何对这些时间进行分析

开发阶段,能够在环境变量中设置DYLD_PRINT_STATISTICS值为1;

启动的时候,就能够看到控制台打出了具体的时间。

Total pre-main time: 622.64 milliseconds (100.0%)
         dylib loading time:  33.89 milliseconds (5.4%)
        rebase/binding time: 279.52 milliseconds (44.8%)
            ObjC setup time: 270.59 milliseconds (43.4%)
           initializer time:  38.63 milliseconds (6.2%)
           slowest intializers :
             libSystem.B.dylib :   7.08 milliseconds (1.1%)
    libMainThreadChecker.dylib :  19.92 milliseconds (3.2%)
复制代码

一样,还能够设置DYLD_PRINT_LIBRARIES值为1,就会打印出来装载了哪些动态库。

dyld: loaded: /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 12.2.simruntime/Contents/Resources/RuntimeRoot/usr/lib/libMobileGestaltExtensions.dylib
dyld: loaded: /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 12.2.simruntime/Contents/Resources/RuntimeRoot/usr/lib/libobjc-trampolines.dylib
dyld: loaded: /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 12.2.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/FontServices.framework/libTrueTypeScaler.dylib
dyld: loaded: /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 12.2.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/Accelerate.framework/Frameworks/vImage.framework/Libraries/libCGInterfaces.dylib
...
复制代码

Instrucment也有工具进行这些时间的分析,好比说你们最经常使用的Time Profiler,以及更复杂的System Trace。
Time Profiler​基于采样的方式进行运行时间统计,大概每毫秒会采样一次,能够经过​勾选Recording Options的​High Frequency来提升采样频率;Time Profiler​的使用比较简单,能直接反馈出来瓶颈的问题。
System Trace能够更细致的分析锁、线程状态、内存变化、系统调用等,好比说下图的​Zero Fill、File Backed Page In、Page Cache Hit、Copy On Write的分布。
​ File Backed Page In 就是PageFault,内存缺页中断,访问一个虚拟内存地址而内存中还不存在时触发,操做系统会分配物理内存并拷贝内容到对应物理内存;
Page Cache Hit 若是操做系统的PageCache里有对应缓存,则会触发一个Page Cache Hit;(参考资料
Copy On Write 操做系统中的内存页存在共享的状况,若是某些页是只读,则一直是能够共享的;可是若是对一个可写的共享内存页进行写操做时,须要先复制一份再尝试写入,这个过程就是Copy On Write;
Zero Fill 部份内存页的值都是0,在读入后须要出发一次填充0的操做,这个过程就是Zero Fill;

4、如何对线上用户进行启动时间统计

最实用的方式就是打点统计:
+load方法开始打点:+load方法的调用顺序是按照连接顺序执行,若是使用CocoaPod来管理集成库,能够新建一个A开头的Pod库(CocoaPod是按照字母升序),让该Pod库的+load方法第一个被执行;
main函数开始打点:__attribute__能够设置函数、变量和类型属性,能够设置一个constructor属性,让函数在main()函数执行以前被自动的执行。

static void __attribute__((constructor)) _mainConstructor() {
    NSLog(@"main constructor");
}
复制代码

didFinishLaunchingWithOptions开始打点:直接在APPDelegate的didFinishLaunchingWithOptions方法开始时打点;
didFinishLaunchingWithOptions结束打点:直接在APPDelegate的didFinishLaunchingWithOptions方法结束时打点;
RootViewControllerDidAppear打点:在viewDidAppear:方法开始时打点;

总结

了解更多关于启动相关的知识,才能更好去分析问题,设计良好的解决方案。
最后介绍了两个工具:MachOView 和 Hopper Disassembler。 前者开源免费(直接搜索下载),后者收费软件(也能够30分钟试用)。

附录

dyld开源代码
iOS的文件内存映射——mmap
WWDC2017-App Startup Time: Past, Present, and Future

相关文章
相关标签/搜索