本文论述了个人对移动端页面加载耗时监控的一些了解,首要从:节点区分及对应的完结计划,线上监控注意点,后续还能做的事 三个方面来和咱们共享。
前语
移动端的页面加载速度,作为最为影响用户体会的要素之一,是咱们做移动端功能优化的重点方向之一。
而优化的作用体现,需求相信的目标进行衡量(常见办法论:寻觅方向->确认目标->实践->量化收益),而本文想要共享的便是:怎么实在、完好、便利的取得页面加载时刻,并会向线上监控环节,有必定延伸。
本文的示例代码都是OC(由于Java和kotlin我也不会),但相关思路和计划也适用于Android(Android端已完结并上线)。
页面加载耗时
常见计划
页面加载时长是一直以来咱们都在攻坚的方向,所以市面上也有十分十分多的度量计划,从节点区分视点看:
较为根底的:ViewController 的 init -> viewDidLoad -> viewDidAppear
更进一步的:ViewController 的 init -> viewDidLoad -> viewDidAppear -> user Interactable
主流计划:ViewController 的 init -> viewDidLoad -> viewDidAppear -> view render completed -> user Interactable
还有什么地方可以改进的吗?
关于这些成熟计划,我还有什么可以更进一步的吗?首要总结为以下几个方面吧:
- 完好反映用户体感
咱们做功能优化,归根到底,更是用户体会优化,在满意功能需求的一起,不影响用户的运用体会。 所以,我个人以为,大多数的功能目标,都要考虑到用户体会这个方向;页面启动速度这一块,更是如此;而传统的计划,可以完好的反响用户体感吗? 我觉得仍是有一部分的缺失的:用户自动主张交互到ViewController这个阶段。这一部分有什么呢,不便是直接tap触发的action里vc就初始化了吗? 实践在一些较为杂乱、大型的项目中,并否则,中心或许会有许多其他处理,例如:办法hook、路由调度、参数解析、containerVC的初始化、动态库加载等等。这一部分的耗时,实践上也是用户体感的一部分,而这一部分的耗时,假如不加监控的话,也会对全体耗时发生劣化。(这儿或许会有小伙伴问了,这些东西,不该该由各自担任的同学,例如担任路由的同学,自行监控吗?这儿我想论述的一个观点时,时长类的监控,假如由几个时刻段拼接,比较于endTime – startTime,难免会发生gap,即,参加endTime = 10,startTime = 0,那么中心分成两段,很有或许endTime2 = 10,startTime2 = 6;endTime1 = 4,startTime1 = 0,形成总时长不准。总而言之,仍是期望得到一个可以完好反映用户体感的时长。)
- 数据搜集与事务解耦
这一点其实市面上的许多计划现已做得很好了。解耦,一方面是为了,提效:防止后续有新的页面需求监控时,需求进行新的开发;另一方面,也是防止事务迭代关于监控数据的影响:假如是手动侵入性埋点,很难确保后续新增的耗时任务对监控数据不发生影响。 而本文计划,不需求在事务代码中刺进任何代码,大都是经过办法hook来完结数据搜集的;而对范围、以及匹配关系等的操控,也都是经过装备来完结的。
具体完结
节点确认&数据搜集办法
依据一个页面(ViewController)的加载过程中,开发首要进行的处理,以及或许对用户体感发生影响的要素,将页面加载过程区分为如上图所示的11个节点,具体解说及完结计划如下:
1. 用户行为触发页面跳转
由于页面的跳转一般是经过用户点击、滑动等行为触发的,因而这儿监听用户接触屏幕的时刻点;但有用节点仅为VC在初始化前的最终一次点击/交互。
具体完结:
hook UIWidow 的 sendEvent:
办法,在swizzle办法内记录信息;为了功能考虑,现在仅记录一个uint64_t的时刻戳,且仅内存写;
注意这儿需求记录手指抬起的时刻,即 touch.phase == UITouchPhaseEnded
,由于一般action被调用的机遇便是此刻;
一起,为了适配各种行为触发的新页面呈现,还增加了一个手动增加该节点的办法,使一些较杂乱且不通用,事务特性较强的初始化场景,也可以有该节点数据,且不依靠hook;但注意该手动办法为侵入式数据搜集办法。
2. ViewController的初始化
具体完结:hook UIViewController
或你的VC基类 的 - (instancetype)init
的办法;
3. 本地UI初始化
不依靠于网络数据的UI开端初始化。
这个节点,我实践上并没有在本次完结,这儿的一个抱负态是:将这部分行为(即UI初始化的代码),经过协议的办法,束缚到指定办法中;例如,架构层面束缚一个setupSubviews的接口,回调给各事务VC,供其进行根底UI制作(现在这种办法再一些更杂乱的事务场景下完结并运转较好);有这个根底束缚的前提下,才干精确的搜集我抱负中该节点的耗时。而我现在所担任的模块,并没有这种强束缚,而又不能简单的去以为全部根底UI都是在viewDidLoad
中去完结的。因而需求 对原有架构的必定修正 或 可以确保全部根底UI行为都在viewDidLoad
中完结,才可以完结该节点数据的精确搜集。
因而2 ~ 3和3 ~ 4间的耗时,被交融为了一段2 ~ 4的耗时。
4. 本地UI初始化完结
不依靠于网络数据的UI初始化完结。
具体完结:监听主线程的闲时状况,VC初始化 节点后的首个闲时状况表示 本地UI初始化完结;(闲时状况即runloop进入kCFRunLoopBeforeWaiting
)
5. 主张网络恳求
调用网络SDK的时刻点。
这儿描绘的便是上面的节点区分图的第二条线,由于两条线的节点间没有强制的线性关系,尽管图中当前节点是放在了VC初始化平行的方位,但实践上,有些完结会在VC初始化之前就主张网络恳求,进行预加载,这种状况在完结的时候也是需求兼容的。
具体完结:hook 事务调用网络SDK主张恳求办法的api;这儿的网络库各家完结计划就或许有较大差异了,依据本身状况完结即可。
6. 网络SDK回调
网络SDK的回调触发的时刻点。
具体完结:hook 网络SDK向事务层回调的api;差异性同5。
7. send request
8. receive response
实在 宣布网络恳求 和 收到response 的时刻点,用于核算实在的网络层耗时。 这俩和5、6是不是重复了啊?并否则,由于,网络库在接收到主张网络恳求的恳求后,实践上在端阶段,还会进行许多处理,例如公参的处理、签名、验签、json2Model等,都会发生耗时;而实在离开了端,在网上逛荡那一段,更是几乎“彻底不可控”的状况。所以,分开来计算:端部分 和 网络阶段,才可以为后续的优化提供数据根底,这也是数据监控的含义地点。
具体完结: 实践上系统网络api中就有对网络层具体功能数据的搜集
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics;
依据官方文档中的描绘
可以发现,咱们实践上需求的时长便是从 fetchStartDate
到 responseEndDate
间的时刻。
因而可以该delegate,获取这两个时刻点。
9. 具体UI初始化
具体UI指,依靠于网络接口数据的UI,这部分UI烘托完结才是页面到达对用户可见的状况。
具体完结:这儿咱们以为从网络SDK触发回调时,即开端进行具体UI的烘托,因而该节点和节点6是同一个节点。
10. 具体UI烘托完结
页面临用户来说,实在到达可见状况的节点。
具体完结: 关于一个惯例的App页面来说,怎么界说一个页面是否实在烘托完结了呢?
被有用的视图铺满。
什么是有用视图呢?视频,图片,文字,按钮,cell,能向用户传递信息,或许发生交互的view; 铺满,并不是指彻底铺满,而是这些有用视图填充到必定份额即可,由于依照正常的视觉设计和交互体会,都不会让整个屏幕的每一个像素点都充满信息或具备交互才干;而这个份额,则是依据事务的不同而不同的。 下面则是上述逻辑的完结思路:
确认有用视图的具体类
UITextView
UITextField
UIButton
UILabel
UIImageView
UITableViewCell
UICollectionViewCell
主流计划中比较常见的,是前几品种,并不包括最终的两个cell;而这儿为什么将cell也作为有用视图类呢? 首要,出于事务特征考虑,现在应用该套监控计划的页面,首要是以卡片列表款式呈现的;并且个人以为,市面上许多App的页面也都是列表形式来呈现内容的;当然,假如事务特征并不相符,例如全屏的视频播映页,就可以不这样处理。 其次,将cell作为有用视图,确实可以极大的下降每次核算掩盖率的耗时的。功能监控本身发生的功能耗费,是功能方向一直以来需求侧重重视的点,究竟你一个为了功能优化服务的东西,反而带来了不小的劣化,怎样也说不太过去啊~ 我也测试了是否包括cell对核算耗时的影响: 下表中为,在一个层级较为杂乱的事务页面,页面彻底烘托完结之后,完结一次掩盖率到达阈值的扫描所需的时长。
有用视图 | 包括 cell | 不包括 cell |
---|---|---|
检测一次掩盖率耗时(ms) | 1~5 | 15~18 |
耗时减少 | 15ms/次(83%) |
并且,有用视图的类,主张支撑在线装备,也可所以一些自界说类。
将cell作为有用视图,咱们或许会发生一个新的顾虑:占位cell的状况,再具体点,便是常见的骨架图怎么办?骨架图是什么,便是在网络恳求未回来的时候,用缓存的data或许模仿款式,烘托出一个包括大致结构,但不包括具体内容的页面状况,例如这种:
掩盖率的核算办法
- (void)checkPageRenderStatus:(UIView *)rootView {
if (kPhoneDeviceScreenSize.width <= 0 || kPhoneDeviceScreenSize.height <= 0) {
return;
}
memset(_screenWidthBitMap, 0, kPhoneDeviceScreenSize.width);
memset(_screenHeightBitMap, 0, kPhoneDeviceScreenSize.height);
[self recursiveCheckUIView:rootView];
}
- (void)recursiveCheckUIView:(UIView *)view {
if (_isCurrentPageLoaded) {
return;
}
if (view.hidden) {
return;
}
// 查看view是否是白名单中的实例,直接用于填充bitmap
for (Class viewClass in _whiteListViewClass) {
if ([view isKindOfClass:viewClass]) {
[self fillAndCheckScreenBitMap:view isValidView:YES];
return;
}
}
// 最终递归查看subviews
if ([[view subviews] count] > 0) {
for (UIView *subview in [view subviews]) {
[self recursiveCheckUIView:subview];
}
}
}
- (BOOL)fillAndCheckScreenBitMap:(UIView *)view isValidView:(BOOL)isValidView {
CGRect rectInWindow = [view convertRect:view.bounds toView:nil];
NSInteger widthOffsetStart = rectInWindow.origin.x;
NSInteger widthOffsetEnd = rectInWindow.origin.x + rectInWindow.size.width;
if (widthOffsetEnd <= 0 || widthOffsetStart >= _screenWidth) {
return NO;
}
if (widthOffsetStart < 0) {
widthOffsetStart = 0;
}
if (widthOffsetEnd > _screenWidth) {
widthOffsetEnd = _screenWidth;
}
if (widthOffsetEnd > widthOffsetStart) {
memset(_screenWidthBitMap + widthOffsetStart, isValidView ? 1 : 0, widthOffsetEnd - widthOffsetStart);
}
NSInteger heightOffsetStart = rectInWindow.origin.y;
NSInteger heightOffsetEnd = rectInWindow.origin.y + rectInWindow.size.height;
if (heightOffsetEnd <= 0 || heightOffsetStart >= _screenHeight) {
return NO;
}
if (heightOffsetStart < 0) {
heightOffsetStart = 0;
}
if (heightOffsetEnd > _screenHeight) {
heightOffsetEnd = _screenHeight;
}
if (heightOffsetEnd > heightOffsetStart) {
memset(_screenHeightBitMap + heightOffsetStart, isValidView ? 1 : 0, heightOffsetEnd - heightOffsetStart);
}
NSUInteger widthP = 0;
NSUInteger heightP = 0;
for (int i=0; i< _screenWidth; i++) {
widthP += _screenWidthBitMap[i];
}
for (int i=0; i< _screenHeight; i++) {
heightP += _screenHeightBitMap[i];
}
if (widthP > _screenWidth * kPageLoadWidthRatio && heightP > _screenHeight * kPageLoadHeightRatio) {
_isCurrentPageLoaded = YES;
return YES;
}
return NO;
}
可是也会有极点状况(相似下图)
无法正确反响有用视图的掩盖状况。可是出于功能考虑,并不会选用二维数组,由于w*h的量太大,遍历和核算的耗时,会有指数级的激增;并且,正常事务形状,应该不太会有相似的极点形状。
即便真的会较高频的呈现相似状况,也有一套备选计划:核算有用视图的面积 占 总面积 的份额;该种办法会涉及到UI坐标系的频繁转换,耗时也会略差于当前的办法。
在某些事务场景下,例如 无/少成果状况,关于页面等,彻底烘托后,也无法到达铺满阈值。 这种状况,会以用户发生交互(同 1、用户行为触发页面跳转 的获取办法)和 主线程闲时状况超越5s (可配)来做兜底,看是否属于这种状况,假如是,则相关功能数据不上报,由于此种页面临功能的耗费较正常铺满的状况要低,并不能实在的反响功能耗费、瓶颈,因而,仅正常铺满的事务场景进行监控并优化,即可。
扫描的触发机遇
以帧改写为准,由于只要每次帧改写后,UI才会实在发生改变;出于功能考虑,不会每帧都进行扫描,每距离x帧(x可配,默以为1),扫描一次;一起,考虑高刷屏 和 大量UI制作时会丢帧 的状况,设置 扫描时刻距离 的上下限,即:满意 隔x帧 的前提下,假如和上次扫描的时刻差小于 下限,仍不扫描;假如 某次扫描时,和上次扫描的时刻距离 大于 上限,则不管中心隔几帧,都敞开一次扫描。
11. 用户可交互
用户可见之后的下一个对用户来说至关重要的节点。假如仅仅可见,然后就疯狂占用主线程或其他资源,形成用户的点击等交互行为,仍是会被卡主,用户只能看,不能动,这个体感也是很差的;
具体完结:具体UI烘托完结 后的 首次主线程闲时状况。
监控计划
这儿由于各家的基建并不相同,因而仅仅总结一些小的主张,或许会比较零星,咱们见谅。
- 主张采样搜集
首要,数据的搜集或许其他的新增行为/办法,必定是会发生耗时的,尽管或许不多,但仍是秉着尽善尽美的原则,仍是能少点就少点的,所以数据的搜集,包括前面的hook等等全部行为,都仅仅随机的面向一部分用户开放,下降影响范围; 并且,假如数据量极大,全量的数据上报,其实对数据链路本身也会发生压力、增加本钱。 当前,采样的前提是根本数据量足够,否则的话,采样样本量过小,简单对计算成果发生较大波动,形成不相信的成果。
- 可装备
除了根本的是否敞开的开关之外,还有其他的许多的点 需求/可以/主张 运用线上装备操控。个人以为,线上装备,除了完结对逻辑的操控,更重要的一个作用,便是呈现问题时及时止损。 举一些我现在运用的装备中的比如: – 有用视图类 – 烘托完结状况,横纵坐标的填充百分比阈值 – 终态的兜底阈值 – VC的类名、对应的网络恳求 等等。
- 本地反常数据过滤
由于咱们的样本数据量会十分大,所以关于反常数据咱们不需求“手软”,咱们需求有一套本地反常数据过滤的机制,来确保上报的数据都是符合要求的;否则咱们后续计算处理的时候,也会因而呈现新的问题需求处理。
后续还能做的事
这一部分,是对后续可完结计划的一个美好想象~
1)页面可见态的结尾,不仅仅掩盖率
其实,实践事务场景中,许多cell,即便制作完,并烘托到屏幕上,此刻,用户可见的也没有到达咱们实在期望用户可见的状况,许多内容,都仍是一个placeholder的状况。例如,经过url加载的image,咱们一般都是先把他的size算好,把他的方位留好,cell烘托完就直接展示了;再进一步,假如是一个视频的播映卡片,即便网络图片加载好了,还要等待视频帧的回来,才干实在到达这张卡片的事务终态\color{red}{事务终态}(请教这儿标红后怎么可以让字体大小一致)。
这个十分后置,并且咱们端上或许也影响不了什么的节点,搜集起来有含义吗?
我觉得这是一个十分有价值的节点。一直都在说“技能反哺事务”,那么事务想要用户实在看到的那个终态,便是很重要的一环;因而,用户能在什么时刻点看到,从事务视点说,可以影响其后续的计划设计(体现形式),完善用户体感对事务目标的影响;从技能视点说,可以感知实在的全链路的体现(不仅仅端),然后有针对性的进行优化。
怎么获取到全部的事务终态呢?
这儿必定是和事务有所耦合的,由于每个事务的终态,只要事务本身才知道;可是咱们仍是要尽量下降耦合度。 这儿可以用协议的办法,为各个事务增加一个到达终态的标识,那么在某个事务到达终态之后,设置该标识即可,这儿便是唯一对事务的侵入了;然后和核算掩盖率相似,这儿的遍历,是事务维度(这儿想象为卡片更好了解一点),只要全部事务的标识都ready之后,才是实在到达事务上的终态。
2)功能目标 关联 事务行为
其实,现在功能监控,各类平台,各个团队,或多或少的都在做,我相信,功能数据搜集的代码,在工程中,也不仅仅只要一份;这个现状,在许多成必定规模的互联网公司中都或许存在。
而假如您和我一样,作为一个事务团队,怎么在不重复造轮子的状况下,夹缝中求生存呢?
我个人现在的了解:将 功能体现 与 事务场景 相关联。
帧率、启动耗时、CPU、内存等等,这些功能目标数据的获取,在业界都有十分成熟的计划,并且咱们的工程里,必定也有相关的代码;而咱们能做的,仅仅是,调一下人家的api,然后把数据再自己上传一份(乃至有的连上传都包括了),就完事了吗?
这样我觉得并不能体现出咱们自建监控的价值。个人了解,监控的含义在于:露出问题 + 辅助定位问题 + 验证问题的处理作用。
所以咱们作为事务团队,将 功能数据 和 咱们的事务做了什么 bind 到一起了,是不是就能必定程度上完结了上面的意图呢?
咱们可以清晰,咱们什么样的事务行为,会影响咱们的功能数据,也便是影响咱们的用户根底体会。这样,不仅会协助咱们定位问题的原因,乃至会影响产品侧的一些产品才干设计计划。
完结这些建造之后,或许咱们的监控就可以变成这样,乃至更好的状况:
3)完善全链路对功能体现的重视
功能数据的重视、监控,不该该仅仅在线上阶段,开发期 → 测试期 → 线上,全链路各个环节都应该具有。
-
现在各家都比较重视线上监控,相信都现已较为完善;
-
测试期的事务流程功能脚本;关于测试的功能测试计划,开放应该参加共建或许有必定程度的参加,这样才干从必定程度上确保数据的精确性,以及双方功能数据的相互认可;
-
开发期,现在可以提供展示实时CPU、FPS、内存数据的根底才干的东西很常见,也比较简单完结;但实践上,在日常开发的过程中,很难让RD一起重视需求状况与功能数据体现。因而,仍是需求一些东西来辅助:例如,咱们可以对某些功能目标,设置一些阈值,当日常开发中,超越阈值时,则弹窗提示RD确认是否原因、是否需求优化,例如,具体UI制作阶段的耗时阈值是800ms,假如某位同学在进行变更后,实践制作耗时屡次超越该值,则弹窗提示。