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-性能优化-卡顿优化

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,539评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,594评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,871评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,963评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,984评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,763评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,468评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,850评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,002评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,144评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,823评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,483评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,026评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,150评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,415评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,092评论 2 355