2. 图像显示原理

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2. 图像显示原理相关的知识,希望对你有一定的参考价值。

参考技术A

Layout:UI布局 文本计算
Display:绘制(drawRect)
Prepare:图片编解码
Commit:提交位图

图层的合成 ,纹理渲染,GPU渲染管线的过程:实际上这个过程指的就是OpenGL的渲染管线
渲染5步:

渲染结束之后,就会把最终的像素点,提交到对应的帧缓冲区中。

最底层是图形硬件(GPU),通过CPU Driver 来调度;上层是OpenGL和CoreGraphics,提供一些接口来访问GPU,在上面是core animation 和core image,处理动画,图形,在上面就是UIKit

像素
即RGB,位图数据有时候被称为RGB数据;
alpha,透明度,透明度直接乘以rgb对应的值

图形合成
多个图层重叠之后,需要统一各个图层的rgb然后算出最后一个展示的rgb值来进行最后展示渲染

透明与不透明:

当源纹理是完全不透明的时候,目标像素就等于源纹理。这可以省下 GPU 很大的工作量,这样只需简单的拷贝源纹理而不需要合成所有的像素值。但是没有方法能告诉 GPU 纹理上的像素是透明还是不透明的。这也是为什么 CALayer 有一个叫做 opaque 的属性了。如果这个属性为 YES,GPU 将不会做任何合成,而是简单从这个层拷贝,不需要考虑它下方的任何东西(因为都被它遮挡住了)。这节省了 GPU 相当大的工作量。

如果你加载一个没有 alpha 通道的图片,并且将它显示在 UIImageView 上,会自动设置opaque 为 YES。

对齐与不对齐

如果几个图层的模版都是完美重合,那我们只要从第一个像素到最后一个像素都计算合成一下,但是如果像素没有对齐好,我们还需要额外进行额外的移位操作,合并原纹理上的像素

两种情况会导致不对齐出现: 缩放,当纹理的起点不在一个像素的边界上

离频渲染

用通俗的语言总结一下:当我们在设置某些UI视图的图层属性,如果说指令为在未预合成之前,不能用于直接显示的时候呢,那么就触发了离屏渲染。

离屏渲染的概念起源于GPU,那GPU层面上呢,如果在当前屏幕缓冲区之外新开辟一个缓冲区去进行渲染操作的话呢,那么就是离屏渲染。

何时会触发离屏渲染

为何要避免离屏渲染?

CPU 和 GPU 在做具体的渲染过程中做了大量的工作,而离屏渲染是发生在 GPU 层面上面的,使 GPU 层面上面触发了 OpenGL 多通道渲染管线,产生了额外的开销,所以需要避免离屏渲染。

标准回答:
在触发离屏渲染的时候,会增加 GPU 的工作量,而增加 GPU 的工作量很有可能会到导致CPU和GPU工作总耗时超出了16.7ms,那么可能就会导致UI的卡顿和掉帧,那么我们就要避免离屏渲染。

另一种回答:
会创建新的渲染缓冲区,会有内存上的开销
会有上下文的切换,因为有多通道渲染管线,要把多通道的渲染结果进行一个合成,那么就有GPU一个额外的开销。

CPU资源消耗分析

1、对象创建:对象的创建会分配内存、调整属性、甚至还有读取文件等操作,比较消耗CPU资源。尽量采取轻量级对象,尽量放到后台线程处理,尽量推迟对象的创建时间。(如UIView / CALayer)

2、对象调整:frame、bounds、transform及视图层次等属性调整很耗费CPU资源。尽量减少不必要属性的修改,尽量避免调整视图层次、添加和移除视图。

3、布局计算:随着视图数量的增长,Autolayout带来的CPU消耗会呈指数级增长,所以尽量提前算好布局,在需要时一次性调整好对应属性。

