iOS视图成像原理与卡顿优化

CTR屏幕成像
视图成像原理.png

CRT(阴极射线管)显示器电子枪,电子枪从屏幕的左上角的第一行开始,从左至右逐行扫描,第一行扫描完后再从第二行的最左端开始至第二行的最右端,一直到扫描完整个屏幕后再从屏幕的左上角开始,这时就完成了一次对屏幕的刷新。

CRT 的电子枪按照上面方式,从上到下一行行扫描,扫描完成后显示器就呈现一帧画面,随后电子枪回到初始位置继续下一次扫描。为了把显示器的显示过程和系统的视频控制器进行同步,显示器(或者其他硬件)会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行,准备进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称 HSync;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization),简称 VSync。显示器通常以固定频率进行刷新,这个刷新率就是 VSync 信号产生的频率。


垂直信号和水平.png
图像显示原理.png

图像显示过程如图,首先通过CPU的计算,绘制成位图(bitmap)交给GPU,GPU拿到位图后会进行相应的图层渲染(顶点变换、纹理混合等),之后把渲染的结果放如帧缓冲区(Frame Buffer)。视频控制器在指定时间提取帧缓冲区的内容,最终显示在了屏幕上。

  • CPU:计算视图frame、图片解码、绘制成位图交给GPU
  • GPU:顶点变换、纹理混合、渲染到帧缓冲区
  • 时钟信号:
    1.水平同步信号(HSync) 准备扫描下一行时发出
    2.垂直同步信号(VSync) 准备画下一帧时发出
  • CRT:阴极电子枪发射电子,在阴极高电压的作用下,电子由电子枪射向荧光屏,使荧光粉发光,将图像显示在屏幕上。采用时钟信号控制。
  • LCD:(光学成像原理)在不加电压的情况下,光线会沿着液晶分子的间隙前进旋转90°,光可以通过。在 加入电压后,光沿着液晶分子的间隙直线前进,被滤光板挡住。
  • 注:LCD的成像原理与CRT截然不同,每一个像素的颜色在需要改变时才去改变电压,但仍需要按照一定的刷新频率向GPU获取新的图像用于显示。
屏幕撕裂原因分析
屏幕撕裂.png
  • 原因分析:当视频还未把当前帧缓冲区读取完成时,GPU新的一帧内容渲染完成提交到帧缓冲区,并把两个缓冲区交换,这时视图控制器从新的帧数据中读取了下一半部分的数据,显示到屏幕上就有可能造成画面撕裂。
  • 解决办法:垂直同步技术。GPU等到垂直同步信号(VSync)发出之后,才开始下一帧内容的渲染和交换缓冲区。
  • 弊端:虽然解决了画面撕裂问题,也增加了画面流畅度,但是需要消耗更多的计算资源,也可能会带来延迟。现在iOS设备会始终使用双缓冲加垂直同步技术。
UI卡顿、掉帧分析

通常来说当fps大于60,我们是不会感到卡顿的。因此每 1/60 S(16.7ms)内完成一帧画面并提交,是不会卡顿的。也就是说在这16.7ms内,CPU和GPU要协同完成一帧的数据,比如CPU花了一定的时间做UI布局、文本计算、视图的绘制和图片解码,并把产生的位图提交给GPU,GPU又要花一定的时间进行纹理混合渲染,然后在下一帧的VSync垂直信号到来之前显示这一帧画面。


卡顿掉帧原因.png

如上图所示,如果CPU花费了太多的时间做UI布局、视图绘制和图片解码,那么留给GPU的时间就不多了。等GPU完成纹理混合渲染,时间可能就超过16.7ms了,这时VSync信号已经发出,新的一帧数据还没有渲染完成,视频控制器还会显示上一帧的数据,就出现了掉帧。同理,如果GPU花费了太多的时间,也可能造成掉帧。

卡顿优化

尽可能减少CPU、GPU的资源消耗。

卡顿优化CPU

1.尽量用轻量级的对象,比如用不到事件处理的地方,可以考虑使用CALayer取代UIView,能用int就不用NSNumber。

2.不要频繁地调用UIView的相关属性,比如frame、bounds、transform等属性,尽量减少不必要的修改,因为每次修改都要重新计算和渲染,消耗性能比较多。

3.尽量提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性,因为多次修改也会重新计算和渲染。

4.Autolayout会比直接设置frame消耗更多的CPU资源,因为Autolayout本身性能就不是很高。

