2021-12-06一次tableView的完整优化记录

本文提纲:
1、什么是离屏渲染?
2、检测app的性能工具,可不可以查看到竞品的各项性能;
3、优化过程。

什么是离屏渲染?

要想深刻的了解离屏渲染,我们要从屏幕显示图片的原理说起。

首先从过去的CRT(Cathode Ray Tube)显示原理说起。

ios_screen_scan.png

CRT电子枪按照上面的方式,从上到下一行一行的扫描,扫描完成后,显示器就呈现出一帧的画面,这一帧完成之后,电子枪会回到初始位置准备下一次的扫描。

为了把显示器的显示过程和系统的视频控制器进行同步,显示器(或者其他硬件)会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行准备扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称HSync;而当一帧绘制完毕后,电子枪复原到初始位置,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchroization),简称VSync。显示器通常以固定的频率刷新,这个刷新率就是VSync信号产生的频率。液晶显示屏的原理也是这样。

ios_screen_display.png

通常来说,计算机系统中的CPU、GPU、显示器是以上面这种方式协同工作的。CPU会计算好显示的内容,然后提交给GPU,GPU渲染完成后将结果放到帧缓冲区(FrameBuffer),随后视频控制器会按照VSync信号逐行读取FrameBuffer中的数据,经过一系列的数模转换传递给显示器显示。

那么在VSync信号来临之前,如果上一个VSnyc信号的把渲染完毕的图片还没有显示完,或者说在上一个VSnyc信号结束之前,帧缓冲区里没有新的内容CPU或者GPU还在处理图像的内容都没有内容提交,那么就没有办法读取到下一帧,我们看到的画面就会停留在上一帧渲染完成的屏幕,也就是造成了卡顿

最简单的情况,帧缓冲区只有一个的时候,那么读取和刷新会有很大的效率问题。现在iOS的设备大多都是双缓冲区,安卓是三缓冲区。

离屏渲染

在iOS中,图像显示的方式依赖于OpenGL,在OpenGL中,GPU有两种渲染方式:
On-Screen RenderingOff-Screen Rendering;也就是当前屏幕渲染,和离屏渲染。

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

那我们常说要避免离屏渲染,是因为离屏渲染会造成大量的系统开销,比较耗性能:
1、离屏渲染需要创建新的缓冲区;
2、离屏渲染的整个过程要多次切换上下文环境,先是从当前屏幕切换到离屏,离屏结束后,将离屏的缓冲区渲染到屏幕上,又需要从离屏切换到当前屏幕;

既然离屏渲染造成如此大的系统开销,那它有什么作用嘛?
  • 一些特殊效果需求运用额外的 Offscreen Buffer 来保存的中心情况,所以不得不运用离屏渲染去处理复杂的图像。
  • 处于效率的意图,在offscreen buffer中的数据也可以被复用。
触发离屏渲染的逻辑

图层的叠加遵循“画家算法”,这种算法会按图层绘制,先去绘制较远的场景,然后绘制较近的部分遮盖较远的部分。在一般的layer绘制中,上层的sublayer会覆盖基层的sublayer,基层绘制完毕的layer就会清除掉。全部的layer绘制完毕后,整个绘制过程就结束了。如果我们不设置剪裁和圆角,那么整个绘制过程如下:


image.png

但是当我们设置了cornerRadius以及 masksToBounds 进行圆角 + 裁剪时,masksToBounds会应用到全部的sublayer上。这也就是说,全部的sublayer都会被剪裁,为了节省效率,全部的sublayer在绘制完毕以后,要进行一次性的剪裁。所以也就是说明,在sublayer在第一次被绘制完毕以后,sublayer不能直接被清除掉,而是会被保存在Offscreen buffer 中等待下一轮的圆角+剪裁,这也就导致了离屏渲染。过程如下:

image.png

总结:在一次画家算法完毕之后,如果还需要额外的裁剪处理就会产生离屏渲染

如果是只有单图层,那么在绘制的过程中会进行直接裁剪,此时不会发生离屏渲染,因为不需要额外的空间去保存图层了只有这一层。

