[Desarrollo de iOS] Optimización del rendimiento de UITableView

algunas optimizaciones básicas

(1) CPU

1. Utiliza objetos ligeros

Por ejemplo, donde no se usa el procesamiento de eventos, considere usar CALayer en lugar de UIView

CALayer * imageLayer = [CALayer layer];
imageLayer.bounds = CGRectMake(0,0,200,100);
imageLayer.position = CGPointMake(200,200);
imageLayer.contents = (id)[UIImage imageNamed:@"xx.jpg"].CGImage;
imageLayer.contentsGravity = kCAGravityResizeAspect;
[tableCell.contentView.layer addSublayer:imageLayer];
复制代码

2. No llame a las propiedades relacionadas con UIView con frecuencia

Por ejemplo frame, bounds, transformy otros atributos, intente reducir las modificaciones innecesarias y no UITableViewCelllas agregue dinámicamente subViewPuede UITableViewCellagregar todos los atributos que deben mostrarse durante la inicialización y luego configurar hiddenlos atributos para mostrarlos y ocultarlos según sea necesario.

3. Calcule el diseño por adelantado

Al deslizar, se llamará continuamente heightForRowAtIndexPath:Cuando la altura de la Celda deba ser adaptable, la altura debe calcularse cada vez que se realice la devolución de llamada, lo que hará que la interfaz de usuario se congele. Para evitar cálculos repetidos sin sentido, se requiere la altura de caché. UITableViewCellHay dos tipos principales de cálculos de altura, uno es altura fija y el otro es altura dinámica. Altura fija: rowHeightla altura predeterminada es 44. Es más eficiente usar la altura fija directamente self.tableView.rowHeight = 77que tableView:heightForRowAtIndexPath:la altura dinámica: si tableView:heightForRowAtIndexPath:se usa este método de proxy, rowHeightno será válido después de configurar este proxy. Se deben cumplir las siguientes tres condiciones

  • Use el diseño automático para las restricciones de diseño de la interfaz de usuario (los cell.contentViewcuatro lados requeridos están restringidos a elementos internos)
  • Especifique estimatedRowHeightel valor predeterminado de la propiedad de TableView
  • Especifique rowHeightlas propiedades de TableView comoUITableViewAutomaticDimension
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 44;
复制代码

Además de mejorar cellla eficiencia del cálculo de la altura, necesitamos almacenar en caché la altura calculada

4. Establecer el marco directamente

El diseño automático frameconsumirá más recursos de CPU que la configuración directa

5. El tamaño de la imagen es adecuado

图片的 size 最好刚好跟 UIImageViewsize 保持一致 图片通过contentMode处理显示,对tableview滚动速度同样会造成影响 从网络下载图片后先根据需要显示的图片大小切/压缩成合适大小的图,每次只显示处理过大小的图片,当查看大图时在显示大图。 服务器直接返回预处理好的小图和大图以及对应的尺寸最好

/// 根据特定的区域对图片进行裁剪
+ (UIImage*)kj_cutImageWithImage:(UIImage*)image Frame:(CGRect)cropRect{
    return ({
        CGImageRef tmp = CGImageCreateWithImageInRect([image CGImage], cropRect);
        UIImage *newImage = [UIImage imageWithCGImage:tmp scale:image.scale orientation:image.imageOrientation];
        CGImageRelease(tmp);
        newImage;
    });
}
复制代码

6. 控制最大并发数量

控制一下线程的最大并发数量,当下载线程数超过2时,会显著影响主线程的性能。可以用一个NSOperationQueue来维护下载请求,并设置其最大线程数maxConcurrentOperationCount。 当然在不需要响应用户请求时,也可以增加下载线程数来加快下载速度:

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
    if (!decelerate) self.queue.maxConcurrentOperationCount = 5;
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
    self.queue.maxConcurrentOperationCount = 5;
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{
    self.queue.maxConcurrentOperationCount = 2;
}
复制代码

7. 子线程处理

尽量把耗时的操作放到子线程

  • 文本处理(尺寸计算、绘制)
  • 图片处理(解码、绘制)

8. 异步绘制

异步绘制,就是异步在画布上绘制内容,将复杂的绘制过程放到后台线程中执行,然后在主线程显示。

// 异步绘制,切换至子线程
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    UIGraphicsBeginImageContextWithOptions(size, NO, scale);
    CGContextRef context = UIGraphicsGetCurrentContext();
    // TODO:draw in context...
    CGImageRef imgRef = CGBitmapContextCreateImage(context);
    UIGraphicsEndImageContext();
    dispatch_async(dispatch_get_main_queue(), ^{
        self.layer.contents = imgRef;
    });
});
复制代码

请添加图片描述

(二)GPU

1. 避免短时间内大量显示图片

尽可能将多张图片合成一张进行显示。

2. 控制尺寸

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

3. 减少图层混合操作

