iOS开发-视图渲染与性能优化

作者: 落影loyinglin | 来源:发表于2016-08-19 14:37 被阅读5270次

前言

关于iOS的视图渲染流程,以及性能优化的建议。
源于WWDC视频
我假设你是一个这样的开发者:

  • 了解OpenGL ES;
  • 了解view hierarchy;
  • 了解instruments;

view hierarchy和instruments网上资料很多,OpenGL ES的你可以看OpenGL ES文集

视图渲染

UIKit是常用的框架,显示、动画都通过CoreAnimation。
CoreAnimation是核心动画,依赖于OpenGL ES做GPU渲染,CoreGraphics做CPU渲染;
最底层的GraphicsHardWare是图形硬件。



下图是另外一种表现的形式。在屏幕上显示视图,需要CPU和GPU一起协作。一部数据通过CoreGraphics、CoreImage由CPU预处理。最终通过OpenGL ES将数据传送到 GPU,最终显示到屏幕。

CoreImage支持CPU、GPU两种处理模式。

显示逻辑

  • 1、CoreAnimation提交会话,包括自己和子树(view hierarchy)的layout状态等;
  • 2、RenderServer解析提交的子树状态,生成绘制指令;
  • 3、GPU执行绘制指令;
  • 4、显示渲染后的数据;


提交流程(以动画为例)

第2步为prepare to commit animation (layoutSubviews,drawRect:);


1、布局(Layout)

调用layoutSubviews方法;
调用addSubview:方法;

会造成CPU和I/O瓶颈;

2、显示(Display)

通过drawRect绘制视图;
绘制string(字符串);

会造成CPU和内存瓶颈;
每个UIView都有CALayer,同时图层有一个像素存储空间,存放视图;调用-setNeedsDisplay的时候,仅会设置图层为dirty。
当渲染系统准备就绪,调用视图的-display方法,同时装配像素存储空间,建立一个CoreGraphics上下文(CGContextRef),将上下文push进上下文堆栈,绘图程序进入对应的内存存储空间。

UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(10, 10)];
[path addLineToPoint:CGPointMake(20, 20)];
[path closePath];
path.lineWidth = 1;
[[UIColor redColor] setStroke];
[path stroke];

在-drawRect方法中实现如上代码,UIKit会将自动生成的CGContextRef 放入上下文堆栈。
当绘制完成后,视图的像素会被渲染到屏幕上;当下次再次调用视图的-setNeedsDisplay,将会再次调用-drawRect方法。

3、准备提交(Prepare)

解码图片;
图片格式转换;

GPU不支持的某些图片格式,尽量使用GPU能支持的图片格式;

4、提交(Commit)

打包layers并发送到渲染server;
递归提交子树的layers;
如果子树太复杂,会消耗很大,对性能造成影响;

尽可能简化viewTree;

当显示一个UIImageView时,Core Animation会创建一个OpenGL ES纹理,并确保在这个图层中的位图被上传到对应的纹理中。当你重写-drawInContext方法时,Core Animation会请求分配一个纹理,同时确保Core Graphics会将你在-drawInContext中绘制的东西放入到纹理的位图数据中。

Tile-Based 渲染

这里有PDF文档
Tiled-Based 渲染是移动设备的主流。整个屏幕会分解成N*Npixels组成的瓦片(Tiles),tiles存储于SoC 缓存(SoC=system on chip,片上系统,是在整块芯片上实现一个复杂系统功能,如intel cpu,整合了集显,内存控制器,cpu运核心,缓存,队列、非核心和I/O控制器)。
几何形状会分解成若干个tiles,对于每一块tile,把必须的几何体提交到OpenGL ES,然后进行渲染(光栅化)。完毕后,将tile的数据发送回cpu。

传送数据是非常消耗性能的,相对来说,多次计算比多次发送数据更加经济高效,但是额外的计算也会产生一些性能损耗。
PS:在移动平台控制帧率在一个合适的水平可以节省电能,会有效的延长电池寿命,同时会相对的提高用户体验。这里有详细的介绍

1、普通的Tile-Based渲染流程

1、CommandBuffer,接受OpenGL ES处理完毕的渲染指令;
2、Tiler,调用顶点着色器,把顶点数据进行分块(Tiling);
3、ParameterBuffer,接受分块完毕的tile和对应的渲染参数;
4、Renderer,调用片元着色器,进行像素渲染;
5、RenderBuffer,存储渲染完毕的像素;


2、离屏渲染 —— 遮罩(Mask)

1、渲染layer的mask纹理,同Tile-Based的基本渲染逻辑;
2、渲染layer的content纹理,同Tile-Based的基本渲染逻辑;
3、Compositing操作,合并1、2的纹理;


3、离屏渲染 ——UIVisiualEffectView


使用UIBlurEffect,应该是尽可能小的view,因为性能消耗巨大。


4、渲染等待

由于每一帧的顶点和像素处理相对独立,iOS会将CPU处理,顶点处理,像素处理安排在相邻的三帧中。如图,当一个渲染命令提交后,要在当帧之后的第三帧,渲染结果才会显示出来。


5、光栅化

把视图的内容渲染成纹理并缓存,可以通过CALayer的shouldRasterize属性开启光栅化。
注意,光栅化的元素,总大小限制为2.5倍的屏幕。
更新内容时,会启用离屏渲染,所以更新代价较大,只能用于静态内容;而且如果光栅化的元素100ms没有被使用将被移除,故而不常用元素的光栅化并不会优化显示。

6、组透明度

