iOS底层原理 - 性能优化 之 卡顿优化

面试题引发的思考:

Q: 列表卡顿的原因?如何优化?

  • 卡顿主要是因为在 主线程 执行了 比较耗时 的操作。

Q: 卡顿解决方法?

  • CPU:
    • 尽量用轻量级的对象:CALayerint等;
    • 不要频繁调用UIView的相关属性;
    • 提前计算好布局;
    • Autolayout会比直接设置frame消耗更多的CPU资源;
    • 图片的size最好刚好跟UIImageViewsize保持一致;
    • 控制一下线程的 最大并发数量;
    • 尽量把 耗时的操作 放到 子线程,充分利用CPU的多核。
  • GPU:
    • 尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示;
    • GPU能处理的最大纹理尺寸是4096x4096;
    • 尽量减少视图数量和层级;
    • 减少透明的视图(alpha<1),不透明的就设置opaqueYES
    • 尽量避免出现离屏渲染。

Q: 如何检测卡顿?

  • 通过添加Observer到 主线程RunLoop 中,监听 RunLoop状态切换 的耗时,以达到监控卡顿的目的。

Q: 何为离屏渲染?

  • 当前屏幕渲染:GPU在 当前屏幕缓冲区 进行渲染操作;
  • 离屏渲染:GPU在 当前屏幕缓冲区以外 新开辟一个缓冲区进行渲染操作。

Q: 离屏渲染触发时机?

  • 图层属性的混合体 在没有预合成之前 不能直接在屏幕中绘制,此时会触发离屏渲染。

Q: 为什么离屏渲染消耗性能?为何要避免离屏渲染?

  • 创建新的缓冲区;
  • 多次切换上下文切换。
  • 触发离屏渲染时,会增加GPU的工作量,可能会导致CPU和GPU的总工作耗时超出16.7ms,进而导致UI的卡顿和掉帧。

Q: 哪些操作会触发离屏渲染?

  • 圆角:cornerRadiusmasksToBounds同时设置时;
    解决方案:UI提供圆角图片、CoreGraphics绘制裁剪圆角;
  • 阴影:layer.shadowXXX
    如果设置了layer.shadowPath就不会产生离屏渲染。
  • 光栅化:layer.shouldRasterize = YES
  • 图层蒙版:layer.mask

1. 屏幕成像原理

(1) CPU和GPU的作用

在屏幕成像过程中,CPU和GPU起着至关重要的作用:

  • CPU (Central Processing Unit,中央处理器):
    作用:对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics);
  • GPU (Graphics Processing Unit,图形处理器):
    作用:纹理的渲染。

(2) 屏幕成像流程

屏幕成像流程如下:

屏幕成像流程
  • CPU进行UI布局、文本计算、图片解码等等,计算完毕将数据提交给GPU
  • GPU对数据进行渲染,渲染完毕将数据放到 帧缓存 里面;
  • 视频控制器帧缓存 读取数据;并将数据显示到 屏幕 上。

在iOS中是双缓冲机制,有前帧缓存、后帧缓存。上图的帧缓存有两块区域,当一块区域满了或一块区域正在进行其他操作,此时GPU会使用另外一块区域缓存,提升效率。


(3) 屏幕成像原理

屏幕成像原理如下:

屏幕成像原理

手机屏幕上的动画是通过一帧一帧(或者说一页)数据组成的。

  • 当屏幕需要显示一帧数据的时候,会发送一个垂直同步信号;然后逐行发送水平同步信号,直到填充整个屏幕,此时一帧数据就显示完成。
  • 接下来再发送一个垂直同步信号;然后逐行发送水平同步信号,直到完成这一帧。
  • 当所有帧数据发送完成之后,这些帧连起来就是屏幕上的动画了。

2. 卡顿产生原因

屏幕成像流程

如上图,红色箭头是CPU计算需要的时间,蓝色箭头是GPU渲染需要的时间。

