RunLoop总结:RunLoop的应用场景(三)滚动视图流畅性优化

今天要讲的RunLoop的应用场景可能太简单了,因此东西比较少。由于跟UITableView、UICollectionView等的滑动优化有关,就顺便总结一下会影响UITableView、UICollectionView等视图滑动流畅的因素。php

参考资料

好的书籍都是值得反复看的,那好的文章,好的资料也值得咱们反复看。咱们在不一样的阶段来相同的文章或资料或书籍都能有不一样的收获,那它就是好文章,好书籍,好资料。 关于iOS 中的RunLoop资料很是的少,如下资料都是很是好的。html

  • CF框架源码(这是一份很重要的源码,能够看到CF框架的每一次迭代,咱们能够下载最新的版原本分析,或与如下文章对比学习。目前最新的是CF-1153.18.tar.gz)
  • RunLoop官方文档(学习iOS的任何技术,官方文档都是入门或深刻的极好手册;咱们也能够在Xcode--->Help--->Docementation and API Reference --->搜索RunLoop---> Guides(59)--->《Threading Programming Guide:Run Loops》这篇便是)
  • 深刻理解RunLoop(不要看到右边滚动条很长,其实文章占篇幅2/5左右,下面有不少的评论,可见这篇文章的火热)
  • RunLoop我的小结 (这是一篇总结的很通俗容易理解的文章)
  • sunnyxx线下分享RunLoop(这是一份关于线下分享与讨论RunLoop的视频,备用地址:pan.baidu.com/s/1pLm4Vf9)
  • iPhonedevwiki中的CFRunLoop(commonModes中其实包含了三种Mode,咱们一般知道两种,还有一种是啥,你知道么?)
  • 维基百科中的Event loop(能够看看这篇文章了解一下事件循环)

应用场景

让UITableView、UICollectionView等延迟加载图片。下面就拿UITableView来举例说明: UITableView 的 cell 上显示网络图片,通常须要两步,第一步下载网络图片;第二步,将网络图片设置到UIImageView上。git

为了避免影响滑动,第一步,咱们通常都是放在子线程中来作,这个不作赘述。github

第二步,通常是回到主线程去设置。有了前两篇文章关于Mode的切换,想必你已经知道怎么作了。 就是在为图片视图设置图片时,在主线程设置,并调用performSelector:withObject:afterDelay:inModes:方法。最后一个参数,仅设置一个NSDefaultRunLoopMode算法

UIImage *downloadedImage = ....;
[self.myImageView performSelector:@selector(setImage:) withObject:downloadedImage afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
复制代码

固然,即便是读取沙盒或者bundle内的图片,咱们也能够运用这一点来改善视图的滑动。可是若是UITableView上的图片都是默认图,彷佛也不是很好,你须要本身来权衡了。数据库

有一个很是好的关于设置图片视图的图片,在RunLoop切换Mode时优化的例子:RunLoopWorkDistribution 先看一下界面布局:数组

1002.png

一个Cell里有两个Label,和三个imageView,这里的图片是很是高清的(2034 × 1525),一个界面最多有18张图片。为了表现出卡顿的效果,我先本身实现了一下Cell,主要示例代码:缓存

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *identifier = @"cellId";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
    }
    for (NSInteger i = 1; i <= 5; i++) {
        [[cell.contentView viewWithTag:i] removeFromSuperview];
    }
    
    UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(5, 5, 300, 25)];
    label.backgroundColor = [UIColor clearColor];
    label.textColor = [UIColor redColor];
    label.text = [NSString stringWithFormat:@"%zd - Drawing index is top priority", indexPath.row];
    label.font = [UIFont boldSystemFontOfSize:13];
    label.tag = 1;
    [cell.contentView addSubview:label];
    
    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(105, 20, 85, 85)];
    imageView.tag = 2;
    NSString *path = [[NSBundle mainBundle] pathForResource:@"spaceship" ofType:@"jpg"];
    UIImage *image = [UIImage imageWithContentsOfFile:path];
    imageView.contentMode = UIViewContentModeScaleAspectFit;
    imageView.image = image;
    NSLog(@"current:%@",[NSRunLoop currentRunLoop].currentMode);
    [cell.contentView addSubview:imageView];
    
    UIImageView *imageView2 = [[UIImageView alloc] initWithFrame:CGRectMake(200, 20, 85, 85)];
    imageView2.tag = 3;
    UIImage *image2 = [UIImage imageWithContentsOfFile:path];
    imageView2.contentMode = UIViewContentModeScaleAspectFit;
    imageView2.image = image2;
    [cell.contentView addSubview:imageView2];
    
    UILabel *label2 = [[UILabel alloc] initWithFrame:CGRectMake(5, 99, 300, 35)];
    label2.lineBreakMode = NSLineBreakByWordWrapping;
    label2.numberOfLines = 0;
    label2.backgroundColor = [UIColor clearColor];
    label2.textColor = [UIColor colorWithRed:0 green:100.f/255.f blue:0 alpha:1];
    label2.text = [NSString stringWithFormat:@"%zd - Drawing large image is low priority. Should be distributed into different run loop passes.", indexPath.row];
    label2.font = [UIFont boldSystemFontOfSize:13];
    label2.tag = 4;
    
    UIImageView *imageView3 = [[UIImageView alloc] initWithFrame:CGRectMake(5, 20, 85, 85)];
    imageView3.tag = 5;
    UIImage *image3 = [UIImage imageWithContentsOfFile:path];
    imageView3.contentMode = UIViewContentModeScaleAspectFit;
    imageView3.image = image3;
    [cell.contentView addSubview:label2];
    [cell.contentView addSubview:imageView3];

    return cell;
}
复制代码

