iOS APP启动监控和优化思路

前言:本文简单描述APP启动过程和监控,一些深入原理性的东西可能需要绕路了,站在大神的肩膀上,简单总结跟APP启动性能有关,如果差错请不吝赐教

冷启动

相对而言冷启动就是App被kill掉以后一切从头开始启动的过程。
App 点击启动前,它的进程不在系统里,需要系统新创建一个进程分配给它启动的情况。这是一次完整的启动过程。
用户感知到的启动慢,其实都发生在主线程上。而主线程慢的原因有很多,比如在主线程上执行了大文件读写操作、在渲染周期中执行了大量计算等。

热启动

当用户按下home键的时候,iOS的App并不会马上被kill掉,还会继续存活若干时间。用户点击App的图标再次回来的时候,App几乎不需要做什么,就可以还原到退出前的状态,继续为用户服务。App 在冷启动后用户将 App 退后台,在 App 的进程还在系统里的情况下,用户重新启动进入 App 的过程。这种持续存活的情况下启动App,称为热启动。

查看APP启动耗时

根据APP启动时间,继续了解APP启动时候都做了哪些
Xcode:(快捷键:command + shift + <)
ProjectSchemeEdit SchemeRunEnvironment Variables
添加 DYLD_PRINT_STATISTICS 环境变量,value为1

环境变量

APP 启动时间:

Total pre-main time: 802.16 milliseconds (100.0%)
         dylib loading time: 294.37 milliseconds (36.6%)
        rebase/binding time: 377.42 milliseconds (47.0%)
            ObjC setup time:  86.68 milliseconds (10.8%)
           initializer time:  43.51 milliseconds (5.4%)
           slowest intializers :
             libSystem.B.dylib :   4.20 milliseconds (0.5%)
    libMainThreadChecker.dylib :  21.22 milliseconds (2.6%)

时间消耗解读

  • main()函数之前总共使用了802.16ms
  • 加载动态库占用36.6%
  • 指针重定位占用47.6%
  • ObjC类初始化占用10.8%
  • 各种初始化使用了5.4%。
  • initializer time中最耗时的是libSystem.B.dylib、libBacktraceRecording.dylib。

换言之:
App开始启动后,系统首先加载可执行文件(自身App的所有.o文件的集合),然后加载动态链接器dyld,dyld是一个专门用来加载动态链接库的库。 执行从dyld开始,dyld从可执行文件的依赖开始, 递归加载所有的依赖动态链接库。
动态链接库包括:iOS 中用到的所有系统 framework,加载OC runtime方法的libobjc,系统级别的libSystem,例如libdispatch(GCD)和libsystem_blocks (Block)。

APP启动阶段

启动时间:用户点击APP → APP首页面加载完成

  • 阶段1:main() 函数执行前
  • 阶段2:main() 函数执行后
  • 阶段3:首屏渲染完成后

main()函数执行前

在 main() 函数执行前,系统主要会做下面几件事情:

  • 【解析Info.plist】:加载信息,例如闪屏;沙盒建立、权限检查
  • 【Mach-O加载】:加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法);加载可执行文件(App 的.o 文件的集合)
  • 【加载动态链接库】:进行 rebase 指针调整和 bind 符号绑定;定位内部、外部指针引用,例如字符串、函数等
  • 【Objc 运行时的初始处理】:包括 Objc 相关类的注册、category 注册、selector 唯一性检查等
  • 【初始化】:执行 +load() 方法,执行声明为attribute((constructor))的C函数,C++静态对象加载

程序执行

  • 调用 main()
  • 调用UIApplicationMain()
  • 调用applicationWillFinishLaunching

可优化的功能点

  • 【减少动态库加载】:每个库本身都有依赖关系,使用更少的动态库,并且建议在使用动态库的数量较多时,尽量将多个动态库进行合并。最多可以支持 6 个非系统动态库合并为一个。
  • 【减少+load方法】:方法里的内容可以放到首屏渲染完成后再执行,或使用 +initialize() 方法替换掉。在一个 +load() 方法里,进行运行时方法替换操作会带来 4 毫秒的消耗。
  • 【减少使用】:减少写attribute((constructor))的C函数,控制 C++ 全局变量的数量;

main()函数之后

main()函数开始至 appDelegate
didFinishLaunchingWithOptions结束,称为main()函数之后的部分。

主要执行内容

  • 首屏初始化所需配置文件的读写操作;
  • 首屏列表大数据的读取;
  • 首屏渲染的大量计算等。

main()函数之后耗时的影响因素

  • 执行 main()函数的耗时
  • 执行applicationWillFinishLaunching的耗时
  • rootViewController及其childViewController的加载、view及其subviews的加载

首屏渲染完成之后

[首屏渲染完成之后]指的是非首屏其他业务服务模块的初始化、监听的注册、配置文件的读取等。
该阶段指的就是截止到 didFinishLaunchingWithOptions 方法作用域内执行首屏渲染之后的所有方法执行完成。从渲染完成时开始,到 didFinishLaunchingWithOptions方法作用域结束时结束。

优化思路一:功能启动优化

main() 函数开始执行后到首屏渲染完成前只处理首屏相关的业务,其他非首屏业务的初始化、监听注册、配置文件读取等都放到首屏渲染完成后去做。