检测app的性能工具
  • 卡顿的检测
    关于界面是否卡顿我们通常用到的参数是FPS,正常情况下是每秒刷新60帧,也就是说在16.67ms内处理完一次绘制任务,当帧率小于60时,说明界面开始卡顿,当低于45的时候卡顿会比较明显。

我们可以使用xcode自带的工具instrumentAnimation Hitches来检测卡顿。

instrument:Instruments是一个强大而灵活的性能分析和测试工具,它是Xcode工具集的一部分。它旨在帮助您分析iOS、watchOS、tvOS和macOS应用程序、流程和设备,以便更好地了解和优化它们的行为和性能。

测试结果.png

我在iPhone8(iOS 13.1.2)上进行的测试,结果如上,发现了大量的卡顿,因为除了大部分的16.67ms是正常的,还有一部分的33.34m和50.0ms甚至还有大于1s的地方,后面会说下优化。

  • 离屏渲染的检测
    首先我在模拟器上进行debug,开启了Color-offScreen Rendered,所有产生离屏渲染的部分会被标记为黄色:


    image.png

然后开启了Color Blended Layers,这个选项基于渲染程度对屏幕中的混合区域进行绿到红的高亮,性能越差的区域就越红(也就是多个半透明图层的叠加)。由于重绘的原因,混合对 GPU 性能会有影响,同时也是滑动或者动画帧率下降的罪魁祸首之一。

image.png
检测竞品的性能数据

了解了自己的app之后,然后就想看看能不能看看竞品的app的性能数据,于是就上网搜索各种方法。

功夫不负有心人!让我找到了啊,找到了一款工具PerfDog可以测试手机上安装的任意一款的性能,虽然我不太明白这个原理到底是什么,等我了解了再进行整理和记录。

我们是做小说阅读的,所以测了下7猫的性能,然后测了下我们自己的又,确实差了不少😓,革命尚未完成,少年仍需努力!

七猫.png
对比图.png

虽然说有一些参数还比较陌生看不太懂,但是这个平台上的文档都有解释,后续查阅一下着重解释一个,既然发现了差距,接下来就是要思考怎么缩小,自己哪里做的出问题了,开始优化!

优化过程

我们通过👆🏻上面的测试可以了解到我们现在存在的问题:
1、图片部分的离屏渲染严重,圆角的优化很有必要。
2、部分透明的layer图层混合增加了绘制的难度,需要设置背景颜色。
3、通过instrument可以了解到一部分耗时的操作是在:在滑动时部分的autolayout自动布局的

计算上,我们用的框架是Masonry,可以考虑用frame替代,或者滑动时不再自动布局。
因为我们的cell的行高是在拿到数据之后在一个管理cell的model中已经计算好了,所以显示的时候没有再去计算了,相当于行高已经缓存好了,直接读就可以,这个地方不用再优化。

我们先从以上这三个方向进行优化,然后再进行测试看看效果,比对下性能。

  • 圆角的优化
    原来使用的切圆角的框架是XKCornerRadius,它是通过创建CAShapeLayer添加到layer上,从目前的离屏渲染检测上来看,这种圆角会引发离屏,所以打算换一种方式来进行。于是就上github上搜一下看看有没有好用的库。这部分我的思路是,先去找有没有已经实现的好用的第三个的框架,如果没有,或者框架过于庞大可以自己去通过UIBezierPath的方式,或者依赖于UI去切一个带圆角的蒙层方式自己处理,通过性能比对下两个方式那个更好。

于是找到了一个ZYCornerRadius,无离屏渲染,然后内容只有一个UIImageView的分类,非常奈斯~哈哈哈哈,是通过UIBezierPath和UIGraphicsGetImageFromCurrentImageContext重绘image的方式处理的,使用完以后效果如下:

image.png

书封的部分应用了这个圆角的处理方法,确实没有了离屏渲染。

  • 优化部分透明的layer图层混合
    给红色的地方设置一个背景颜色,设置前效果如下:


    image.png

然后我就设置了下图片的背景色,发现并不管用,所以他这里造成的图层混合不是因为UIImageView没有背景颜色,具体原因我继续上网搜索资料。然后发现sd_webImage会有影响,于是我屏蔽掉这句代码:

