从视图的创建到界面的优化

前端的开发,是离不开页面的,那我们就需要了解,从最基本的视图创建和绘制过程。
再来看一下,当前优化的点有哪些,并且分析一下开源的几种框架他们是怎么做的优化。


UIView和CLAyer的关系

iOS开发中,内容的显示,是CLayer来做的,CLayer等同于一个纹理,纹理是GPU渲染的重要依据;虽然我们一直操作的都是UIView,他们之间的关系是相互持有,UIView的一个属性就是他内部的CLayer,而CLayer也通过delegate来持有UIView,设置frame/color这些都是在UIView内部直接调用了layer.frame...,其结论就是,UIView生成视图树,我们操纵的是视图树,但是图层树也会同步添加或者删除。

之前看过一篇文章,写为什么要设计成这样;如果把设计的权交给问这个问题的人,那么整个框架肯定变成了UILayer这种形式,既能接受事件、又能对外展示。但是随着时间的推移,功能的添加和风格的切换,我们会不断的在这个UILayer类中添加和修改,很快整个类就无法维护和继续了;所以在认定展示是一个基本属性后,作为根本,不需要太多的改变,各自负责的功能也要遵守单一原则。文章地址


视图的创建、修改到显示的过程

首先,由 app 处理事件(Handle Events),如:用户的点击操作,在此过程中 app 可能需要更新 视图树,相应地,图层树 也会被更新。

其次,app 通过 CPU 完成对显示内容的计算,如:视图的创建、布局计算、图片解码、文本绘制等。在完成对显示内容的计算之后,app 对图层进行打包,并在下一次 RunLoop 时将其发送至 Render Server,即完成了一次 Commit Transaction 操作。

Render Server 主要执行 Open GL、Core Graphics 相关程序(现在的渲染引擎改成Metal),并调用 GPU

GPU 则在物理层上完成了对图像的渲染。

最终,GPU 通过 Frame Buffer、视频控制器等相关部件,将图像显示在屏幕上。


Core Animation不仅能做动画,还可以进行渲染操作。
Core Graphics 2D的绘图引擎,主要用于运行时绘制图像

commit经过的是以下四个步骤
Lyout/Display/Prepare/Commit

Lyout 布局:设置 layer 的属性,如 frame,background color 等,设置layer的层级信息。layoutSubviews 在这一步调用
Display : 生成位图,这一步会调用drawrect 方法;这一步有两种情况需要区分,我们要显示的layer,有两种方式可以设置显示的位图;
一、系统默认的,会在layer中创建一个backing store来存放生成的位图。当然,系统的调用过程是给我们开了一个口子的,那就是drawRect,我们可以在这里边使用UIKit或者Graphics进行绘制一个要显示的位图。
二、我们可以拦截,自己绘制出位图,直接设置到layer.cntents中。这一步很重要,因为大部分进行异步渲染的框架,都是在这一步做的拦截,进行自己的绘制。

Prepare :准备提交的参数,包括动画的参数;整个动画我们不需要做额外的操作,只需要起始和终止条件,然后交给CA(Core Animation),他会在render server进行计算,计算动画的中间状态,然后重复完整个动画。
这一步还会进行图片的解码操作。

Commit:CA进行数据的提交,递归提交。

在GPU参与以前,都是CPU在进行一系列的工作,所以优化分为CPU约束型(CPU bound) 和 GPU约束型(GPU bound)。

针对上边写的流程,从创建到渲染完成,整个步骤包括commit的四步,render server中的绘制,还有就是到了GPU中的渲染

Lyout:
减少视图的创建;使用轻量级的视图CALyer代替UIView;尽量重用cell;

Display:
可以采用异步绘制的方式,充分利用CPU的多核。
GPU:

  • 减少Blending,并且使用没有Alpha通道的图片;
  • 减少图层的层级;目前很多库,都采用了绘制成一个位图来GPU进行渲染。
  • 尽量避免离屏渲染。

以上只是较大的要注意的地方,很多小的注意点在最后列出。

