iOS 底层原理

iOS底层原理 - 性能优化 之 卡顿优化

2020-03-20  本文已影响0人  hazydream

面试题引发的思考:

Q: 列表卡顿的原因?如何优化?

Q: 卡顿解决方法?

Q: 如何检测卡顿?

Q: 何为离屏渲染?

Q: 离屏渲染触发时机?

Q: 为什么离屏渲染消耗性能?为何要避免离屏渲染?

Q: 哪些操作会触发离屏渲染?


1. 屏幕成像原理

(1) CPU和GPU的作用

在屏幕成像过程中,CPU和GPU起着至关重要的作用:

  • CPU (Central Processing Unit,中央处理器):
    作用:对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics);
  • GPU (Graphics Processing Unit,图形处理器):
    作用:纹理的渲染。

(2) 屏幕成像流程

屏幕成像流程如下:

屏幕成像流程
  • CPU进行UI布局、文本计算、图片解码等等,计算完毕将数据提交给GPU
  • GPU对数据进行渲染,渲染完毕将数据放到 帧缓存 里面;
  • 视频控制器帧缓存 读取数据;并将数据显示到 屏幕 上。

在iOS中是双缓冲机制,有前帧缓存、后帧缓存。上图的帧缓存有两块区域,当一块区域满了或一块区域正在进行其他操作,此时GPU会使用另外一块区域缓存,提升效率。


(3) 屏幕成像原理

屏幕成像原理如下:

屏幕成像原理

手机屏幕上的动画是通过一帧一帧(或者说一页)数据组成的。

  • 当屏幕需要显示一帧数据的时候,会发送一个垂直同步信号;然后逐行发送水平同步信号,直到填充整个屏幕,此时一帧数据就显示完成。
  • 接下来再发送一个垂直同步信号;然后逐行发送水平同步信号,直到完成这一帧。

2. 卡顿产生原因

屏幕成像流程

如上图,红色箭头是CPU计算需要的时间,蓝色箭头是GPU渲染需要的时间。

注意:发送一个垂直同步信号,就会立即把CPU计算和GPU渲染完成的数据从帧缓存中读取显示到屏幕上,并且立即开始下一帧的操作。


3. 卡顿解决方法

卡顿解决方法是尽可能减少CPU、GPU资源消耗。

  • 一般帧率FPS(Frames Per Second每秒传输帧数)达到60FPS就会感觉不到卡顿;
  • 按照60FPS的刷帧率,每隔16ms就会有一次VSync信号(1000ms / 60 = 16.667ms)。

(1) 卡顿优化 CPU

1> CPU卡顿优化方式

2> 耗时操作:文本处理(尺寸计算、绘制)

比如: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];
}

3> 耗时操作:图片处理(解码、绘制)

我们经常会写如下代码加载图片:

- (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加载图片,加载完成后是不会直接显示到屏幕上面的,因为加载后的是经过压缩的图片二进制,当真正想要渲染到屏幕上的时候再拿到图片二进制解码成屏幕显示所需要的格式,然后进行渲染显示,而这种解码一般默认是在主线程操作的,如果图片数据比较多比较大的话也会产生卡顿。

一般的做法是在子线程提前解码图片二进制,不需要主线程解码,这样在图片渲染显示之前就已经解码出来了,主线程拿到解码后的数据进行渲染显示就可以了,这样主线程就不会卡顿。网上很多图片处理框架都有这个异步解码功能的。下面演示一下:

图片异步解码

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


(2) 卡顿优化 GPU

1> GPU卡顿优化方式

2> 离屏渲染

在OpenGL中,GPU有2种渲染方式:

当前用于显示的屏幕缓冲区就是下图的帧缓存:

屏幕成像流程

Q: 离屏渲染触发时机?

Q: 为什么离屏渲染消耗性能?为何要避免离屏渲染

Q: 哪些操作会触发离屏渲染?

Q: 为什么要开辟新的缓冲区?

因为上面进行的那些操作比较耗性能、资源,当前屏幕缓冲区不够用(就算是双缓冲机制也不够用),所以才会开辟新的缓冲区。


(3) 卡顿检测

“卡顿”主要是因为在主线程执行了比较耗时的操作;

通过添加Observer到主线程RunLoop中,监听RunLoop状态切换的耗时,以达到监控卡顿的目的。

如下图,主线程的大部分操作,比如点击事件的处理,view的计算、 绘制等基本上都在source0source1。我们只要监控一下从结束休眠(步骤08)处理soure1(步骤09-C)一直到绕回来处理source0(步骤05), 如果发现中间消耗的时间比较长,那么就可以证明这些操作比较耗时。

RunLoop运行逻辑

借助可以监控哪个方法卡顿的第三方库<LXDAppFluecyMonitor>,进行检测:

// TODO: ----------------- ViewController类 -----------------
- (void)viewDidLoad {
    [super viewDidLoad];
    //开启卡顿检测
    [[LXDAppFluecyMonitor sharedMonitor] startMonitoring];
    [self.tableView registerClass: [UITableViewCell class] forCellReuseIdentifier: @"cell"];
}

#pragma mark -
#pragma mark - UITableViewDataSource
- (NSInteger)tableView: (UITableView *)tableView numberOfRowsInSection: (NSInteger)section {
    return 1000;
}

- (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
    UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier: @"cell"];
    cell.textLabel.text = [NSString stringWithFormat: @"%lu", indexPath.row];

    if (indexPath.row > 0 && indexPath.row % 30 == 0) {
        // 模拟卡顿
        sleep(2.0);
    }
    return cell;
}

运行以上代码,打印结果如下:

打印结果

由打印结果可知:可以检测到cellForRowAtIndexPath的卡顿。

下面简单介绍下LXDAppFluecyMonitor框架的核心代码:

LXDAppFluecyMonitor框架里面就两个文件:
LXDBacktraceLogger文件里面是关于方法调用栈的一些代码;
LXDAppFluecyMonitor文件就是卡顿检测文件。

进入LXDAppFluecyMonitor文件的startMonitoring方法:

startMonitoring方法
上一篇下一篇

猜你喜欢

热点阅读