-(void)setMenuModel:(YueYouBookStoreTabMenu *)menuModel{
    _menuModel = menuModel;
//    [_iconImageView sd_setImageWithURL:[NSURL URLWithString:menuModel.imageUrl]];
    _iconLabel.text = menuModel.name;
}

然后效果图:


image.png

所以是下载完的图片出现了问题,因为我原来这个里面有占位图,这个时候它不产生混合区域。于是我们着重在下载完的图片之后看看能不能做什么处理。通过查阅资料我们了解到之所以会产生红色区域是因为:

  • 由于网络图片不会有@2x和@3x之分,通过SDWebImage库下载的图片不加以处理就直接显示,会有一些常见的问题,如像素不对齐。

  • App中经常使用圆角图片,一般采用裁剪图片的方式;但是这些图片源来自服务器(本地圆角图片让UI直接提供就可以了),我们需要在SDWebImage基础上增加对网络圆角图片的处理。

那么像素不对齐会引发什么问题呢?
  • 像素不对齐是指物理像素(pixel)不对齐;出现像素不对齐,会导致GPU在渲染时,对没对齐的边缘,进行插值计算,造成性能损耗了。

  • 当图片的size和显示图片View的size不同 或 图片的scale和屏幕的scale不同,就会发生像素不对齐的问题。要想像素对齐,必须保证image.size和显示图片view.size相等 且 image.scale和 [UIScreen mainScreen].scale相等。

  • 当UIView(及其子类)的frame像素不对齐显示洋红色;当图片的像素大小与控件的大小不一致,显示黄色。

所以其实我们要解决的是,当图片下载完毕后我们要调整一下图片的大小,来避免这种不对齐的情况。在下载图片完成的回调里对图片的大小进行下处理。

我们先把label都设置上背景色,建了一个label的基类,统一处理一下背景颜色。然后最后再去统一处理图片,设置完背景色效果图如下:


image.png

那么最后剩下的红色区域的都是图片的问题了,我们来处理这个图片的问题。我通过image的缩放处理发现,并没有解决这个问题,我在SDWebImage图片的回调中处理如下:

- (void)yy_setImageWithImageUrlStr:(NSString *)urlStr yyCompleted:(nonnull yy_completed)yycompleted{
    [self sd_setImageWithURL:[NSURL URLWithString:urlStr] placeholderImage:[UIImage imageNamed:@"coverPlaceholder"] completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
        [image cornerImageWithSize:self.bounds.size fillColor:[UIColor whiteColor] cornerRadius:2.0f completion:^(UIImage * _Nonnull resultImage) {
                yycompleted(image,imageURL);
        }];
    }];
}

cornerImageWithSize是处理图片的缩放的问题的,但是在Color Blended Layers开启后,并没有明显的优化效果(看来SDWebImage已经优化了这个部分的问题,搜到的资料说有这个问题的都比较早期):

image.png

于是又查找问题,我们的这个书封的图片构成是由三个部分组成的:

  • 主图部分:显示图片的imageView
  • 阴影部分:书封底部的阴影图片
  • 还有一个1像素通过drawRect绘制的内边框部分

现在可以排除问题不在imageView的显示上,然后来看阴影和内边框部分,我通过代码注释掉内边框,或者直接给内边框的view一个背景颜色,他原来是透明的view,大概猜想问题就出现在这个透明。于是我就设置了个背景白色:


image.png

然后书封部分的红色就消失了:


image.png

或者我直接不添加这层border,如下:
image.png

书封下面那个小的红色的是阴影的部分,应该也是图片的透明导致的,这个部分到时候管UI同学要一个不带透明度的图就好了,于是接下来最后处理的问题就是这个border的绘制,还是用UIBezierPath绘制一下,不使用view的drawRect实现了。


image.png

OK这个问题解决,我在上边画了一个宽5个像素的大黑边,为了效果明显一点,可以看到这个部分不再显示红色了,剩下的都是本地图片的处理了✌🏻!