CALayer的allowsGroupOpacity属性,UIView 的alpha属性等同于 CALayer opacity属性。GroupOpacity=YES,子 layer 在视觉上的透明度的上限是其父 layer 的opacity。当父视图的layer.opacity != 1.0时,会开启离屏渲染。
layer.opacity == 1.0时,父视图不用管子视图,只需显示当前视图即可。

The default value is read from the boolean UIViewGroupOpacity property in the main bundle’s Info.plist file. If no value is found, the default value is YES for apps linked against the iOS 7 SDK or later and NO for apps linked against an earlier SDK.
为了让子视图与父视图保持同样的透明度,从 iOS 7 以后默认全局开启了这个功能。

性能优化

这个是WWDC推荐的检查项目:



1、帧率一般在多少?

60帧每秒;(TimeProfiler)

2、是否存在CPU和GPU瓶颈? (查看占有率)

更少的使用CPU和GPU可以有效的保存电量;

3、额外的使用CPU来进行渲染?

重写了drawRect会导致CPU渲染;在CPU进行渲染时,GPU大多数情况是处于等待状态;

4、是否存在过多离屏渲染?

越少越好;离屏渲染会导致上下文切换,GPU产生idle;

5、是否渲染过多视图?

视图越少越好;透明度为1的视图更受欢迎;

6、使用奇怪的图片格式和大小?

避免格式转换和调整图片大小;一个图片如果不被GPU支持,那么需要CPU来转换。(Xcode有对PNG图片进行特殊的算法优化)

7、使用昂贵的特效?

理解特效的消耗,同时调整合适的大小;例如前面提到的UIBlurEffect;

8、视图树上不必要的元素?

理解视图树上所有点的必要性,去掉不必要的元素;忘记remove视图是很常见的事情,特别是当View的类比较大的时候。


以上,是8个问题对应的工具。遇到性能问题,先分析、定位问题所在,而不是埋头钻进代码的海洋。

性能优化实例

1、阴影


上面的做法,会导致离屏渲染;下面的做法是正确的做法。

2、圆角


不要使用不必要的mask,可以预处理图片为圆形;或者添加中间为圆形透明的白色背景视图。即使添加额外的视图,会导致额外的计算;但仍然会快一点,因为相对于切换上下文,GPU更擅长渲染。
离屏渲染会导致GPU利用率不到100%,帧率却很低。(切换上下文会产生idle time)

3、工具

使用instruments的CoreAnimation工具来检查离屏渲染,黄色是我们不希望看到的颜色。


使用真机来调试,因为模拟器使用的CALayer是OSX的CALayer,不是iOS的CALayer。如果用模拟器调试,会发现所有的视图都是黄色。

总结

视频中的这一句话,让我对iOS的视图渲染茅塞顿开。

CALayer in CA is two triangles.

文章中关于Tile-Based架构,以及像素显示渲染的理解基于我对OpenGL ES学习以及iOS开发收获。
iOS开发收获很容易找到,但是OpenGL ES相对来说很少。越来越觉得自己花时间去研究是值得的。

Core Animation的核心是OpenGL ES的一个抽象物,CoreAnimation让你直接使用OpenGL ES的功能,却不需要处理OpenGL ES的复杂操作。
可是我仍越过CoreAnimation去学习OpenGL ES。因为我不满足用Apple提供的API拼凑界面。

相关文章

网友评论

  • 筑梦师Winston:这篇文章详细的说明一个视图有几种渲染方式,如何渲染到屏幕上,对我帮助颇大!!谢谢落影前辈
  • 沐晨__:最近在思考一个问题,为什么设置 `view` 的遮罩、阴影、抗锯齿、不透明等,会触发离屏渲染
    落影loyinglin:@Limon_ 是的
    沐晨__:@落影loyinglin 在 `drawRect` 方法用了 CoreGraphics Framework,此时算是 CPU 版的离屏渲染。另外一种情况是,GPU 爆满,CPU 闲置,比如设置了遮罩、阴影的属性,这种情况应该是关乎 OpenGL 的问题,有可能是在着色器程序后期的问题?对 OpenGL 这块还不熟 :grin:
    落影loyinglin:@Limon_ 你的思考答案是什么
  • mkb2:你写的文章,质量真高,厉害
  • 小鱼儿的四叶草: :+1: 厉害,大哥,我现在用openGL ES画图遇到一个问题,cpu消耗很高,从而导致帧率下降(因为我画的图比较大1920*1080*4)很多,下降到只有12/13镇的样子,有什么办法可以提高效率吗,采用Tile-Based 渲染会有提高吗
    小鱼儿的四叶草:@落影loyinglin 每帧都draw不同的图,纹理渲染就初始化的时候
    落影loyinglin:@小鱼儿的四叶草 你是每帧都上传纹理数据吗
  • OneByte:偶像
    落影loyinglin:@OneByte 谢夸奖
  • 落影loyinglin:objc中国的原文:
    当 Xcode 优化一个 PNG 文件的时候,它将 PNG 文件变成一个从技术上讲不再是有效的PNG文件。但是 iOS 可以读取这种文件,并且这比解压缩正常的 PNG 文件更快。Xcode 改变他们,让 iOS 通过一种对正常 PNG 不起作用的算法来对他们解压缩。值得注意的重点是,这改变了像素的布局。正如我们所提到的一样,在像素之下有很多种方式来描绘 RGB 数据,如果这不是 iOS 绘制系统所需要的格式,它需要将每一个像素的数据替换,而不需要加速来做这件事。
  • 4118d10da922:详细说了界面显示的原理,👍

本文标题:iOS开发-视图渲染与性能优化

本文链接:https://www.haomeiwen.com/subject/anavsttx.html