圆角卡顿刨根问底

老文章迁移掘金ios

整篇文章其实说来讲去,最后其实只是把卡顿的这个事情用最通俗最简单最没技术含量的方案实行了。但那么多方案,为何选择一个方案,每一个方案都有优点,一样也有弊端,不一样的case,不一样的场景,不可能一个方案万金油适用,这个过程须要咱们刨根问底,去了解简单的解决方案背后的为何?性能优化

前言

tableView or collectionView的Cell使用中若是大量出现了view.layer.cornerRadius + ClipToBoundsormasksToBounds的设置,会形成滚动不流畅,滚动起来十分的卡顿。app

这一点相信不少iOS developer都不陌生,相关的搜索圆角卡顿圆角性能优化,都能看到不少文章,思路大体以下异步

  • 这样切圆角会形成GPU的离屏渲染
  • 离屏渲染会消耗太多GPU资源,可是CPU却没有太多的占用
  • GPU消耗太大拖慢了单帧绘制
  • 解决办法,采用CPU预先绘制bitmap,交给GPU直接渲染

最普遍的办法就是预先用CPU,构建圆角路径贝塞尔曲线UIBezierPath,用原来的图片填充进圆角路径,得到自然的自带圆角透明的bitmap数据UIImage,从而直接交给GPU进行普通渲染ide

有很多的blog,以及大量的demo,都验证了这一点,但看完后不由有几个疑问,因而产生了今天的刨根问底函数

  • 什么才是卡顿的根本缘由?
  • 离屏渲染是什么?
  • GPU消耗交给CPU,CPU难道就没消耗了么?

每一种解决方案,都是在必定得特定情景下而产生的最优解,针对CPU预绘bitmap的方案,在至关多的使用场景下,是正确的没有任何问题的。性能

可是这里要讲一个真实的case,若是这个方案都没法解决问题,依然仍是卡,那么该怎么办?学习

到底卡顿根本缘由是啥?测试

没法完全缓解卡顿,该怎么办?优化

真实Case

咱们的App有一个用UICollectionView制做了书架的功能,能够放置图书,多本图书叠放在一块儿自动生成文件夹,如图

icon

能够看到图中每一本书都是一个圆角矩形,而一个文件夹,若是图书超过4本则是4+1个圆角矩形,普通的用户使用习惯,可能不会有太多的文件夹(也说不许哟!)可是若是文件夹数量超过2个屏幕,每个文件夹都是4本以上的图书,那么在2各屏幕内来回滚动,产生了很恐怖的结果。。。

咱们的app,在ip6上,同屏幕最多能够有9个Item,每一个Item若是都是4个以上图书的文件夹就是5个圆角矩形,换句话说,一个屏幕内最多有45个圆角矩形。在这样的极限状况下,iPhone6上会卡的只有15帧左右!60帧才是满帧啊亲。。。

更况且咱们的app,在iPhone 6 plus上是4*4个item,因而一个屏幕内最多有80个圆角矩形。

咱们还支持iPad universal

细思极恐

真实Case环境说明

  • 测试中的图书阴影为贴图,不是GPU渲染,排除这部分干扰
  • 测试中的图书均为已下载完成,排除后台下载线程致使的干扰
  • 测试的书架上半部分大概有20本左右的图书,足够在2个屏幕的范围内来回滚动测试纯图书1圆角的帧率
  • 测试的书架下半部分大概有20个4本以上图书的文件夹,足够在2个屏幕的范围内来回滚动测试纯文件夹5圆角的帧率

无优化处理状况

首先在没有任何优化代码的状况下,都是最直接的

bookProfileImageView.layer.cornerRadius = 3.0f;
bookProfileImageView.clipsToBounds = YES;
复制代码

性能检测

让咱们看一组Instrument里面core Animation的数据结果

图书范围内滚动帧率

icon

咱们能够看出,在全是图书的状况下,仅仅9个圆角矩形,并不会影响帧率,至少能保持在55帧左右,滚动流畅度接近100% 而且Cpu的占用率并不高

文件夹范围内滚动帧率

icon

可怕地事情来了,在全是文件夹的状况下,已经达到了单屏45个圆角矩形,帧率已经降到了平均15,这是一种什么感受,满帧率60,如今只达到了滚动流畅的25%,简直惨不忍睹

你们再看下Cpu占用率,仍是不高

看一下Cpu的消耗状况

咱们仔细看看Cpu都消耗在哪?

icon

能够看到,Cpu的消耗在曲线图上并无陡然增高,消耗也都是一些基本的UICollectionView的处理

性能分析

能够看到,在极限纯文件夹区域滚动的时候,就只有那4个字能够形容惨不忍睹

这样的产生缘由,其实在一开始就提到了,由于圆角遮罩,在Gpu运算的时候会发生离屏渲染

离屏渲染

将离屏渲染做为关键词去搜索一下你会查到不少的信息,好比iOS离屏渲染的研究