image.png
  • 优化cell在滑动时autoLayout的自动布局
    这个部分就根据instrument的调试,去一点一点修改了部分UI的数值的设置问题,从xcode自带的debug view hierarchy先来查看一部分UI有问题的部分:
    image.png

上面图片标记红色的部分是UI的设置有问题的部分,根据debug view hierarchy的提示可以先去优化这个部分的ambiguous的问题。其实xcode会帮你标记出来是哪个view有问题,要仔细去看,比如下图,我标记出的UI控件是已经加了蓝色描边的,但是要仔细一点,不然就可能找不到,xcode帮忙标记出来就非常方便了:

image.png

然后我去查看这个view的实现代码,果然有问题,不小心写成了这个鬼样子!(Σ(⊙▽⊙"a):

 [_rankImageView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.mas_equalTo(self.bookShadowImageView.mas_right).offset(kMatchW(12));
        make.top.mas_equalTo(self.bookShadowImageView).offset(kMatchW(5));
        make.width.mas_equalTo(kMatchW(14));
        make.width.mas_equalTo(kMatchW(16));
    }];

后面就一点一点跟着提示去处理就可以了。然后继续处理cell赋值时检查下有没有更新layout的部分,这个部分也比较耗时,把滑动时更新的layout处理掉后,会有明显的优化效果,但是还是不符合预期,还需要进一步的优化。

  • 层级结构自查
    我们的主界面是通过tableView嵌套collectionView进行实现的,我们参考了其余APP的层级实现,觉着这个部分也可以接着优化,上网搜了一些资料,这里有两个思路:

  • 一层collectionView去实现,外层不嵌套tableView(参考番茄小说)

  • tableView+cell去实现 内部不嵌套collectionView了 (参考qq阅读)

我用自己的手机IPhone11(iOS 15.0)分别下载看了下效果,发现番茄的还是有一点卡顿(肉眼可见的那种🐶),qq的比较流畅,因为原来的部分也是基于外层是table实现的,所以table在外层其实比较好改也。

虽然我们的书城的结构和番茄小说的很像, 包括榜单的部分结构都相似,那个部分需要collectionViewcell内部再加collectionView还有一个scrollView,感觉这样嵌套导致了卡顿,尽管我的离屏渲染已经做过优化工作了,我猜测可能是因为嵌套代理的回调导致耗时,由于工时的原因,后面搞清楚了再来记录。

而且用collectionView还有一个问题就是不同section的layout布局管理,这个地方需要一定的设计,不然数据源和布局就会很凌乱,不好维护,collectionView要比tableView复杂,我们目前的需求cell的样式从产品上定义大概有十几种左右,定义的枚举就十九种,所以用collecionView直接去根据section去布局代码会很凌乱。


cell模块枚举类型定义示例.png

出于上述,工时,以及效果,还有修改难度等因素来考虑,打算用tableView+cell不再嵌套多层的方案来继续修改一下。

  • 自查出来的复用问题


    image.png

因为有一些不同的section的样式用的是一个cell只是根据type去区分下细微的UI,所以在复用的ID的时候拼接了section,然而从服用队列中取的时候并没有拼接section!导致部分的cell一直在创建!

这个问题是在代码自查,在局部cell的创建打了个断点,发现滑动的过程中创建代码一直执行,然后找到了问题的根源。

通过以上这些优化,终于卡顿消失了,性能也提升了不少,最后放上结果对比效果,还是很明显的!

结果比对
  • 通过性能测试工具PrefDog可以看到性能明显的提升


    结果比对.png
  • instruments测试可以看到大部分都是16.67ms了


    image.png
  • 录制的实际界面感受

附地址:https://v.youku.com/v_show/id_XNTgyNTc0MDA4OA==.html

总结

非常开心能有这个机会来做一次这样的优化,通过文字记录,一是希望后来者可以通过这片文章再遇到相似的问题,有更多的解决思路和灵感。二是让自己印象更加深刻,并且通过这样的一次优化,间接的了解和学习到很多新的知识,记录一次让自己印象更加深刻,也方便自己以后有类似的问题再去查阅相关资料。

最后附了对于解决这次问题比较有帮助的一些参考资料(感谢这些大佬)。

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

推荐阅读更多精彩内容