根据刚需分置阶段进行
根据启动流程把刚需功能放置在启动阶段,其他业务功能放在合适的阶段

  • 首屏渲染必要的初始化功能
  • App 启动必要的初始化功能
  • 只需要在对应功能开始使用时才需要初始化的功能
  • 例如:主视图第一时间加载,里面的数据和界面延后加载

优化思路二:方法启动优化

检查首屏渲染完成前主线程上的耗时方法,将非刚需的耗时方法滞后或异步执行。耗时较长的方法主要发生在计算大量数据的情况下,例如加载、编辑、存储图片和文件等资源。
+load() 方法,一个耗时 4 毫秒,100 个就是 400 毫秒,不可小视

优化三:移除不必要的动态库

移除项目中非必要的动态库

优化四:移除不必要用到的类

代码工程的维护非常重要

优化五:合并功能相似的类和扩展(Category)

由于Category的实现原理,和ObjC的动态绑定有很强的关系,实际上类的扩展是比较占用启动时间的。尽量可能合并一些扩展,并不是让你不使用扩展

优化六:压缩资源图片

图片小了,IO操作量小了,启动就快了。推荐 TinyPNG

优化七:优化applicationWillFinishLaunching

需要在applicationWillFinishLaunching里处理的业务较多时,可以管理起这些任务
将不需要马上在applicationWillFinishLaunching执行的代码延后执行

优化八:优化rootViewController

rootViewController的加载,适当将某一级的childViewController或subviews延后加载
如果你的App可能会被后台拉起并冷启动,可考虑不加载rootViewController

优化九:小优化

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

Debug 打印代码块

//MARK: - DEBUG print
func printLog<T>(msg: T,
                 file: String = #file,
                 method: String = #function,
                 line: Int = #line){
    if !DEBUG_ALPHA{//线上环境不print
        return
    }
    print("\((file as NSString).lastPathComponent) \(method),[\(line)]: \(msg)")
}

APP监控方法一:计算主线程方法耗时

定时抓取主线程上的方法调用对战,计算在一段时间内各个方法的耗时。Xcode 工具套件里自带的 Time Profiler,开发类似工具成本不高,能够快速开发后集成到你的 App 中,以便在真实环境中进行检查。

对于定时时间间隔的控制

  • 间隔长:会漏掉某些方法,从而导致检查出来的耗时不精确
  • 间隔短:抓取堆栈这个方法本身调用过多会影响整体耗时,导致结果不准确
  • 定时抓取主线程调用栈的方式精准度不够高,做参考足以
  • 大神得出最合适时间为 0.01 秒,虽然导致许多运行速度快的方法监控误差,对整体耗时影响小

APP监控方法二:objc_msgSend 方法 hook 所有方法耗时

Hook:在原方法开始执行时换成执行其他指定的方法,或者在原有方法执行前后执行你指定的方法,来达到掌握和改变指定方法的目的。

  • 优点:非常精确
  • 缺点:只能对Objective-C方法,对c方法和block需要借助第三方框架处理,编写维护成本高

Objective-C 里每个对象都会指向一个类,每个类都会有一个方法列表,方法列表里的每个方法都是由 selector、函数指针和 metadata 组成的。
objc_msgSend就是在运行时根据对象和方法的 selector 去找到对应的函数指针,然后执行。objc_msgSendObjective-C 里方法执行的必经之路,能够控制所有的 Objective-C 的方法,可自行阅读objc_msgSend 源码获得更深层次的理解。

objc_msgSend 用汇编语言写的:

  • objc_msgSend 的调用频次最高,在它上面进行的性能优化能够提升整个 App 生命周期的性能。汇编语言在性能优化上属于原子级优化,能够把优化做到极致。
  • 其他语言难以实现未知参数跳转到任意函数指针的功能。

objc_msgSend 方法执行逻辑
先获取对象对应类的信息,再获取方法的缓存,根据方法的 selector 查找函数指针,经过异常错误处理后,最后跳到对应函数的实现

Tips

Swift AppDelegate 没有main函数入口了

错,swift有main函数,被精简成了一个@NSApplicationMain

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
检查方法耗时的工具

推荐大神神作 SMCallTrace
友情提示:需要在SMCallTrace.m中打开第54行的注释。

+load为什么会增加4毫秒,Swift呢

aop 的耗时,swift没有

iOS中用llvm的IR中插桩来统计函数耗时这个方法也可行

有待学习,嘤嘤嘤

objc的hook是用method swizzle来实现,对于swift

使用Time Profiler 或者 使用Clang打桩统计耗时

oc的代码在编译时会转成c++,再转成c,那swift如何转换?

swift 和 c 编译方式类似。Swift 会先编成 SIL( Swift Intermediate Language)然后再编成机器码。

未完待续…

【干货推荐】
iOS启动时间优化
iOS App 启动性能优化
今日头条iOS客户端启动速度优化
优化 App 的启动时间
《How we cut our iOS app’s launch time in half (with this one cool trick)》
汇编相关
https://blog.nelhage.com/2010/10/amd64-and-va_arg/
http://infocenter.arm.com/help/topic/com.arm.doc.ihi0055b/IHI0055B_aapcs64.pdf
https://developer.apple.com/library/ios/documentation/Xcode/Conceptual/iPhoneOSABIReference/Articles/ARM64FunctionCallingConventions.html
博客、课程、开源 推荐 戴神

小结:一枚突然顿悟的小白兔,学无止境

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