iOS开发启动优化

优化(首页加载时间)

  1. 整理启动项, 总结出耗时长的模块
  2. 分为2部分 main函数之前耗时和mian之后耗时
  3. Main前面主要做一些动态库dyld的加载和load方法的调用, 对于pre-main阶段,Apple提供了一种测量方法来计算耗时时长
  4. 减少依赖不必要的库,无论是动态还是静态库,如果可以的话把动态库改为静态库
  5. 用linkmap检测出所有的方法和类,在用AppCode找出无用代码和类,删除掉,减少load方法调用
  6. 少在类的+load方法里做事情,尽量把这些事情推迟到+initiailize (可以减少main之前的运行时间)
  7. 对于main()阶段,主要是测量main()函数开始执行到didFinishLaunchingWithOptions执行结束的耗时,就需要自己插入代码到工程中了。先在main()函数里用变量StartTime记录当前时间, 最后在didFinishLaunchingWithOptions里,再获取一下当前时间,与StartTime的差值即是main()阶段运行耗时。
  8. 懒加载(避免在首页控制器的viewDidLoad和viewWillAppear做太多事情,这2个方法执行完,首页控制器才能显示)
  9. 图片压缩,图片小了,io操作量也会变小,启动就快了
  10. 非必要加载业务延迟去做(比如放到首页控制器的viewDidAppear方法)
  11. 主页不用storyboard或者xib加载, 全部改为纯代码
  12. 采用性能更好的API

应用启动原理流程

  1. 点击图标,创建进程
  2. mmap 主二进制,找到 dyld 的路径 (mmap 的全称是 memory map,是一种内存映射技术,可以把文件映射到虚拟内存的地址空间里,这样就可以像直接操作内存那样来读写文件。)
  3. mmap dyld,把入口地址设为_dyld_start (dyld 是启动的辅助程序,是 in-process 的,即启动的时候会把 dyld 加载到进程的地址空间里,然后把后续的启动过程交给 dyld;iOS 13 开始 Apple 对三方 App 启用了 dyld3,dyld3 的最重要的特性就是启动闭包,闭包里包含了启动所需要的缓存信息,从而提高启动速度。)
  4. 重启手机/更新/下载 App 的第一次启动,会创建启动闭包
  5. 把没有加载的动态库 mmap 进来,动态库的数量会影响这个阶段
  6. 对每个二进制做 bind 和 rebase(Rebase:修复内部指针。这是因为 Mach-O 在 mmap 到虚拟内存的时候,起始地址会有一个随机的偏移量 slide,需要把内部的指针指向加上这个 slide。Bind:修复外部指针。这个比较好理解,因为像 printf 等外部函数,只有运行时才知道它的地址是什么,bind 就是把指针指向这个地址),主要耗时在 Page In,影响 Page In 数量的是 objc 的元数据有定义,过程:MMU 找到空闲的物理内存页面 ;触发磁盘 IO,把数据读入物理内存;如果是 TEXT 段的页,要进行解密;对解密后的页,进行签名验证)
  7. 初始化 objc 的 runtime,由于闭包已经初始化了大部分,这里只会注册 sel 和装载 category
  8. +load 和静态初始化被调用,除了方法本身耗时,这里还会引起大量 Page In
  9. 初始化 UIApplication,启动 Main Runloop
  10. 执行 will/didFinishLaunch,这里主要是业务代码耗时
  11. Layout,viewDidLoad 和Layoutsubviews 会在这里调用,Autolayout 太多会影响这部分时间
  12. Display,drawRect 会调用
  13. Prepare,图片解码发生在这一步
  14. Commit,首帧渲染数据打包发给 RenderServer,启动结束


基于启动原理优化

t(App 总启动时间) = t1(main 调用之前的加载时间) + t2(main 调用之后的加载时间),分main函数之前和main函数之后的相关方法优化


查看耗时情况

获得 main() 方法执行前的耗时比较简单,通过 Xcode 自带的测量方法既可以。将 Xcode 中 Product -> Scheme -> Edit scheme -> Run -> Environment Variables 将环境变量 DYLD_PRINT_STATISTICS 或 DYLD_PRINT_STATISTICS_DETAILS 设为 1 即可获得执行每项耗时:

Total pre-main time: 1.2 seconds (100.0%)

dylib loading time: 147.51 milliseconds (12.0%)