当使用圆角,阴影,遮罩的时候,图层属性的混合体被指定为在未预合成以前不能直接在屏幕中绘制,因此就须要屏幕外渲染被唤起。 屏幕外渲染并不意味着软件绘制,可是它意味着图层必须在被显示以前在一个屏幕外上下文中被渲染(不论Cpu仍是Gpu)。 因此当使用离屏渲染的时候会很容易形成性能消耗,由于在OPENGL里离屏渲染会单独在内存中建立一个屏幕外缓冲区并进行渲染,而屏幕外缓冲区跟当前屏幕缓冲区上下文切换是很耗性能的。

离屏渲染能够是广义的理解为,在屏幕外的时候就要进行渲染,不管是Cpu仍是Gpu

在咱们的当前的Case里,由于过分的使用了Gpu去处理圆角遮罩,所以Gpu发生了大量的离屏渲染,大幅度拖慢了速度,致使帧率如此悲惨

但由于是Gpu那边的资源被过分消耗,Cpu这边显然处于比较悠闲的状态,所以,这三张图片里面,Cpu占用率一直不高,而且没有明显的某个异常函数严重消耗Cpu资源

Cpu绘制bitmap优化处理状况

那么,咱们就对他进行必定的优化

这段代码是引用来的,由于前一阵子不少人讨论这个话题,已经有了不少优秀的方案,好比 iOS高效添加圆角实战讲解

-(void)kt_addCorner:(CGFloat)radius
{
    if (self.image) {
        self.image = [self.image imageAddCornerWithRadius:radius andSize:self.bounds.size];
    }
    return;
}

- (UIImage*)imageAddCornerWithRadius:(CGFloat)radius andSize:(CGSize)size{
    CGRect rect = CGRectMake(0, 0, size.width, size.height);
    
    UIGraphicsBeginImageContextWithOptions(size, NO, [UIScreen mainScreen].scale);
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    UIBezierPath * path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(radius, radius)];
    CGContextAddPath(ctx,path.CGPath);
    CGContextClip(ctx);
    [self drawInRect:rect];
    CGContextDrawPath(ctx, kCGPathFillStroke);
    UIImage * newImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return newImage;
}
复制代码

能够看到,思路就是绘制一个圆角的路径,而后填充生成一个天生自带圆角的bitmap

而后注掉了代码中全部的cornerRadius 换上了

[bookProfileImageView kt_addCorner]
复制代码

上问题到离屏渲染能够是广义的理解为,在屏幕外的时候就要进行渲染,不管是Cpu仍是Gpu 因此思路就是,既然Gpu目前负荷会比较大,而Cpu则至关空闲,那咱们何不让Cpu提早处理一下bitmap数据呢,这也算是另外一种Cpu离屏渲染

让咱们看看效果

性能检测

让咱们看一组Instrument里面core Animation的数据结果

图书范围内滚动帧率

icon

什么鬼!图书范围的性能竟然降低了!! 对比前面的截图能够明显看出来,图书范围的滚动帧率从平均55左右降低到平均50,虽然很是的细微,但很明显,在这个优化改动下,图书范围滚动性能反而降低了!

仔细看一下Cpu占用率,经过前面的图进行对比,仍是能看出来有增高,或许不明显,咱们继续看

文件夹范围内滚动帧率

icon

你们注意看,一样的代码下,文件夹区域的滚动性能却有大幅度提高,从刚才的15帧左右提高到了35帧,提高效果超过了100%

可是咱们仔细看,Cpu占用率此次能明显看出提高了不少不少!

看一下Cpu的消耗状况

咱们仔细看看Cpu都消耗在哪?

icon

看到了吧,此时此刻,Cpu被大量消耗在了imageAddCornerWithRadius:andSize:这个咱们专门为优化而写的函数里,很明显的说明,在Cpu的层面,此处已经在重点消耗Cpu资源了

性能分析

能够看到,咱们专门针对Gpu离屏渲染作的专门的优化,彷佛?并无那么有效!是否是?

  • 首先,9圆角矩形的量级下,性能反而降低,虽然降低并不明显
  • 其次,即使是在45圆角矩形的量级下,性能还真提高很多,可是依然停留在35帧做用,直观感受,仍是卡!

到底为何会这样呢?

简单的来讲就一句话

Gpu运算会有消耗,转移给Cpu去运算必定也会产生消耗

  • 在图书区域的对比中能够看到,本来的9个圆角的数量级,在Gpu能够承受的运算范围内,此时此刻并无很大的Gpu压力,因此还算流畅55帧,
  • 可是咱们的优化方向,再减轻Gpu压力,加大Cpu负荷(虽然也没多大),所以性能反而略微降低到50帧
  • 在文件夹区域能够看到,45个圆角的数量级,Gpu已经彻底不可承受了,压力其大无比,帧率悲剧到15帧
  • 通过咱们的优化,Gpu的压力被大幅度减小,Cpu的压力随之上升,此消彼长,但最终的结果是总体帧率提高到了35帧

为何会是这样,仍是得从离屏渲染下手

离屏渲染

上文提到的离屏渲染 有这样一句话

