阅读视图

发现新文章,点击刷新页面。

离屏渲染(二)

离屏渲染是在当前屏幕帧缓冲区外增加了一个临时新的缓冲区对将要进行显示的图片进行渲染操作再写回帧缓冲区,过个过程造成性能损耗。

离屏渲染(一)

前言

在iOS中图形图像的渲染流程,在平时开发中最常使用的开发框架是UIKit,我们会使用UIKit的框架来绘制界面,而UIKit实际可以看成是集成CoreAnimationCoreGraphics这两个框架,来方便开发者使用,设置UIKit的一些布局和相关属性来绘制界面。

Pasted Graphic.png

  • 如果界面如果需要动画效果就需要CoreAnimation这个框架来实现,而CoreAnimation又依赖于OpenGL ES/Metal 来做GPU的渲染。
  • CoreGraphics框架是一个高级的绘图引擎,它的主要作用是运行的时候去绘制图像。我们可以使用这个框架来做一些绘图、转换、离屏渲染、阴影、图像的创建等等。
  • CoreGraphics是在CPU是执行的 ,CoreImageCoreGraphics是相反的,CoreImage是处理创建图像运行前的操作,CoreImage拥有一套现成的图像过滤性,它能对一些已经存在的图像进行高效的处理,这个框架既能在CPU上执行,也能在GPU上执行,我们APP就会使用CoreGraphicsCoreAnimationCoreImage来绘制一些可视化的内容,这些框架都需要再通过OpenGL ES/Metal来做GPU的绘制,最终再将图像显示到屏幕上面。

一、图像图像渲染流程

Pasted Graphic 7.png

上面这张图像就告诉我们图形图像在渲染的时候CPUGPU分别做了哪些事情:

  • 首先,在Application阶段,是由CPU来处理,CPU会创建我们的视图,它会计算视图的一些数据,进行编解码,绘制纹理等等的操作后再交给GPUGPU在第一阶段会通过顶点着色器去确定图像在硬件上具体的显示位置;
  • 然后,通过片源着色器来计算每个像素点的颜色值,最后进行光栅化,这个光栅化会找到图形上像素的点的范围,然后把一个一个的像素点的颜色显示上去,最终会把图形转化为一个个的实际的屏幕像素,当这些操作都处理完之后,就会把渲染完成之后的数据放到帧缓存区里面;
  • 最后,再交由显示系统将帧缓存区里的数据给读取出来进行显示 。

二、图像显示流程图

Pasted Graphic 9.png

上面就是图像显示流程图,当一个图像在CPUGPU处理完之后,它就会被存放到FrameBuffer里面,FrameBuffer被称为帧缓存区,然后视频控制器就会往FrameBuffer去读取数据,读取的数据就会交给显示器显示,我们就能看到屏幕上一帧一帧的画面。

三、屏幕扫描

Pasted Graphic 12.png

它是通过屏幕扫描的方式,会通过CRT电子枪从上到下逐行扫描,这个扫描的过程就是读取帧缓存区里的数据,当它扫描完的时候,它就会显示一帧的画面, 当显示完一帧画面后,CRT电子枪又回到原来的位置继续从上到下扫描,这样就会显示下一帧的画面,如此循环往复,这就是我们能看到手机屏幕上的一些内容。

当电子枪扫描完一行要换到下面一行的时候,这个时候显示器会发出水平同步信号HSync;当电子扫描完一帧,要回到原来位置,这个时候显示器就会发出垂直同步信号VSync。这个显示器通常都是按照固定的频率来刷新的,这个刷新的频率就是垂直同步信号VSync的频率。苹果手机(除了新出的高刷)的刷新频率是每秒60次,也就是60FPS,这就是我们在进行屏幕卡顿监测或者卡顿优化的时候,会用FPS来作为衡量的指标。

Pasted Graphic 13.png