注意:发送一个垂直同步信号,就会立即把CPU计算和GPU渲染完成的数据从帧缓存中读取显示到屏幕上,并且立即开始下一帧的操作。

  • 第一帧的数据需要显示,发送一个垂直同步信号,此时将CPU计算和GPU渲染完成的数据从帧缓存中读取显示到屏幕上,就完成了第一帧的显示。

  • 第二帧的数据CPU计算和GPU渲染的比较快,在下一次垂直同步信号来之后,将CPU计算和GPU渲染完成的数据从帧缓存中读取显示到屏幕上,完成了第二帧的显示。

  • 第三帧的数据CPU计算和GPU渲染的比较慢,在下一次垂直同步信号来到之后还没处理完毕,此时帧缓存里面还是第二帧的数据,将第二帧的数据从帧缓存中读取显示到屏幕上,如此便产生掉帧现象,也就是我们说的卡顿

  • 第三帧的数据会在下一帧的垂直同步信号来到之后再显示到屏幕上,慢了一帧的时间。


3. 卡顿解决方法

卡顿解决方法是尽可能减少CPU、GPU资源消耗。

  • 一般帧率FPS(Frames Per Second每秒传输帧数)达到60FPS就会感觉不到卡顿;
  • 按照60FPS的刷帧率,每隔16ms就会有一次VSync信号(1000ms / 60 = 16.667ms)。

(1) 卡顿优化 CPU

1> CPU卡顿优化方式

  • 尽量用轻量级的对象,比如无需事件处理的地方使用CALayer取代UIView、能用int就不用NSNumber

  • 不要频繁地调用UIView的相关属性,比如frameboundstransform等属性,尽量减少不必要的修改;

  • 尽量提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性;

  • Autolayout会比直接设置frame消耗更多的CPU资源,因为Autolayout本身性能就不是很高;

  • 图片的size最好刚好跟UIImageViewsize保持一致,如果不一致CPU就会对图片进行伸缩操作,这样比较消耗CPU资源;

  • 控制一下线程的最大并发数量,不要无限制的并发,这样比较消耗CPU资源;

  • 尽量把耗时的操作放到子线程,充分利用CPU的多核。

2> 耗时操作:文本处理(尺寸计算、绘制)

比如:boundingRectWithSize计算文字宽高是可以放到子线程去计算的,或者drawWithRect文本绘制,也是可以放到子线程去绘制的,如下:

- (void)text {
    // 此类操作可以放到子线程
    // 文字计算
    [@"text" boundingRectWithSize:CGSizeMake(100, MAXFLOAT) 
    options:NSStringDrawingUsesLineFragmentOrigin attributes:nil context:nil];

    // 文字绘制
    [@"text" drawWithRect:CGRectMake(0, 0, 100, 100) 
    options:NSStringDrawingUsesLineFragmentOrigin attributes:nil context:nil];
}

3> 耗时操作:图片处理(解码、绘制)

我们经常会写如下代码加载图片:

- (void)image {
    UIImageView *imageView = [[UIImageView alloc] init];
    imageView.frame = CGRectMake(100, 100, 100, 56);
    //加载图片
    imageView.image = [UIImage imageNamed:@"timg"]; 
    [self.view addSubview:imageView];
    self.imageView = imageView;
}

通过imageNamed加载图片,加载完成后是不会直接显示到屏幕上面的,因为加载后的是经过压缩的图片二进制,当真正想要渲染到屏幕上的时候再拿到图片二进制解码成屏幕显示所需要的格式,然后进行渲染显示,而这种解码一般默认是在主线程操作的,如果图片数据比较多比较大的话也会产生卡顿。

一般的做法是在子线程提前解码图片二进制,不需要主线程解码,这样在图片渲染显示之前就已经解码出来了,主线程拿到解码后的数据进行渲染显示就可以了,这样主线程就不会卡顿。网上很多图片处理框架都有这个异步解码功能的。下面演示一下:

图片异步解码

上面代码,不单单通过imageNamed加载的本地图片可以提前渲染,通过imageWithContentsOfFile加载的网络图片也可以这样进行提前渲染,只要获取到UIImage对象都可以对UIImage对象进行提前渲染。


