前言
卡顿优化一直是客户端功能办理的重要方向之一,在这之前,咱们先来解说下什么是卡顿。
卡顿,直白来说便是用户在运用APP的进程中能感受到界面一卡一卡的不流通。从原理来说,便是在用户能够感知的视觉场景中,当事情处理和UI展现的综合耗费时刻超越用户视觉体系的最大期待时刻时,就会呈现卡顿现象。卡顿会影响用户的操作,危害用户体验,进一步影响用户对APP的点评和留存。因而,操作流通度是决议APP体验好坏的关键因素之一。优化卡顿,将APP的用户体验做到极致,在必定程度上能够提升用户的忠诚度和APP的市场占有率。
行业规范
那么,APP的卡顿率在多少区间算是正常或许优异呢? 咱们能够参阅 《2020移动使用功能办理白皮书 | 基调听云》推荐的行业规范:
功能指标 | 优异值 | 及格值 | 极差值 | 行业参阅值 |
---|---|---|---|---|
卡顿率(%) | <=2 | 5 | >=8 | 4 |
全体状况
APP卡顿优化是一个长时刻进程,货拉拉用户端APP卡顿办理分多期进行,在前期的办理中,咱们的卡顿率数据采用的是bugly的卡顿监控。办理前,APP的卡顿率是6.13%,通过2个月的办理实践,卡顿率降到了2.1%,已接近行业优异规范。因而,咱们总结了这段时刻的一些探究和实践,期望能给大家在App卡顿优化方面提供一些借鉴和思路。
卡顿原理和检测
为什么呈现卡顿?
屏幕显示图像是需求CPU和GPU结合工作。CPU 负责核算显示内容,包括视图创立、布局核算、图片解码、文本制作等,CPU 完结核算后,会将核算内容提交给 GPU;GPU 进行改换、组成、烘托,将烘托成果提交到帧缓冲区,当下一次垂直同步信号(简称 V-Sync)到来时,将烘托成果显示到屏幕上。
UI视图显示到屏幕中的进程:
在屏幕显示图像前,CPU 和 GPU 需求完结自身的使命,体系会每(1000/60=16.67ms)将UI的改变重新制作,烘托到屏幕上。如果在16ms内,主线程进行了耗时操作,CPU和GPU没有来得及生产出一帧缓冲,那么这一帧会被丢弃,显示器就会坚持不变,继续显示上一帧内容,用户的视觉上就呈现了卡顿;因而卡顿发生的原因便是,CPU和GPU没有及时处理好数据。所以,针对卡顿优化的思路是,尽或许削减 CPU 和 GPU 资源耗费。
UIEvent的事情是在Runloop循环机制驱动下完结的,主线程恣意一个环节进行了耗时操作,主线程都无法履行Core Animation回调,从而形成界面无法改写。用户交互是需求UIEvent的传递和呼应,也必须在主线程中完结。所以说主线程的堵塞会导致UI和交互的双双堵塞,这也是导致卡顿的根本原因。
卡顿检测
知道了卡顿呈现的根本原因,咱们就很好理解怎么进行卡顿检测了。业界常见的卡顿检测是对主线程的Runloop进行监控,由于卡顿直接导致操作无呼应,界面动画迟缓,所以通过检测主线程能否呼应使命,来判别是否卡顿。在讲怎么用Runloop来检测卡顿之前,咱们先来回忆下Runloop的运行机制。
-
Runloop的运行机制
RunLoop 会接纳两种类型的输入源:
- 来自另一个线程或许来自不同使用的异步音讯;
- 来自预订时刻或许重复距离的同步事情;
RunLoop首要的工作是,当有事情要去处理时坚持线程忙,当没有事情要处理时让线程进入休眠 。
整个 RunLoop 进程 :
-
RunLoop监控卡顿的原理
如果 RunLoop 的线程,进入睡觉前,办法履行时刻过长而导致无法进入睡觉;或许线程唤醒后,接纳音讯时刻过长而无法进入下一步,就能够认为是线程受阻。如果这个线程是主线程,表现出来的便是呈现卡顿。 所以,使用 RunLoop 来监控卡顿,就需求重视这两个阶段。进入睡觉之前和唤醒后的两个loop状况值,也便是 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting(触发 Source0 回调和接纳 match_port 音讯两个状况)。线程的音讯事情是依赖于 RunLoop ,通过拓荒一个子线程来监控主线程的 RunLoop 的状况,就能够发现调用办法是否履行过长,从而判别出是否呈现卡顿。
RunLoop
的六个状况 :
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //进入loop
kCFRunLoopBeforeTimers = (1UL << 1), //触发 Timer 回调
kCFRunLoopBeforeSources = (1UL << 2),//触发 Source0 回调
kCFRunLoopBeforeWaiting = (1UL << 5),//等待 mach_port 音讯
kCFRunLoopAfterWaiting = (1UL << 6),//承受 mach_port 音讯
kCFRunLoopExit = (1UL << 7), //退出 loop
kCFRunLoopAllActivities = 0x0FFFFFFFU //loop 一切状况改动
};
-
监控的完成
- 创立一个
RunLoop
的调查者:
- 创立一个
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
_runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
//将调查者添加到主线程runloop的common形式下
CFRunLoopAddObserver(CFRunLoopGetMain(), _runLoopObserver, kCFRunLoopCommonModes);
- 再将调查者 runLoopObserver 添加到主线程 RunLoop 的 common 形式下,然后再创立一个继续的子线程专门用来监控主线程的RunLoop状况。实时核算 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting 两个状况区域之间的耗时是否超越某个阈值,超越即可判别为卡顿,然后把对应的仓库信息进行上报。
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//子线程敞开一个继续的loop用来进行监控
while (YES) {
long semaphoreWait = dispatch_semaphore_wait(self.dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC));
if (semaphoreWait != 0) {
if (!self.runLoopObserver) {
self.timeoutCount = 0;
self.dispatchSemaphore = 0;
self.runLoopActivity = 0;
return;
}
//BeforeSources和AfterWaiting这两个状况区间时刻能够检测到是否卡顿
if (self.runLoopActivity == kCFRunLoopBeforeSources || self.runLoopActivity == kCFRunLoopAfterWaiting) {
//上报对应的卡顿仓库信息
}
}
}
});
除了上面介绍的自研卡顿监控计划,也能够运用第三方SDK,不过检测原理都是大同小异的。货拉拉用户端因工程中自身就集成了Bugly SDK,因而在办理前期,咱们仅仅把Bugly的卡顿检测翻开,以最小的投入本钱,达到线上赶快有卡顿指标能够参阅的意图。
卡顿办理实践
咱们在敞开Bugly的卡顿监控时,将卡顿阈值blockMonitorTimeout设置为3秒,这也是SDK默认阈值。即,监控主线程 Runloop 的履行,调查履行耗时是否超越3s。在监控到卡顿时会当即记录线程仓库到本地,在App从后台切换到前台时,履行上报。办理前期有许多的卡顿反常上报,咱们对上报的卡顿进行分期办理,依据反常发生次数划分为Top 20、Top50等。上报量Top 20的卡顿为高频卡顿,上报次数频频、影响用户多,需优先办理。
用户端Top4的卡顿如下:
常见卡顿
在办理进程中,咱们将常见的卡顿原因做了聚合分类,而且针对不同的卡顿原因,总结了对应不同的处理计划,以下为针对性的办理计划:
-
IO读写
- 在主线程做许多的数据读写操作
优化计划:敞开子线程,异步去读取和保存本地的数据,逻辑处理完了再回到主线程改写UI
例如:+[CityManager saveLocalCityList:] (CityManager.m:)
全国的城市列表数据量大,在获取到最新的数据后,会将数据存放在本地。在数据写入和读取的时分,敞开子线程,异步读取和存入本地。优化后的代码:
// 子线程异步存储
- (void)getCityList {
......
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[CityManager saveLocalCityList:list];
......
});
}
-
UI制作相关
- 杂乱的UI、图文混排的制作量过大
优化计划:无事情处理的当地尽或许的运用CALayer,坚持视图的轻量,避免重写drawRect办法;
- 频频运用setNeedsLayout,layoutIfNeeded来改写UI
有时分为了达到当即改写UI的作用,会调用如下的代码:
- (void)updateConfig {
......
[self setNeedsLayout];
[self layoutIfNeeded];
}
这样调用后,会触发调用layoutSubViews,强制视图当即更新其布局,运用自动布局时,布局引擎会依据需求来更新视图的方位,以满意约束的更改。这样会增加耗费,简单形成卡顿。
优化计划:有时分想要当即改写UI,或许仅仅为了获取最新的frame数据。可进行代码逻辑的调整,换一种办法完成,就能削减layoutIfNeeded的调用,削减卡顿的呈现。
-
主线程相关
- 在主线程上做网络同步恳求,或许在主线程中做数据解析和模型转化
优化计划:在子线程中发起网络恳求,而且在子线程中进行数据的解析和模型的转化;处理完逻辑后,再回到主线程中改写UI。
- 主线程做许多的逻辑处理,运算量大,CPU 继续高占用
优化计划:有杂乱逻辑的当地,主张整理逻辑,优化算法,而且把逻辑的处理放在子线程中进行处
理;处理完后,再回到主线程中改写UI。
首页是用户运用率最高的页面,版别不停的迭代,货拉拉首页的需求也是频频的改变;在车型挑选模块,随着需求的改变,有许多的AB试验叠加在一起,导致车型挑选模块有许多的逻辑判别,视图十分多且杂乱;随着需求的迭代,越来越难以保护,属于卡顿反常高发区。通过综合分析,决议对这个模块进行逻辑整理,重构车型模块的UI。上线后该模块的卡顿上报量显着下降,收益显着。
- boundingRectWithSize在主线程中履行
优化计划:文本高度的核算会占用很大一部分CPU资源,因而在涉及核算的当地,最好不要在主线程中进行;在子线程中核算好再回到主线程中改写对应的UI。例如:
优化后的代码:
NSString *text = @"货拉拉...";
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
CGFloat width = [text boundingRectWithSize:CGSizeMake(375, 20)
options:NSStringDrawingUsesLineFragmentOrigin
attributes:@{NSFontAttributeName :[UIFont systemFontOfSize:15]}
context:nil].size.width;
dispatch_async(dispatch_get_main_queue(), ^{
self.logoLabel.width = width;
});
});
- 发动时使命太多,没有做线程的优先级办理,影响首页的UI创立,导致卡顿
优化计划:针对发动的一切使命进行整理,依据使命的重要性进行优先级的划分;非发动必须的使命,可推迟履行,等待首页UI初始化完结后再进行履行;优先级较低的使命,将其放入子线程中履行,避免形成主线程堵塞,引起卡顿。
-
其他
- 本地读取icon
优化计划:本地的小图片尽量运用Images.xcassets来办理,不主张运用bundle。Images.xcassets中的图片,运用imageName
读取,加载到内存中,会有缓存,占据内存空间。关于需求重复加载的icon,由于有缓存,加载速度会提升许多。
可是关于内存大的图片资源,最好放在bundle中,而且运用imageWithContentsOfFile
读取,这个办法不会有缓存,这样能够更好的控制内存。
- 第三方SDK相关的问题
优化计划:需求结合具体的SDK进行优化;若是SDK内部引起的,可联络第三方进行对应的问题反应,促进问题的优化;
疑难卡顿
上报的卡顿线程仓库信息中,或许存在信息不准确的状况,也或许存在许多信息缺乏的状况,依据上报的内容,无法准确定位卡顿的具体方位。例如:
针对这种疑难的卡顿上报,需求借助用户的日志,进一步定位。在上报卡顿时,也上报用户的userid,依据用户的id在内部平台上查询用户详细的实时日志和离线日志。
实时日志:
- 记录了用户的操作路由,可定位卡顿的具体页面
- 包含行为埋点、自动化埋点、反常埋点内容,可分析用户的行为,进一步定位卡顿的代码方位
离线日志:
- 属于实时日志的补充,实时日志无法定位问题时,进一步分析离线日志
- 记录了更多的打点内容和网络恳求相关数据
总结
以上都是对线上现已发生的卡顿进行办理的计划,更重要的其实是,咱们怎么在编码阶段就规避卡顿的发生。因而,咱们也总结了一些思路和规范。
怎么避免卡顿
- 避免运用CPU自定义绘图,无事情处理的当地尽或许的运用CALayer,坚持视图的轻量;
- 尽量复用视图,削减视图的添加和移除;例如移除视图需求动画,可运用隐藏特点来完成;
- 避免重写drawRect办法,该办法会拓荒额外的内存空间进行CPU制作,更要避免在其间做耗时操作;
- 在更新布局的时分,削减layoutIfNeeded的运用,尽量只运用setNeedsLayout ;
- 将耗时操作放在子线程中进行,减轻主线程的压力
- 避免主线程进行IO相关的操作
- 针关于必须在 CPU 上进行制作的组件,测验运用多线程的异步制作能力,减轻主线程压力
- 图片的大小和UIImageView的size坚持一致,避免CPU进行伸缩操作
- 控制线程的最大并发数量,CPU调度处理也需求耗时,线程过多会使CPU繁忙
- 避免呈现离屏烘托
防劣化办法
在通过卡顿办理后,为了进一步办理优化,而且避免数据恶化,咱们采取了以下办法:
-
代码质量
进步开发阶段的代码质量,在开发阶段就削减卡顿的发生
- 建立了Code Review 准则
- 将引起卡顿的常见原因加入代码规范,code review时需特别注意
- 通过CR发现可优化点,提早发现或许引起卡顿的当地
-
版别迭代办理
每个版别上线初期,调查卡顿的上报状况。关于新增的卡顿,计算并分配使命,在下一个版别中进行优化办理。最大程度的削减了卡顿的存在,避免指标的恶化。
-
监控平台
在自研的监控平台上,用户端针对页面进行了卡顿次数的上报计算,新版别上线后,可依据版别号调查数据的改变,及时发现新的卡顿问题。
后续规划
- 卡顿办理的一期都是基于bugly工具上报的线程信息进行优化,后续用户端将接入自研的卡顿检测工具,会在此数据上进一步办理卡顿;
- 目前优化的都是普通的卡顿,关于APP卡死还未进行专项办理。后续会接入自研的工具,重点进行卡死现象的采集和办理;
- 在DEBUG形式下,敞开卡顿弹框提醒;检测到卡顿状况后弹出弹框,在开发和测验阶段可尽早的发现和办理卡顿;
结语
iOS的卡顿优化是一个杂乱且艰巨的使命,它涉及到代码的重构、逻辑的重写、底层组件的改动,在优化的同时,还必须要保障业务逻辑的正常和稳定。因而,合理地分期进行,优先处理卡顿上报量大的问题,再去处理上报量小的问题,抓大放小,继续办理,APP的用户体验必定会有耳濡目染的提升。
参阅
13 | 怎么使用 RunLoop 原理去监控卡顿?-极客时刻
2020移动使用功能办理白皮书 | 基调听云