5.图片的size最好刚好跟UIImageView的size保持一致,如果不一致CPU就会对图片进行伸缩操作,这样比较消耗CPU资源。

6.控制一下线程的最大并发数量,不要无限制的并发,这样会让CPU很忙。

7.尽量把耗时的操作放到子线程,这样可以充分利用CPU的多核,这样CPU的资源消耗分担的也比较合理。

那么哪些操作比较耗时呢?

  • 文本处理(尺寸计算、绘制)
    比如:boundingRectWithSize计算文字宽高是可以放到子线程去计算的,或者drawWithRect文本绘制,也是可以放到子线程去绘制的,如下:
- (void)text
{
    //下面操作都可以放到子线程
    // 文字计算
    [@"text" boundingRectWithSize:CGSizeMake(100, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:nil context:nil];
    
    // 文字绘制
    [@"text" drawWithRect:CGRectMake(0, 0, 100, 100) options:NSStringDrawingUsesLineFragmentOrigin attributes:nil context:nil];
}
  • 图片处理(解码、绘制)
    我们经常会写如下代码加载图片:
- (void)image
{
    UIImageView *imageView = [[UIImageView alloc] init];
    imageView.frame = CGRectMake(100, 100, 100, 56);
    imageView.image = [UIImage imageNamed:@"timg"]; //加载图片
    [self.view addSubview:imageView];
    self.imageView = imageView;
}

其实通过imageNamed加载图片,加载完成后是不会直接显示到屏幕上面的,因为加载后的是经过压缩的图片二进制,当真正想要渲染到屏幕上的时候再拿到图片二进制解码成屏幕显示所需要的那种格式,然后渲染显示,而这种解码一般默认是在主线程操作的,如果图片数据比较多比较大的话也会产生卡顿。一般我们的做法是在子线程提前解码图片二进制,主线程就不需要解码,这样在图片渲染显示之前就已经解码出来了,主线程拿到解码后的数据进行渲染显示就可以了,这样主线程就不会卡顿了。其实网上好多图片处理框架都有这个异步解码功能的。下面演示一下:

- (void)image
{
    UIImageView *imageView = [[UIImageView alloc] init];
    imageView.frame = CGRectMake(100, 100, 100, 56);
    //    imageView.image = [UIImage imageNamed:@"timg"];
    [self.view addSubview:imageView];
    self.imageView = imageView;
    
    //异步图片解码
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 获取CGImage
        CGImageRef cgImage = [UIImage imageNamed:@"timg"].CGImage;
        // 获取网络图片
        // CGImageRef cgImage = [UIImage imageWithContentsOfFile:@"www.baidu.com"].CGImage;        

        // alphaInfo
        CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage) & kCGBitmapAlphaInfoMask;
        BOOL hasAlpha = NO;
        if (alphaInfo == kCGImageAlphaPremultipliedLast ||
            alphaInfo == kCGImageAlphaPremultipliedFirst ||
            alphaInfo == kCGImageAlphaLast ||
            alphaInfo == kCGImageAlphaFirst) {
            hasAlpha = YES;
        }
        
        // bitmapInfo
        CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
        bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
        
        // size
        size_t width = CGImageGetWidth(cgImage);
        size_t height = CGImageGetHeight(cgImage);
        
        // context
        CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, CGColorSpaceCreateDeviceRGB(), bitmapInfo);
        
        // draw
        CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
        
        // get CGImage
        cgImage = CGBitmapContextCreateImage(context);
        
        // into UIImage
        UIImage *newImage = [UIImage imageWithCGImage:cgImage];
        
        // release
        CGContextRelease(context);
        CGImageRelease(cgImage);
        
        // back to the main thread
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = newImage;
        });
    });
}

上面代码,不单单通过imageNamed加载的本地图片可以提前渲染,通过imageWithContentsOfFile加载的网络图片也可以这样进行提前渲染,只要获取到UIImage对象都可以对UIImage对象进行提前渲染。

卡顿优化 GPU

1.尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示,这样只渲染一张图片,渲染更快。

2.GPU能处理的最大纹理尺寸是4096x4096,一旦超过这个尺寸,就会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸。

3.尽量减少视图数量和层级,视图层级太多会增加渲染时间。

4.减少透明的视图(alpha<1),不透明的就设置opaque为YES,因为一旦有透明的视图就会进行很多混合计算增加渲染的资源消耗。

5.尽量避免出现离屏渲染。

参考
iOS视图成像理论及性能优化
iOS-性能优化-卡顿优化

你可能感兴趣的:(iOS视图成像原理与卡顿优化)