美文网首页
iOS绘制与渲染--渲染流程

iOS绘制与渲染--渲染流程

作者: 人生看淡不服就干 | 来源:发表于2017-03-25 13:15 被阅读952次

    视图渲染框架

    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
    中绘制的东西放入到纹理的位图数据中。




    渲染总流程

    CPU与GPU协作 同步时钟触发重绘 掉帧卡顿问题

    在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。

    渲染时机

    上面已经提到过:Core Animation 在 RunLoop 中注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件 。当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。当Oberver监听的事件到来时,回调执行函数中会遍历所有待处理的UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

    这个函数内部的调用栈大概是这样的:

    _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
        QuartzCore:CA::Transaction::observer_callback:
            CA::Transaction::commit();
                CA::Context::commit_transaction();
                    CA::Layer::layout_and_display_if_needed();
                        CA::Layer::layout_if_needed();
                              [CALayer layoutSublayers];
                              [UIView layoutSubviews];
                        CA::Layer::display_if_needed();
                              [CALayer display];
                              [UIView drawRect];
    

    渲染具体步骤

    动画和屏幕上组合的图层实际上被一个单独的进程管理,即所谓的渲染服务。
    当运行一段动画时,这个过程会被四个分离的阶段打破:

    1. 布局--准备视图的层级关系,设置图层属性
    2. 显示--图层的寄宿图片被绘制的阶段。涉及到-drawRect和-drawLayer:inContext:等方法
    3. 准备--准备发送动画数据给渲染服务的阶段。比如图片解码
    4. 提交--打包所有图层和动画属性,通过IPC发送到渲染服务

    渲染服务拿到数据后,反序列化成一个叫做渲染树的图层树,使用这个树状结构,渲染服务队动画的每一帧做如下工作:

    1. 对所有的图层属性计算中间值,设置OpenGL几何形状(纹理化三角形)来执行渲染
    2. 在屏幕上渲染可见的三角形

    所以一共六个阶段:最后两个阶段在动画过程中不停地重复,前五个阶段都在软件层面处理(通过CPU),只有最后一个被GPU执行。而且,你真正只能控制前两个阶段:布局和显示。剩下的在CoreAnimation内部处理。

    CADisplayLink简介

    当你设置一个NSTimer,他会被插入到当前任务列表中,然后直到指定时间过去之后才会被执行。但是何时启动定时器并没有一个时间上限,而且它只会在列表中上一个任务完成之后开始执行。这通常会导致有几毫秒的延迟,但是如果上一个任务过了很久才完成就会导致延迟很长一段时间。

    用CADisplayLink而不是NSTimer,会保证帧率足够连续,使得动画看起来更加平滑,但即使CADisplayLink也不能保证每一帧都按计划执行,一些失去控制的离散的任务或者事件(例如资源紧张的后台程序)可能会导致动画偶尔地丢帧。当使用NSTimer的时候,一旦有机会计时器就会开启,但是CADisplayLink却不一样:如果它丢失了帧,就会直接忽略它们,然后在下一次更新的时候接着运行。

    参考文章

    iOS开发-视图渲染与性能优化
    iOS 事件处理机制与图像渲染过程

    相关文章

      网友评论

          本文标题:iOS绘制与渲染--渲染流程

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