1.启动优化
我们的App如果启动时间过长,会出现白屏的问题。在我们App中,我们一般会集成很多的功能,在启动时,会加载很多的组件以及初始化,这样耗费的时间越多,白屏时间就会越长,用户体验相对来说就会很差,今天来学习一下启动优化!
2.启动方式
- 1.冷启动:我们在AppStore下载应用安装后,第一次启动;
- 2.热启动:我们运行App后,切换到后台或者杀死应用以后,再次启动,这里有一个前提,那就是在这个过程中,我们没有开启其他应用,那这就是热启动!如果在这个过程中我们频繁的开启其他多个应用,那么再次启动我们的App时,就是一种特别的热启动,这个耗时和第一次冷启动差不多啦!
- 3.热启动和类似于热启动的情况,这个过程是很快的,我们主要优化的是冷启动!
3.优化思路
我们把App的启动分为两个阶段:main函数之前和main函数之后!
在Edit Scheme的Arguments中添加环境变量DYLD_PRINT_STATISTICS,运行打印时间得到下图:
-
1.main函数之前:上图就是main函数之前的冷启动时间!
dylib loading time:lib库加载 rebase/binding time: 虚拟指针偏移修正(随机偏移值ALSR) /符号绑定 ObjC setup time:OC类注册耗时 initializer time:load加载耗时 下面的slowest initializers是列举initializer time中最慢的几个! dylib loading time的耗时,Apple建议,我们自己导入的动态库数量不大于6个!如果过多,考虑合并! 中间两个优化的可能性比较小,我们只能通过减少OC类来实现,有数据显示,减少2万个OC类可节约时间800毫秒! 但是,有一些情况,比如说我们开发过程中会有比较多的弃用类,这个时候我们就要使用工具来清理一下! 当然,也不排除老板要改回原来的需求!!!谨慎!!! 在这里也可看出,swift的好处啦! initializer time考虑优化的建议是,我们OC类加载中尽量不要在load方法中处理耗时操作!
-
2.main函数之后:这个是从main函数之后到第一个界面展示的时间!
main函数阶段考虑的优化 1.懒加载,尽可能不需要的不初始化 2.发挥CPU的作用,使用多线程进行初始化,当然这里要当心,不要出现bug! 3.很多人喜欢使用xib、storyboard,很方便,但是,在启动时的界面展示,不要使用xib!
-
3.上面优化阶段我们说过了,那么,这里要说的,是对pre-main的优化!!!
4.虚拟内存和物理内存
-
1.物理内存
在虚拟内存之前,进程加载到内存中,是全部加载进去,一个完整的进程内存空间,多个进程就是顺序排列。 但是这样做会有安全问题,直接访问的话,可以访问到其他进程!并且会很浪费资源,比如我们打开一个应用,并不会试用其全部的功能,但是整个进程都被加载到了内存中,造成资源的浪费!
-
2.虚拟内存
为了解决直接访问物理内存及资源的浪费等问题,出现了虚拟内存! 虚拟内存会让你的应用以为自己拥有一片大的连续的内存来加载进程,但是其实不是! 而是通过页表映射的方式来连接虚拟内存与物理内存, 虚拟内存地址通过页表的映射,找到真正的物理内存进行访问!!在物理内存中加载的都是当前使用过的进程功能模块,在内存紧张时,当前使用的功能模块需要加载进物理内存,也会覆盖掉当前进程不使用的功能模块的物理内存(或者覆盖其他进程不使用的模块占用的内存)!
-
3.虚拟内存访问物理内存
-
1.看上面的图,进程1启动后,会分配一片连续的虚拟内存,这个是整个进程1的连续的内存地址!
- CPU在访问进程里面的数据时,通过虚拟地址寻址,虚拟地址在被送到物理内存之前,会通过操作系统管理的页表,进行翻译,这个过程是地址翻译,并且需要CPU硬件上的内存管理单元mmu协助,然后会得到不同区域的物理内存地址!
- 根据这种原理,那么没一个进程有自己的虚拟内存和被CPU硬件管理的映射表,那么,进程地址访问就不可能跳出物理内存去访问其他进程内存,这样就解决了安全问题!
-
2.安全问题解决了,那么效率问题又是如何解决的呢?再看下面一张图:
-
3.虚拟内存效率问题 内存分页管理
- iOS操作系统的页表pageSize的大小为16K
- 在上面说的地址翻译的过程中,CPU拿到一个虚拟内存地址,在页表中查找时,发现该虚拟内存的标识为0,那么就说明,要访问的地址在物理内存还没有分配,此时要用到了,才会加载到物理内存,也就是懒加载模式的,出现这种情况,系统会立刻阻断堵塞进程,这个是page fault的缺页异常,操作系统会在物理内存的空白处开辟这一页的内存空间,然后把虚拟内存页表指向这一片内存空间,如果内存没有能放置这一页,操作系统也不会崩掉,他会覆盖掉不活跃的进程的一页内存!
- 所以,当你在使用手机时,随便的开应用,都不会报内存不够用的问题,就是虚拟内存实现的!并且在你开到一定多时,前面最先开启的进程再次启动时,就会出现重启的想象!
-
4.新的安全问题!
- 进程在安装编译好后虚拟内存地址是不变的,那么就会出现hook静态注入的问题,此时又引入了ASLR,给虚拟内存加一个随机的偏移,这样就解决了!
-
4.二进制重排
1.缺页中断检测
在上面提到了缺页中断page fault,某音发布了一篇文章,称一个page fault耗时0.6-0.8毫秒,在使用过程中,这个缺页中断很难感知到,但是在启动过程中,大量加载数据,如果存在的大量的page fault,那么这个就会感知的到!
- 如何检测缺页中断的次数呢?我们使用Xcode自带的检测工具,Xcode->Open Developer Tool->Instruments->System Trace;
- 检测:从启动到首次加载第一个界面这段时间!
- 上图中,检测到缺页中断的次数为3494次,耗时460.08ms!
- 杀死应用,不开启其他应用,再次直接启动,我们看一下结果:
- 这次的结果是page fault只有126次,耗时23.68ms!
- 我们再次杀死App,并且连续启动6个以上的其他应用,然后再次启动App,检测结果如下:
- 这次的结果,4547次,耗时584.04ms,甚至超过了第一次冷启动的次数!
- 这几次的检测结果过也证实了上面所说的虚拟内存及冷启动与热启动的几种情况!
2.二进制重排
- 二进制重排在Xcode是可以配置的,难点在于获取符号顺序,这个顺序是运行时的顺序!
- Xcode使用的连接器是ld,ld有一个order_file文件,我们只要重新把自己重排后的符号生成这样一个文件,放到Xcode的路径,那么Xcode在打包链接的时候就会按照这个符号顺序生成可执行文件!
- objc4-750的一份源码,文件夹里有一个.order的文件:
- 这就是符号顺序!
- 打开objc的工程,在Build Setting-> Linking -> Order File选项看到如图设置:
- 这说明我们的Xcode是支持二进制重排的,并且苹果的开发也在使用这个功能!
- 我们知道,Xcode在编译时,.m->.o文件,.o文件的链接顺序在Xcode中可以看到,在Build Phases->Compile Sources源文件顺序就是.o文件的链接顺序!
- 按上面的顺序,如果在AppDelegate.m中实现+load方法,在ViewController中同样实现+load方法,那么调用顺序就是先AppDelegate,再ViewController。如果将Compile Sources这里的文件顺序调换,AppDelegate放到ViewController后面,load方法的调用顺序就会跟着改变!
- 那么如何查看我们整个项目的链接顺序呢?在Xcode->Build Setting->Linking中将Write Link Map File项设置为Yes!编译项目,在工程中找到Products->XXX.app,Show In Finder,会到/Products/Debug-iphoneos/demo.app这个路径下,在/Products同级别文件路径下还有一个Intermediates文件夹,按照路径/Intermediates.noindex/demo.build/Debug-iphoneos/demo.build/demo-LinkMap-normal-arm64.txt找到demo-LinkMap-normal-arm64.txt文件:
- 那么我们要做的就是把这些内存重排,这个需要动的内存是很大的!
- 现在我们实际操作重排一下:我们把main、两个load和ViewController的viewDidLoad放在最前面,在排在一起,操作.order文件:
- command+shift+k clean一下,然后编译,完成后重新找到xxx-LinkMap-normal-arm64.txt文件查看:
- 我们很清晰的看到,这个符号顺序已经是按照我们order文件排列的顺序排列!并且,如果我们在order中添加了没有到符号,Xcode 链接的时候会忽略,所以不用担心!当然,我们不会用这种体力方式解决的!下一篇我们研究一下如何进行二进制的重排,难点与核心点就是找到启动时候的符号,并且获得调用的顺序!