如果屏幕刷新频率不是接近固定刷新频率,那就是出现了掉帧。当一个垂直同步信号过来的时候,如果说CPUGPU还没有完成渲染的结果去做提交,也就是没有把数据放到FrameBuffer里面,这种情况,未过来提交过来这一帧的画面就会被丢弃,然后等待下一次垂直同步信号过来,再来显示新的画面,这个过程被称为掉帧。 掉帧的时候,屏幕刷新的FPS频率就会减少,这也是界面显示真正卡顿的原因。

接下来是可能造成屏幕卡顿的离屏渲染.

四、离屏渲染

Pasted Graphic 14.png

比如我们给图像添加了遮罩,设置了某些圆角,CPUGPU没有办法把渲染的数据放到FrameBuffer上,它会把渲染的数据先放到FrameBuffer之外的一块缓冲区,在这块区域进行合成渲染,渲染到我们最终想要的画面,再把数据放到FrameBuffer里面,这个过程就是离屏渲染

Pasted Graphic 15.png

先看UIViewCALayer的关系,UIView是基于CALayer的封装,一个View他本身是不能显示的,它想要显示,需要通过内部图层的CALayer来显示,UIViewCALayer的只读属性和遵循CALayer的代理。当View显示什么内容,都要绘制在它内部图层Layer上面,这个Layer就是负责显示,而View负责处理响应的事件。因此,我们看到的界面是View里面的layer层所呈现出来的。

Pasted Graphic 16.png

下图可以看出,layer主要包括三个部分backgroundColorcontentsborderColor

Pasted Graphic 17.png

  • 当视图层级比较复杂的时候,GPU无法直接把渲染数据放到FrameBuffer。比如下面的图例,第三张图是我们最终要看到的画面,它比较复杂,由于CPUGPU是硬件,它有性能瓶颈,它去读取画面数据需要一定的算法,这个时候由于GPU没有办法通过一次遍历就能拿到一帧的完整数据,这个时候它就会遵循画家算法。
  • 要画下面这张图,GPU它是一个机器,它就一块画布,第一次遍历扫描到图片上的山,这样第一次遍历结束后第二次遍历开始的时候,需要一块新的画布,又扫描出了草地, 以此类推,第三次遍历在新的画布上加上了树。
  • 但是,无论是树还是草地或者树都不是我们最终想要的画面效果,这样就不能把任一画布单独放进FrameBuffer里面,因为画面不够完整,这样就需要开启一块额外的内存缓冲区,然后山、草地和树就在这里面合成,合成最终的效果再把合成的数据存到帧缓存区,这就是离屏渲染的整个流程。

Pasted Graphic 19.png

离屏渲染就是硬件的瓶颈,有的图形,它没有办法做到一次渲染完成,于是通过离屏渲染的方式来处理,这就是离屏渲染产生的原因。

启动优化clang插桩(一)

启动优化clang插桩(一)

一、了解Clang

首先到Clang地址:Clang Documentation Pasted Graphic.pngPCs指的是CPU的寄存器,用来存储将要执行的下一条指令的地址,Tracing PCs就是跟踪CPU将要执行的代码。

二、如何使用

网页下拉有个Example Pasted Graphic 1.png 使用之前要在工程添加标记: Pasted Graphic 2.png

编译器就会在每一行代码的边缘插入这一段函数:__sanitizer_cov_trace_pc_guard(&guard_variable)

打开实例demo,在Build Settings 搜索 Other c Flag 填入 -fsanitize-coverage=trace-pc-guard

1__#$!@%!#__Pasted Graphic 1.png

项目会报未定义符号的错:

Pasted Graphic 7.png

这就需要去定义这两个符号,先把这两个函数复制过来:

Pasted Graphic 5.png 先把代码复制进ViewController

