前言
APP 的启动耗时直接关系到用户对 APP 的印象,如果启动耗时过长会直接导致用户切换到竞品 APP,所以,对 APP 启动耗时进行监控和优化非常重要。
抖音的技术团队曾经分享过一个通过 applicationDidBecomeActive 监控启动耗时的监控方案,但是通过构造测试场景,我们会发现该方案的结果与真实的用户体验存在一些差距。
测试视频(通过特殊方案将首屏视图渲染阻塞 3000
毫秒):
通过观察视频的内容,我们可以发现 DidBecomeActive 方案检测到的耗时是 1716
毫秒,而 IPC 方案(与红色视图显示时间接近) 的展示耗时是 4764,双方的差异是 3048
毫秒
所以,IPC 方案更加适合对 APP 启动耗时进行监控。
术语对齐
-
启动耗时
启动耗时是指 启动图完全消失的第一帧 减去 启动的时间戳
不同的 APP 对 启动终点 的定义存在轻微的差异,本文会采用 抖音品质建设 - iOS 启动优化《原理篇》 提供的定义:启动终点为启动图完全消失后的第一帧
-
IPC
进程间通信(IPC,Inter-Process Communication),指至少两个进程或线程间传送数据或信号的一些技术或方法
-
XPC
XPC 是 iOS/OS X 下的一种 IPC 技术, 它实现了权限隔离等各种底层能力
iOS 的渲染机制
iOS 的渲染机制依赖 Render Loop
进行。
Render Loop
主要分为以下几步进行:
-
Event
:主要发生在 APP 内部,由 APP 处理各类事件并修改视图状态 -
Commit
:发生在 APP 内部,由进行 APP 布局、绘制等任务(下一段会重点讲解) -
Render prepare
:发生在渲染服务,为 GPU 的渲染做准备,比如处理图层、动画等内容 -
Render execute
:发生在渲染服务,由 GPU 执行绘制内容 -
Display
:屏幕展示内容
Commit
因为 Commmit
阶段 发生在 APP 内部,并且距离 渲染服务 最近,所以,我们需要先重点了解 Commmit
阶段的具体流程。
Commit Phase
流程分析
当 APP 第一次启动时,系统库会先调用 UIWindow
的 makeKeyAndVisible
方法,并通过 rootViewController
触发第一个视图树的创建和构造过程。
本文以 14.3 为例进行讲解
构造过程结束后,会开始调用 Transition
(过场动画)相关的方法:
并通过 UIScene
或者 Appdelegate
的回调方法,通知 APP 已经切换到 active
状态
抖音目前采用的监控入口
随后开始注册 CATransactionPhase
变化回调
敲重点:这里是为了后面的 xpc 通信做准备
当我们对视图的各种属性修改后结束后,会在主线程休眠前会触发 UIKitCore
或者 QuartzCore
的回调方法,并最终调用到 CA::Transaction::commit()
CA::Transaction::commit()
会依次执行以下步骤:
-
Layout(布局) :调用
layout
等与布局相关的方法 -
Display(绘制):调用
drawRect:
等与绘制相关的方法 -
Prepare(准备):这个过程中会完成图片的解码
-
Commit(提交):打包 Render Tree,并通过 xpc 框架发给 Render Server
xpc 通信发生在前面注册的
CATransactionPhase
变化回调
ipc 监控方案原理
通过前面的分析,我们可以发现最合适的时机是在 mach_msg_trap
函数的 ret
处进行 hook 操作。
但是mach_msg_trap
被调用的地方比较多,不适合增加打点逻辑,所以,我们只能寻找其它位置增加 hook。
通过分析上面的堆栈特征,我们发现比较合适的位置是 -[BSXPCServiceConnectionMessage _sendSynchronously:]
方法
经过笔者的实际测试,因为编译优化的原因,即使通过 objc 运行时 对 -[BSXPCServiceConnectionMessage _sendSynchronously:]
方法进行替换,新的方法也无法被调用
hook 方案失效原因分析
通过 lldb
,我们可以发现 __79+[BSXPCServiceConnectionProxy invokeMethod:onTarget:withMessage:forConnection:]_block_invoke
并没有调用 -[BSXPCServiceConnectionMessage _sendSynchronously:]
方法,而是调用了 send
方法
-[BSXPCServiceConnectionMessage send]
方法会通过 指令b
直接调用 -[BSXPCServiceConnectionMessage _sendSynchronously:]
方法
因为通过 objc 运行时 进行方法替换只能影响通过 objc_msgSend
函数进行的方法调用,所以,上面的 hook 方案会失效
hook 升级方案
通过上一段的分析,我们可以发现 __79+[BSXPCServiceConnectionProxy invokeMethod:onTarget:withMessage:forConnection:]_block_invoke
是通过 objc_msgSend
函数调用 send
方法,所以,我们可以 hook send
方法。
实现代码如下:
|
|
完整的测试代码,可以关注公众号并回复“启动监控”获取 测试代码包含两种方案:hook 系统库方法和调用 半公开 API 的方案
总结
本文通过介绍 Render Loop
和 commit phase
的流程,分享了通过监控 ipc 通信机制监控启动耗时的解决方案。