4、文本渲染:屏幕上能看到的所有文本内容控件,包括UIWebView,在底层都是通过CoreText排版、绘制为位图显示的。常见的文本控件,其排版与绘制都是在主线程进行的,显示大量文本是,CPU压力很大。对此解决方案唯一就是自定义文本控件,用CoreText对文本异步绘制。(很麻烦,开发成本高)

5、图片解码:当用UIImage或CGImageSource创建图片时,图片数据并不会立刻解码。图片设置到UIImageView或CALayer.contents中去,并且CALayer被提交到GPU前,CGImage中的数据才会得到解码。这一步是发生在主线程的,并且不可避免。SD_WebImage处理方式:在后台线程先把图片绘制到CGBitmapContext中,然后从Bitmap直接创建图片。

6、图像绘制:图像的绘制通常是指用那些以CG开头的方法把图像绘制到画布中,然后从画布创建图片并显示的一个过程。CoreGraphics方法是线程安全的,可以异步绘制,主线程回调。

GPU资源消耗分析

1、纹理混合:尽量减少短时间内大量图片的显示,尽可能将多张图片合成一张进行显示。

2、视图混合:尽量减少视图层次和数量,并在不透明的视图里标明opaque属性以避免无用的Alpha通道合成。

3、图形生成:尽量避免离屏渲染,尽量采用异步绘制,尽量避免使用圆角、阴影、遮罩等属性。必要时用静态图片实现展示效果,也可尝试光栅化缓存复用属性。

4、 比如视图层级十分复杂,那GPU需要合成每一个对应像素点的像素值,做大量的计算,这个合成过程也会变得复杂。减轻视图层级的复杂性,会减轻GPU合成视图时的压力。 包括CPU的异步绘制机制,来达到提交的位图本身就是一个层级非常少的视图,这样也可以减轻GPU的压力。

如果UIView实现了方法
方法: - (void)displayLayer:(CALayer*)layer;
就会进行异步绘制,反之,走系统绘制;

调用 UIView 的setNeedsDisplay之后并没有立即执行当前视图的绘制工作,而是在调用时立即调用当前view 的 layer 的同名方法,于是在当前 layer 上打上了一个脏标记,在当前 runloop快要结束的时候才会调用CALayer display方法,然后才会进行当前视图真正的绘制流程。

drawLayer: inContext: 实现了这个方法,就不会再去走 drawRect:,没实现就直接走drawRect:

屏蔽drawLayer: inContext: 则会进入drawRect:方法。这里为什么要有个drawLayer: inContext:方法呢?为什么不直接drawRect:,我猜想可能是为了增加灵活性吧,drawRect:是UIView的一个方法,只能在UIView中调用。而drawLayer:inContext:则更加自由,是要实现了CALayer的代理的类都可以使用drawLayer: inContext:。

允许我们在系统的绘制之上,做一些其他的绘制工作

左侧是主队列,右侧是全局并发队列,假如我们在某一时机一个 View 调用了setNeedsDisplay这个方法之后呢,在当前 runloop 将要结束的时候呢,系统就会调用视图所对应 layer 的display方法。

如果我们的代理实现了displayLayer:这个函数的时候,会调用代理的displayLayer:这个方法,然后会通过子线程的切换,在子线程中进行位图的绘制。主线程这会就可以做一些其他的工作。

子线程在全局并发队列中所做的工作:

之后再回到主队列当中,提交这个位图,设置给 CALayer 的contents属性,这样就完成了一个UI控件 的异步绘制过程

是否受到CPU或者GPU的限制?
是否有不必要的CPU渲染?
是否有太多的离屏渲染操作?
是否有太多的图层混合操作?
是否有奇怪的图片格式或者尺寸?
是否涉及到昂贵的view或者效果?
view的层次结构是否合理?

以上是关于2. 图像显示原理的主要内容,如果未能解决你的问题,请参考以下文章

Android 进阶——图形显示系统之底层图像显示原理小结

Android 进阶——图形显示系统之底层图像显示原理小结

LCD硬件原理

图像旋转的原理与实现

2维图像旋转(转)

iOS 图像渲染原理