extern "C" void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                                    uint32_t *stop) {
  static uint64_t N;  // Counter for the guards.
  if (start == stop || *start) return;  // Initialize only once.
  printf("INIT: %p %p\n", start, stop);
  for (uint32_t *x = start; x < stop; x++)
    *x = ++N;  // Guards should start from 1.
}
// This callback is inserted by the compiler on every edge in the
// control flow (some optimizations apply).
// Typically, the compiler will emit the code like this:
//    if(*guard)
//      __sanitizer_cov_trace_pc_guard(guard);
// But for large functions it will emit a simple call:
//    __sanitizer_cov_trace_pc_guard(guard);
extern "C" void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  if (!*guard) return;  // Duplicate the guard check.
  // If you set *guard to 0 this code will not be called again for this edge.
  // Now you can get the PC and do whatever you want:
  //   store it somewhere or symbolize it and print right away.
  // The values of `*guard` are as you set them in
  // __sanitizer_cov_trace_pc_guard_init and so you can make them consecutive
  // and use them to dereference an array or a bit vector.
  void *PC = __builtin_return_address(0);
  char PcDescr[1024];
  // This function is a part of the sanitizer run-time.
  // To use it, link with AddressSanitizer or other sanitizer.
  __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
  printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

把头文件也粘贴进来:

#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>

两个方法里面都有extern “C”extern “C”的主要作用是为了能够正确实现C++去调用其他C语言的代码,加上extern “C”就会指示作用域内的代码按照C语言区编译,而不是C++,这个extern “C”在OC项目里没什么用,直接删除

此时还会包一个错误:

Pasted Graphic 8.png

这个__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));函数没有什么作用,直接删除即可。

三、代码调试

cmd + r运行,此时终端会打印一些信息:

Pasted Graphic 9.png

删除两个函数里面的注释,先注释第二个的内容,然后运行

INIT: 0x1025c5478 0x1025c54f0

这是运行打印得到的地址,就是函数(uint32_t *start, uint32_t *stop)startstop两个指针的地址

stop存储的就是我们工程里面符号的个数

for (uint32_t *x = start; x < stop; x++)
        *x = ++N;

看一下这个for循环,start会先复制给*xx++就是内存平移,按照uint32_t的大小去平移,而uint32_t的定义是typedef unsigned int uint32_t; 是无符号整型,占4个字节,所以每次按4个字节平移。

startstop里面存的是什么,打断点调试:

Pasted Graphic 10.png

先看start:

INIT: 0x1042a5278 0x1042a52e0
(lldb) x 0x1042a5278
0x1042a5278: 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00  ................
0x1042a5288: 05 00 00 00 06 00 00 00 07 00 00 00 08 00 00 00  ................
(lldb)

由于uint32_t4个字节来存储发现start就是 0 1 2 3 4…,再看stop,由于stop的已经是结束位置,读取的数据是在startstop之间的数据,所以需要向前平移4个字节得到其真实数据。

(lldb) x (0x1042a52e0-4)
0x1042a52dc: 1a 00 00 00 00 00 00 00 00 00 00 00 fe f1 29 04  ..............).
0x1042a52ec: 01 00 00 00 00 00 00 00 00 00 00 00 90 40 2a 04  .............@*.
(lldb)

可以得到1a 就是26,也可以循环外面打印结果: Pasted Graphic 11.png 可以得到:

TraceDemo[16814:301325] 26

也是26个符号。

四、测试验证方法

可以验证一下,添加一个函数:

void test(void) {
    NSLog(@"%s",__func__);
}

符号变成27

TraceDemo[16911:304537] 27

再添加一个block

void (^block) (void) = ^{
    NSLog(@"%s",__func__);
};

符号变成28

TraceDemo[16933:305465] 28

添加一个数据类型属性:

@property (nonatomic ,assign) int age;

由于系统自动生成getter、setter方法,符号变成30

TraceDemo[16975:306816] 30

添加一个对象属性:

@property (nonatomic ,copy) NSString *str;

符号变成33

TraceDemo[17041:308780] 33

对象属性由于ARC,系统自动除了生成getter、setter方法外还生成了cxx_destruct()析构函数

添加一个方法:

- (void)test{
}

符号变成34

TraceDemo[17114:311256] 34

在其他类AppDelegate类中添加一个属性:

@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (nonatomic, strong) NSString *name;
@end

符号变成37:

TraceDemo[17266:316294] 37

符号变成37

结论

这就说明了通过这个方法整个项目里的符号,它都能捕获到。

❌