因此当使用离屏渲染的时候会很容易形成性能消耗,由于在OPENGL里离屏渲染会单独在内存中建立一个屏幕外缓冲区并进行渲染,而屏幕外缓冲区跟当前屏幕缓冲区上下文切换是很耗性能的。

另一篇文章有这样一句话 A Performance-minded take on iOS design

You’d think the GPU would always be faster than the CPU at this sort of thing, but there are some tricky considerations here. It’s expensive for the GPU to switch contexts from on-screen to off-screen drawing (it must flush its pipelines and barrier), so for simple drawing operations, the setup cost may be greater than the total cost of doing the drawing in CPU via e.g.

咱们能够理解为Gpu在处理浮点运算,处理矩阵运算的时候,必定会比Cpu快得,毕竟他天生就是拿来作图形处理的,因此在离屏渲染的数量比较少的时候,咱们把运算交给Cpu,反而是略微增长了耗时与卡顿

就像引文里说的,离屏渲染真正的消耗,在于不一样缓冲区的来回切换,一旦圆角的数量增多,计算量加大,这种切换会更加频繁,因此当数量庞大的时候,Gpu最终全部的操做就会更加耗时

咱们面临的特殊问题

要说明的是,咱们此次的case和网上的其余例子比是有不一样的

  • 网上的一些demo都是针对UILabel UIBotton一些相对简单的UI,来进行的Cpu圆角绘制
  • 咱们的状况是,咱们是针对一张张图书封面bookcover,一个个真实的丰富多彩的png图,来进行Cpu圆角绘制,这样更加的耗时
  • 最终的结果就是,即使咱们采用了Cpu离屏渲染,可是帧率依然只有35帧左右,还卡!怎么办!

其余的优化处理办法

咱们的核心目的是,消除卡顿,感觉感觉丝般顺滑,可是现有的一些手段,虽然有效果,但还远远达不到目标怎么办?

有人说了,不要切圆角了,直接让UE出一张圆角切图,把全部的运算都省了

那咱们来试试

去掉全部圆角代码

//bookProfileImageView.layer.cornerRadius = 3.0f;
//bookProfileImageView.clipsToBounds = YES;
复制代码

换上这样的一张图,中间透明四个角有背景色

icon

让咱们测试下帧率

图书范围内滚动帧率

icon

文件夹范围内滚动帧率

icon

不管是图书,仍是文件夹都已经达到了55帧左右的帧率,接近满帧

丝般顺滑

刨根问底深刻思考

这样就知足了么?显然是不能够的,由于若是一旦圆角item背后有背景图,有纹理,那这种贴图的方式根本不能实现了。难道就这么让app卡着凑合用么

显然不能够

深刻思考卡顿的原理

解决问题应该从源头入手,因此咱们相应地要思考,卡顿是怎么来的?

这块就要从ibireme大神的 iOS 保持界面流畅的技巧 这篇博客来深刻学习

图为原博客中的图

icon

iOS设备都是采用双缓冲区+垂直同步开启的方式来进行图形渲染,什么意思呢?

  • Cpu运算处理结束后将要渲染的任务提交给Gpu
  • Gpu运算渲染完成后讲最终图形放入缓冲区
  • Gpu触发离屏渲染,会有多缓冲区来回切换管理等复杂耗时操做
  • 视频控制器在固定的频率内,从缓冲区取渲染结果,展示到显示器上
icon

这幅图更加直观

  • 每个VSync的点,都是垂直同步做用下,控制器去取渲染结果,准备展示的时间点
  • 当Vsync的点到来时,Cpu蓝色+Gpu红色都运算结束,那么就没有发生掉帧,没有发生卡顿,很顺畅的绘制了下去
  • 当Vsync的点到来时,运算没有结束,那么说明这一帧尚未渲染完毕,所以没法顺畅绘制,产生了掉帧,也就是卡顿
  • 红色的Gpu持续时间过长,会致使Vsync点到来时运算没有结束致使卡顿,这是咱们Gpu离屏渲染15帧的状况
  • 蓝色的Cpu持续时间长,也会致使Vsync点到来时运算没有结束致使卡顿,这就是咱们Cpu离屏渲染35帧的状况

当图形的总运算量在那里摆着,就是很大,就是很卡怎么办呢?

AsyncDisplay

异步绘制

简单地说,就是已经采用了Cpu离屏渲染,仍是会由于主线程计算耗时很长而卡顿UI,那咱们就把Cpu计算bitmap这个过程放到线程里去。

运算量大怎么办?

  • 优化运算,合并图层,在需求范围内替换贴图
  • 开个后台线程慢慢算,算好了再回到主线程绘制

但由于咱们面对的是频繁复用的UICollectionView或者UITableView,因此要有很完善的线程管理机制,再辅助以cache机制

采用图片的方法已经解决了当下app的卡顿问题,可是后续对AsyncDisplay的支持,等有空了再整理一篇吧。。

其实 facebook开源的 AsyncDisplayKit 就是实现了这些,功能很强大,我还没用熟,感受有点重

相关文章
相关标签/搜索