而后在滑动的时候,顺便打印出当前的runloopMode,打印结果是:性能优化

2016-12-08 10:34:31.450 TestDemo[3202:1791817] current:UITrackingRunLoopMode
2016-12-08 10:34:31.701 TestDemo[3202:1791817] current:UITrackingRunLoopMode
2016-12-08 10:34:32.184 TestDemo[3202:1791817] current:UITrackingRunLoopMode
2016-12-08 10:34:36.317 TestDemo[3202:1791817] current:UITrackingRunLoopMode
2016-12-08 10:34:36.601 TestDemo[3202:1791817] current:UITrackingRunLoopMode
2016-12-08 10:34:37.217 TestDemo[3202:1791817] current:UITrackingRunLoopMode
复制代码

能够看出,为imageView设置image,是在UITrackingRunLoopMode中进行的,若是图片很大,图片解压缩和渲染确定会很耗时,那么卡顿就是必然的。bash

查看实时帧率,咱们能够在Xcode 中选择真机调试,而后 Product -->Profile-->Core Animation

而后点击开始监测便可:

下面就是帧率:

这里就可使用先使用上面的方式作一次改进。

[imageView performSelector:@selector(setImage:) withObject:image afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
复制代码

能够保证在滑动起来顺畅,但是停下来以后,渲染还未完成时,继续滑动就会变的卡顿。 在切换到NSDefaultRunLoopMode中,一个runloop循环要解压和渲染18张大图,耗时确定超过50ms(1/60s)。

咱们能够继续来优化,一次runloop循环,仅渲染一张大图片,分18次来渲染,这样每一次runloop耗时就比较短了,滑动起来就会很是顺畅。这也是RunLoopWorkDistribution中的作法。

简单描述一下这种作法: 首先建立一个单例,单例中定义了几个数组,用来存要在runloop循环中执行的任务,而后为主线程的runloop添加一个CFRunLoopObserver,当主线程在NSDefaultRunLoopMode中执行完任务,即将睡眠前,执行一个单例中保存的一次图片渲染任务。关键代码看 DWURunLoopWorkDistribution类便可。

一点UITableView滑动性能优化扩展

影响UITableView的滑动,有哪些因素呢?

关于这一点,人眼能识别的帧率是60左右,这也就是为何,电脑屏幕的最佳帧率是60Hz。 屏幕一秒钟会刷新60次(屏幕在一秒钟会从新渲染60次),那么每次刷新界面之间的处理时间,就是1/60,也就是1/60秒。也就是说,全部会致使计算、渲染耗时的操做都会影响UITableView的流畅。下面举例说明:

1.在主线程中作耗时操做

耗时操做,包括从网络下载、从网络加载、从本地数据库读取数据、从本地文件中读取大量数据、往本地文件中写入数据等。(这一点,相信你们都知道,要尽可能避免在主线程中执行,通常都是建立一个子线程来执行,而后再回到主线程)

2.动态计算UITableViewCell的高度,时间太久

在iOS7以前,每个Cell的高度,只会计算一次,后面再次滑到这个Cell这里,都会读取缓存的高度,也即高度计算的代理方法不会再执行。可是到了iOS8,不会再缓存Cell的高度了,也就是说每次滑到某个Cell,代理方法都会执行一次,从新计算这个Cell的高度(iOS 9之后没测试过)。 因此,若是计算Cell高度的这个过程过于复杂,或者某个计算使用的算法耗时很长,可能会致使计算时间大于1/60,那么必然致使界面的卡顿,或不流畅。

关于这一点,我之前的作法是在Cell中定义一个public方法,用来计算Cell高度,而后计算完高度后,将高度存储在Cell对应的Model中(Model里定义一个属性来存高度),而后在渲染Cell时,咱们依然须要动态计算各个子视图的高度。(多是没用什么太过复杂的计算或算法,时间都很短滑动也顺畅)

其实,更优的作法是:再定义一个ModelFrame对象,在子线程请求服务器接口返回后,转换为对象的同时,也把各个子视图的frame计算好,存在ModelFrame中,ModelFrame 和 Model 合并成一个Model存储到数组中。这样在为Cell各个子控件赋值时,仅仅是取值、赋值,在计算Cell高度时,也仅仅是加法运算。

3.界面中背景色透明的视图过多

为何界面中背景色透明的视图过多会影响UITableView的流畅?

不少文章中都提到,可使用模拟器--->Debug--->Color Blended Layers来检测透明背景色,把透明背景色改成与父视图背景色同样的颜色,这样来提升渲染速度。

简单说明一下,就是屏幕上显示的全部东西,都是经过一个个像素点呈现出来的。而每个像素点都是经过三原色(红、绿、蓝)组合呈现出不一样的颜色,最终才是咱们看到的手机屏幕上的内容。在 iPhone5 的液晶显示器上有1,136×640=727,040个像素,所以有2,181,120个颜色单元。在15寸视网膜屏的 MacBook Pro 上,这一数字达到15.5百万以上。全部的图形堆栈一块儿工做以确保每次正确的显示。当你滚动整个屏幕的时候,数以百万计的颜色单元必须以每秒60次的速度刷新,这是一个很大的工做量。

每个像素点的颜色计算是这样的:

R = S + D * (1 - Sa)

结果的颜色 是子视图这个像素点的颜色 + 父视图这个像素点的颜色 * (1 - 子视图的透明度) 固然,若是有两个兄弟视图叠加,那么上面的中文解释可能并不贴切,只是为了更容易理解。

若是两个兄弟视图重合,计算的是重合区域的像素点: 结果的颜色 是 上面的视图这个像素点的颜色 + 下面这个视图该像素点的颜色 * (1 - 上面视图的透明度)

只有当透明度为1时,上面的公式变为R = S,就简单的多了。不然的话,就很是复杂了。

每个像素点是由三原色组成,例如父视图的颜色和透明度是(Pr,Pg,Pb,Pa),子视图的颜色颜色和透明度是(Sr,Sg,Sb,Sa),那么咱们计算这个重合区域某像素点的颜色,须要先分别计算出红、绿、蓝。

Rr = Sr + Pr * (1 - Sa),

Rg = Sg + Pg * (1 - Sa),

Rb = Sb + Pb * (1 - Sa)。

若是父视图的透明度,即Pa = 1,那么这个像素的颜色就是(Rr,Rg,Rb)。

可是,若是父视图的透明Pa 不等 1,那么咱们须要将这个结果颜色当作一个总体做为子视图的颜色,再去与父视图组合计算颜色,如此递推。

因此设置不透明时,能够为GPU节省大量的工做,减小大量的消耗。

更加详细的说明,能够看绘制像素到屏幕上这篇文章,这是一篇关于绘制像素的很是棒👍的文章,我反复看了三遍。

4.主线程RunLoop切换到UITrackingRunLoopMode时,视图有过多的修改

这也就是上面介绍的RunLoop的使用,避免在主线程RunLoop切换到UITrackingRunLoopMode时,修改视图。

Have Fun!

相关文章
相关标签/搜索