rebase/binding time: 112.82 milliseconds (9.2%)

ObjC setup time:  45.94 milliseconds (3.7%)

initializer time: 919.07 milliseconds (75.0%)

       slowest intializers :

libSystem.B.dylib :   6.79 milliseconds (0.5%)

libMainThreadChecker.dylib :  34.62 milliseconds (2.8%)

libglInterpose.dylib : 353.67 milliseconds (28.8%)

TCLTV : 944.10 milliseconds (77.0%)

优化实践

启动任务拆分优化
(1) 确定在展示 UI 前必须执行的任务。
如果应用是第一次启动,那么没有必要加载任何用户偏好,如主题、刷新间隔、缓存大小等。此时是没有任何自定义值的。初始缓存肆意增长也是没问题的,因为它的增长不会超过最终的限制值。崩溃报告系统应第一个被初始化。
(2) 按顺序执行任务。
排序是非常重要的,因为任务之间可能具有相互依赖性,同时,排序还可以节省用户的宝贵时间。例如,如果先触发了访问令牌的验证操作,那么其他任务可能会并行执行,因为验证过程需要进行网络连接。但是这样就会导致一种情况:如果其他任务先完成,而验证还未完成,应用就必须等待验证完成才能继续执行。
(3) 将任务拆分为两类:一类是必须在主线程中执行的任务,另一类是可以在其他线程中执行的任务 ,然后分别执行。还可以进一步将在非主线程中执行的任务分为可以并发执行的和不能并发执行的。
(4) 其他任务可以在加载 UI 后执行或异步执行。
延迟其他子系统(如记录仪和分析方法)的初始化。在应用的后续阶段将一些操作(例如,写日志消息或跟踪事件)放入队列中,直到子系统完全完成初始化。

优化方法

(1)在t1阶段加快App启动:

  • 尽量使用静态库,减少动态库的使用,动态链接比较耗时,如果要用动态库,尽量将多个dylib动态库合并成一个
  • 尽量避免对系统库使用optional linking,如果App用到的系统库在你所有支持的系统版本上都有,就设置为required,因为optional会有些额外的检查
  • 减少Objective-C Class、Selector、Category的数量,可以合并或者删减一些OC类(怎样删减无用代码:ViewConteroller 渗透率,hook 对应的声明周期方法即可统计;Class 渗透率,遍历运行时的所有类,通过 Objective C Runtime 的标志位判断类是否被访问;行级渗透率,需要用编译期插桩,对包大小和执行速度均有损。)
  • 删减一些无用的静态变量,删减没有被调用到或者已经废弃的方法
  • 将不必须在+load中做的事情尽量挪到+initialize中,+initialize是在第一次初始化这个类之前被调用,+load在加载类的时候就被调用;或者做load方法迁移
  • 尽量不要用C++虚函数,创建虚函数表有开销
  • 不要使用attribute((constructor))将方法显式标记为初始化器,而是让初始化方法调用时才执行。比如使用dispatch_once(),pthread_once()或 std::once()
  • 在初始化方法中不调用dlopen(),dlopen()有性能和死锁的可能性
  • 在初始化方法中不创建线程

(2)在t2阶段加快App启动:

  • 尽量不要使用xib/storyboard,而是用纯代码作为首页UI,如果要用xib/storyboard,不要在xib/storyboard中存放太多的视图
  • 使用简单的广告页作为过渡,将首页的计算操作及网络请求放在广告页展示时异步进行。
  • 对application:didFinishLaunchingWithOptions:里的任务尽量延迟加载或懒加载
  • 不要在NSUserDefaults中存放太多的数据,NSUserDefaults是一个plist文件,plist文件会被反序列化一次
  • 避免在启动时打印过多的log,少用NSLog,因为每一次NSLog的调用都会创建一个新的NSCalendar实例
  • 为了防止使用GCD创建过多的线程,解决方法是创建串行队列,或者使用带有最大并发数限制的NSOperationQueue
  • 不要在主线程执行磁盘、网络、Lock或者dispatch_sync、发送消息给其他线程等操作

总结

总结起来,启动速度优化就一句话:让系统在启动期间少做一些事。当然我们得先清楚工程里做的哪些事是在启动期间做的、对启动速度的影响有多大,然后case by case地分析工程代码,通过放到子线程、延迟加载、懒加载等方式让系统在启动期间更轻松些。

iOS 启动优化总结

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容