OpenGL 图片从文件渲染到屏幕的过程
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了OpenGL 图片从文件渲染到屏幕的过程相关的知识,希望对你有一定的参考价值。
参考技术A
实际上iPhone有两个帧缓冲区,一个叫屏幕缓冲区,一个叫离屏缓冲区。
从上述流程可以看出,图片的显示是通过CPU和GPU的配合完成,这其中会出现一个问题,就是在GPU收到V-Sync信号时,CPU和GPU开始工作,在下一个V-Sync信号出现时CPU和GPU都没有处理完自己的工作,导致此时的离屏缓冲区中没有帧数据,就会出现 卡顿现象 。 更详细的解释可以参考ibireme大神的 ios 保持界面流畅的技巧 。
在开始探索图片加载的流程前,先弄清楚两个概念。
我们先说说CPU都要做什么?
当使用UIImage的几个方法加载图片时,图片并不会立刻解码,它会经过一系列的步骤
以上就是着色器的渲染过程。
看到这里,我们已经知道 图片解码 过程非常耗时,对于App来说,静止的倒还好说,滑动的列表下,性能影响就会很严重了。 我们通常会用帧数FPS(Frames Per Second)来衡量手机的性能问题,60FPS(每秒60帧)是衡量会不会卡顿的标准,换算过来1帧的处理时间不应该超过0.1666s即16.7ms,接下来学习一下UIImage和YYImage的处理方式有什么可取之处。
UIImage有多种方法可以获取图片,那这些不同方法的区别是什么呢? 一、imageNamed
imageNamed 方式,在第一次渲染到屏幕上时会在主线程进行图片解码操作,位图数据会被缓存起来,之后再访问这个图片时,就从缓存获取了,看网上有说在手机发出内存警告时会清除UIImage的缓存。有两个问题: 1、第一次的图片解码操作还是在主线程做的 2、位图数据如果非常大,这么存到缓存里不太好。
二、imageWithContentsOfFile和imageWithData
imageWithContentsOfFile 和 imageWithData 在第一次渲染到屏幕时同样会在主线程进行图片解码操作,若把 UIImage 对象释放掉以后,再访问还是会出现在主线程进行图片解码的操作,这个小icon还好,大图就是有问题
所以一般市面上的做法是把 图片解码 强制提前执行,即在CPU进行解压缩前使用 CGBitmapContextCreate 在子线程中对其重新绘制。
找了YYKit的Demo直接拿来用了,YYImage的调用方式如下
YYImage集成自UIImage,重写了 imageNamed 方法避免了系统的全局缓存
这里面基本上就是从磁盘获取图片的二进制数据的过程。 scales 是类似@[@1,@2,@3]的数组,由于不同机型的物理分辨率或逻辑分辨率不同,相应的查询优先级也不同。 YYImage的多个方法都统一调用 initWithData ,接着看 initWithData
_preloadedLock :这是线程锁,通过信号量控制,为了预加载数据读取安全。 @autoreleasepool :在内部会产生大量的临时变量,为保证内存峰值不会暴涨,使用自动释放池控制 YYImageDecoder :获取图片的一些基本信息,图片宽高、帧数、图片类型 YYImageFrame :此时已经得到了解压缩后的位图
YYImage中使用的是Image I/O进行的图片解码,有两种方式 一、CGImageGetDataProvider(imageRef)方式
CGImageSourceRef :使用 CGImageSourceCreateWithData 函数,根据从磁盘获取的图片二进制数据创建imageSource CGImageRef :使用 CGImageSourceCreateImageAtIndex 函数创建一个CGImage CGDataProviderRef :位图数据提供者, CGImageGetDataProvider 函数,总之就是能从这里拿到位图数据 CFDataRef : CGDataProviderCopyData ,从DataProvider中获取CFDataRef数据。 CGImageRef : CGImageCreate ,根据位图数据再创建一个CGImage。
二、CGBitmapContextCreate方式
这里使用的是 CGBitmapContextCreate 函数进行的解压缩操作
可以看到这个函数有如下参数
通过打印执行这个函数时的线程可以看到,这个操作是 异步执行 的,后面我们会知道这确切来说是 异步串行 。
YYAnimatedImageView集成UIImageView,并重写了很多方法,先从初始化开始看
初始化时定义了一个 runloopMode 为 NSRunLoopCommonModes ,表示在runloop切换时也要播放动图。
_link :是 CADisplayLink ,计时器,用来播放动画的。 第一次进入 setImage 会先执行 imageChanged 方法来确定图片和容器大小,以及标记定时器,等待下次runloop开始播放动画。
在 resetAnimated 方法中进行了初始化操作,包括 _lock 线程锁、 _buffer 缓存容器的初始化、 _requestQueue 线程队列的初始化以及定时器 _link 的初始化等等。
1、图片渲染到屏幕的过程:从磁盘读取文件->计算Frame->图片解码->通过数据总线提交给GPU渲染->顶点着色器->光栅化处理->片元着色器着色->渲染到帧缓冲区->视频控制器指向帧缓冲区->显示。 2、YYImage避免了全局缓存,在图片显示之前就异步强制图片解压缩,对性能有很大提高,其实这个库还有很多优点,没有再仔细的去看,以后会抽时间看一下。
收录: 原文地址
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的层次结构是否合理?
以上是关于OpenGL 图片从文件渲染到屏幕的过程的主要内容,如果未能解决你的问题,请参考以下文章