当多个视图叠加,放在上面的视图是半透明的,那么这个时候GPU就要进行混合,把透明的颜色加上放在下面的视图的颜色混合之后得出一个颜色再显示在屏幕上,这一步是消耗GPU资源

  • UIViewbackgroundColor不要设置为clearColor,最好设置和superViewbackgroundColor颜色一样
  • 图片避免使用带alpha通道的图片

4. 透明处理

减少透明的视图,不透明的就设置opaque = YES

5. 避免离屏渲染

离屏渲染就是在当前屏幕缓冲区以外,新开辟一个缓冲区进行操作。离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕切换到离屏;等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕。

(1)下面的情况或操作会引发离屏渲染

  • 光栅化,layer.shouldRasterize = YES
  • 遮罩,layer.mask
  • 圆角,同时设置 layer.masksToBounds = YESlayer.cornerRadius > 0
  • 阴影,layer.shadow
  • layer.allowsGroupOpacity = YESlayer.opacity != 1
  • 重写drawRect方法

(2)圆角优化

这里主要其实就是解决同时设置layer.masksToBounds = YESlayer.cornerRadius > 0就会产生的离屏渲染。其实我们在使用常规视图切圆角时,可以只使用view.layer.cornerRadius = 3.0,这时是不会产生离屏渲染。但是UIImageView有点特殊,切圆角时必须上面2句同时设置,则会产生离屏渲染,所以我们可以考虑通过 CoreGraphics 绘制裁剪圆角,或者叫美工提供圆角图片。

- (UIImage *)billy_ellipseImage {
    UIGraphicsBeginImageContextWithOptions(self.size, NO, 0.0);
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
    CGContextAddEllipseInRect(ctx, rect);
    CGContextClip(ctx);
    [self drawInRect:rect];
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}
复制代码

此外,还可以通过贝塞尔曲线画圆角:

- (void)clipCornerWithImageView:(UIImageView *)originView
                     andTopLeft:(BOOL)topLeft
                    andTopRight:(BOOL)topRight
                  andBottomLeft:(BOOL)bottomLeft
                 andBottomRight:(BOOL)bottomRight
                   cornerRadius:(CGFloat)radius
{
    CGRect rect = originView.bounds;
    UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:radius];
    // 创建遮罩层
    CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
    maskLayer.frame = rect;
    maskLayer.path = maskPath.CGPath;   // 轨迹
    originView.layer.mask = maskLayer;
}

- (void)clipCornerWithImageView:(UIImageView *)originView
                    cornerRadius:(CGFloat)radius {
    CGRect rect = originView.bounds;
    UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(radius, radius)];
    // 创建遮罩层
    CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
    maskLayer.frame = rect;
    maskLayer.path = maskPath.CGPath;   // 轨迹
    originView.layer.mask = maskLayer;
}
复制代码

这样还可以控制特定角是否设置圆角。这种情况有个弊端,就是切割角度有限,所以实现大角度圆角只能采取自己画线的方式来操作。

(3)阴影优化

对于shadow,如果图层是个简单的几何图形或者圆角图形,我们可以通过设置shadowPath来优化性能,能大幅提高性能。

imageView.layer.shadowColor = [UIColor grayColor].CGColor;
imageView.layer.shadowOpacity = 1.0;
imageView.layer.shadowRadius = 2.0;
UIBezierPath *path = [UIBezierPath bezierPathWithRect:imageView.frame];
imageView.layer.shadowPath = path.CGPath;
复制代码

(4)强制开启光栅化

当图像混合了多个图层,每次移动时,每一帧都要重新合成这些图层,十分消耗性能,这时就可以选择强制开启光栅化layer.shouldRasterize = YES。 当我们开启光栅化后,会在首次产生一个位图缓存,当再次使用时候就会复用这个缓存,但是如果图层发生改变的时候就会重新产生位图缓存。 所以这个功能一般不能用于UITableViewCell中,复用反而降低了性能。最好用于图层较多的静态内容的图形。

(5)优化建议

  • 使用中间透明图片蒙上去达到圆角效果
  • 使用ShadowPath指定layer阴影效果路径
  • 使用异步进行layer渲染
  • 将UITableViewCell及其子视图的opaque属性设为YES,减少复杂图层合成
  • 尽量使用不包含透明alpha通道的图片资源
  • 尽量设置layer的大小值为整形值
  • 背景色的alpha值应该为1,例如不要使用clearColor
  • 直接让美工把图片切成圆角进行显示,这是效率最高的一种方案
  • 很多情况下用户上传图片进行显示,可以让服务端处理圆角

加载图片的特殊需求

对于没有大型项目经验的我,很难触碰到设备的性能瓶颈,可是未来接触的项目里需要处理的数据会有很多,可能会有各种特殊的需求,比如要求实现:

  1. 要求 tableView 滚动的时候,滚动到哪行,哪行的图片才加载并显示,滚动过程中图片不加载显示;
  2. 页面跳转的时候,取消当前页面的图片加载请求;