关于离屏渲染的问题
我们认为的关于CPU的离屏渲染,可能会造成一些额外的开销,但不是真正的离屏渲染,通过Xcode的Color Offscreen_Rendered Yellow,看下是不是变黄色。
离屏渲染发生在GPU上,他的性能消耗是在上下文的切换;通常下边的操作会引发离屏渲染:
1.cornerRadius+clipsToBounds 切圆角,如果两个函数一起出现,就会引发离屏渲染。
只有cornerRadius,表示只需要一个圆角,不需要将外围切掉,不会引发离屏渲染。
优化:可以使用Graphics提前绘制圆角的Texture,然后交个GPU。

  1. shadow 引发离屏渲染,通过通过shadowPath属性可以避免。
    3.group opacity
  2. mask
  3. UIBlurEffect
    优化:不用系统的模糊效果,另外实现CIGaussianBlur
    ....
    具体可以参考即刻

其他框架做的渲染优化
美团的Graver,采用了异步绘制 + 自己实现绘制的方式;
自己实现绘制就是将多个层级的layer,绘制到一个layer上,将多层级,降低为只有一个层级,GPU渲染毫无压力,不用进行分层绘制了、也不会引发离屏渲染
具体方式是通过CoreText进行layer的绘制。
异步绘制:实现layer的displayer的代理方法,然后在这里边进行界面的异步绘制,绘制完回到主线程进行显示。

这地方稍微拓展一下layer的渲染过程,上边说过,分为两种,系统自己的渲染和我们可以介入的方式。


参考图来自其他博文

大部分的异步绘制,都是自己实现了displayerLayer这个代理方法,在这里进行异步绘制,美团的Graver就是拦截了这个函数使用Core Graphics 和 Core Text进行绘制的。

系统自己的渲染流程

自己渲染流程

会判断layer有没有delegate - > 有代理(也就是有UIView)并且实现了drawLayer:inContext 方法,就会进行绘制,这个时候,如果我们自己实现了drawReact ,会在drawLayer:inContext 方法中被直接调用。

VVeboTableViewDemo
这个思路是计算滑动手指松开时的坐标,优先渲染周围的几个。参考自保持页面流畅的技巧

YYKit中文本的异步绘制也是在layer 的display函数中,直接进行了异步绘制。他是自己实现了一个具有异步绘制功能的layer,然后我们使用的时候,可以将UILabel中内部的layer换成这个,然后进行Graphics的异步绘制,他做了一些优化,作者在他的博客中也提到了,控制并发线程的数量、及时取消不需要的绘制任务。

控制线程数量
控制线程数量这个通过他自建的queue 池,实现逻辑就是开辟线程的时候,他是通过串行队列异步开启,就等于有多少个串行队列,就能开辟多少个线程。

及时取消任务
及时取消任务,他的解释就是滑动过快,很多任务其实不需要绘制了,就把不需要绘制的任务取消掉,每个layer都有一个标记,在异步绘制的时候,判断当前自己身上的标记和最新的标记比对,如果不一样,直接返回。(上下滑动过程中、设置字体等会触发重绘)

监控runloop周期进行提交渲染
还有一个就是和Texture一样的就是,监测了runloop的周期,在runloop的waiting和exit阶段才进行绘制提交(我们自己绘制的时候,设置contetnts,就会触发CA的提交),注意,这地方设置runloop的observer的时候,优先级是比系统的低,系统的commit先提交完,再处理自定义的。

工欲善其事必先利其器
检测工具的使用,我们在开发中,还有在线上,都需要对界面进行调试和监控。具体的监控思想有以下几种。

开发:
Instruments肯定是比较理想的检查工具,Core Animation..
关于离屏渲染、视图的混合都可以直接观察(目前改变颜色这些直接在Xcode的debug工具里边)
线上的时候:

  • 我们卡顿的检测可以使用Runloop进行状态时间的检测,添加observer进行监控。
  • FPS的统计,通过加入帧时间定时器,判断FPS
  • 配合内存的监控,如果内存过高,也将堆栈信息保存下来。
    以上几种最好一起使用,因为FPS获取堆栈的时候,很可能抓取的是卡顿之后函数的堆栈信息。

微信的Matrix的思路
为了解决抓取调用栈不准的问题,微信直接使用了另一种思路,每50毫秒抓取一次,保存最近的20个函数调用栈,发生卡顿后,判断这20个调用栈中出现次数最多的那个调用栈,最耗时的方法就在里边。
这样做会造成百分之3的CPU上升,这个微信团队自己测试过。
退火算法
先判断每次抓取堆栈的hash是不是一样,重复的不上传,只上传一次。

你可能感兴趣的:(从视图的创建到界面的优化)