(2) 卡顿优化 GPU

1> GPU卡顿优化方式

  • 尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示,这样只渲染一张图片,渲染更快;

  • GPU能处理的最大纹理尺寸是4096x4096,一旦超过这个尺寸,就会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸;

  • 尽量减少视图数量和层级,视图层级太多会增加渲染时间;

  • 减少透明的视图(alpha<1),不透明的就设置opaqueYES,因为一旦有透明的视图就会进行很多混合计算增加渲染的资源消耗;

  • 尽量避免出现离屏渲染。

2> 离屏渲染

在OpenGL中,GPU有2种渲染方式:

  • On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作;
  • Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。

当前用于显示的屏幕缓冲区就是下图的帧缓存:

屏幕成像流程

Q: 离屏渲染触发时机?

  • 图层属性的混合体 在没有预合成之前 不能直接在屏幕中绘制时,会触发离屏渲染。

Q: 为什么离屏渲染消耗性能?为何要避免离屏渲染

  • 创建新的缓冲区;
  • 多次切换上下文切换。
  • 触发离屏渲染时,会增加GPU的工作量,可能会导致CPU和GPU的总工作耗时超出16.7ms,进而导致UI的卡顿和掉帧。

Q: 哪些操作会触发离屏渲染?

  • 圆角:cornerRadiusmasksToBounds同时设置时;
    解决方案:UI提供圆角图片、CoreGraphics绘制裁剪圆角;
  • 阴影:layer.shadowXXX
    如果设置了layer.shadowPath就不会产生离屏渲染。
  • 光栅化:layer.shouldRasterize = YES
  • 图层蒙版:layer.mask

Q: 为什么要开辟新的缓冲区?

因为上面进行的那些操作比较耗性能、资源,当前屏幕缓冲区不够用(就算是双缓冲机制也不够用),所以才会开辟新的缓冲区。


(3) 卡顿检测

“卡顿”主要是因为在主线程执行了比较耗时的操作;

通过添加Observer到主线程RunLoop中,监听RunLoop状态切换的耗时,以达到监控卡顿的目的。

如下图,主线程的大部分操作,比如点击事件的处理,view的计算、 绘制等基本上都在source0source1。我们只要监控一下从结束休眠(步骤08)处理soure1(步骤09-C)一直到绕回来处理source0(步骤05), 如果发现中间消耗的时间比较长,那么就可以证明这些操作比较耗时。

RunLoop运行逻辑

借助可以监控哪个方法卡顿的第三方库<LXDAppFluecyMonitor>,进行检测:

// TODO: ----------------- ViewController类 -----------------
- (void)viewDidLoad {
    [super viewDidLoad];
    //开启卡顿检测
    [[LXDAppFluecyMonitor sharedMonitor] startMonitoring];
    [self.tableView registerClass: [UITableViewCell class] forCellReuseIdentifier: @"cell"];
}

#pragma mark -
#pragma mark - UITableViewDataSource
- (NSInteger)tableView: (UITableView *)tableView numberOfRowsInSection: (NSInteger)section {
    return 1000;
}

- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
    UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier: @"cell"];
    cell.textLabel.text = [NSString stringWithFormat: @"%lu", indexPath.row];

    if (indexPath.row > 0 && indexPath.row % 30 == 0) {
        // 模拟卡顿
        sleep(2.0);
    }
    return cell;
}

运行以上代码,打印结果如下:

打印结果

由打印结果可知:可以检测到cellForRowAtIndexPath的卡顿。

下面简单介绍下LXDAppFluecyMonitor框架的核心代码:

LXDAppFluecyMonitor框架里面就两个文件:
LXDBacktraceLogger文件里面是关于方法调用栈的一些代码;
LXDAppFluecyMonitor文件就是卡顿检测文件。

进入LXDAppFluecyMonitor文件的startMonitoring方法:

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

推荐阅读更多精彩内容