先来看看一般的加载逻辑,放一段我之前写过的项目的代码: 请添加图片描述

如上设置,如果我们有20行cell,页面启动的时候,直接滑动到最底部,20个cell都进入过了界面,- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 这个方法就会被调用20次,不符合需求1。 为此,我学习了一下,学习到了两个解决方案,并自己动手实践了一下:

Runloop的小技巧

runloop - 两种常用模式介绍: trackingMode && defaultRunLoopMode

  • 默认情况 - defaultRunLoopMode
  • 滚动时候 - trackingMode

滚动的时候,进入trackingMode,这会导致defaultMode下的任务会被暂停,停止滚动的时候再次进入defaultMode并继续执行defaultMode下的任务。 代码:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    if (!cell) {
        cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
    }
    
    DemoModel *model = self.datas[indexPath.row];
    cell.textLabel.text = model.text;
   
    
    if (model.iconImage) {
        cell.imageView.image = model.iconImage;
    } else {
        cell.imageView.image = [UIImage imageNamed:@"placeholder.png"];
        [self performSelector:@selector(billy_loadImgeWithIndexPath:)
                   withObject:indexPath
                   afterDelay:0.0
                      inModes:@[NSDefaultRunLoopMode]];
    }
    return cell;
}

//下载图片,并渲染到cell上显示
- (void)billy_loadImgeWithIndexPath:(NSIndexPath *)indexPath {
    
    DemoModel *model = self.datas[indexPath.row];
    UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
    
    [ImageDownLoadManager runloop_loadImageWithModel:model success:^{
        //主线程刷新UI
        dispatch_async(dispatch_get_main_queue(), ^{
            cell.imageView.image = model.iconImage;
            //[cell layoutSubviews];
        });
    }];
}
复制代码

其他办法

我们可以手动判断UITableView的状态,保存下载任务,然后决定执行哪些下载任务或者在适当的时机取消这些任务。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    if (!cell) {
        cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
    }
    
    DemoModel *model = self.datas[indexPath.row];
    cell.textLabel.text = model.text;
   
    
    if (model.iconImage) {
        cell.imageView.image = model.iconImage;
    } else {
        cell.imageView.image = [UIImage imageNamed:@"placeholder.png"];

//        [self performSelector:@selector(billy_loadImgeWithIndexPath:)
//                   withObject:indexPath afterDelay:0.0
//                      inModes:@[NSDefaultRunLoopMode]];

        //拖动的时候不显示
        if (!tableView.dragging && !tableView.decelerating) {
            //下载图片数据
            [self billy_loadImgeWithIndexPath:indexPath];
        }
    }
    return cell;
}
复制代码
- (void)billy_loadImgeWithIndexPath:(NSIndexPath *)indexPath {
    
    DemoModel *model = self.datas[indexPath.row];
    //保存当前正在下载的操作
    ImageDownLoadManager *manager = self.imageLoadDic[indexPath];
    if (!manager) {
        
        manager = [[ImageDownLoadManager alloc] init];
        //开始加载-保存到当前下载操作字典中
        [self.imageLoadDic setObject:manager forKey:indexPath];
    }
    
    [manager loadImageWithModel:model success:^{
        //主线程刷新UI
        dispatch_async(dispatch_get_main_queue(), ^{
            UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
            cell.imageView.image = model.iconImage;
            [cell layoutSubviews];
        });
        
        //加载成功-从保存的当前下载操作字典中移除
        [self.imageLoadDic removeObjectForKey:indexPath];
    }];
}
复制代码
- (void)billy_loadImage {
    //拿到界面内-所有的cell的indexpath
    NSArray *visableCellIndexPaths = self.tableView.indexPathsForVisibleRows;
    for (NSIndexPath *indexPath in visableCellIndexPaths) {
        DemoModel *model = self.datas[indexPath.row];
        if (model.iconImage) {
            continue;
        }
        [self billy_loadImgeWithIndexPath:indexPath];
    }
}
复制代码
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    if (!decelerate) {
        //直接停止-无动画
        [self billy_loadImage];
    }
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
    [self billy_loadImage];
}
复制代码

界面消失:

- (void)viewWillDisappear:(BOOL)animated{
    [super viewWillDisappear:animated];
    NSArray *loadImageManagers = [self.imageLoadDic allValues];
    //当前图片下载操作全部取消
    [loadImageManagers makeObjectsPerformSelector:@selector(cancelLoadImage)];
}
复制代码

下面附上我demo的地址:github.com/BillyMiracl…。欢迎大家下载一起学习。

先写这么多吧,学习的道路还长着呢。。。

Supongo que te gusta

Origin juejin.im/post/7144593474916974628
Recomendado
Clasificación