iOS UITableView性能优化

简介: iOS UITableView性能优化

前言


UITableView是我们经常会使用的控件,那么关于这块的优化还是很有必要,网上关于这块优化的资料很多,其实核心本质还是降低 CPU和GPU 的工作来提升性能


CPU:对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制

GPU:接收提交的纹理和顶点描述、应用变换、混合并渲染、输出到屏幕


卡顿产生原因


在VSync信号到来后,系统图形服务会通过CADisplayLink等机制通知App,App主线程开始在CPU中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后CPU会将计算好的内容提交到GPU去,由GPU进行变换、合成、渲染。GPU会把渲染结果提交到帧缓冲区去,等待下一次VSync信号到来时显示到屏幕。由于垂直同步机制,如果在一个VSync时间内,CPU或者GPU没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变,从而产生界面卡顿现象。


在开发中,CPU和GPU中任何一个压力过大,都会导致掉帧现象。


CPU层面优化


1.用轻量级对象


比如用不到事件处理的地方,可以考虑使用 CALayer 取代 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.不要频繁地调用UIView的相关属性


比如 frame、bounds、transform 等属性,尽量减少不必要的修改

不要给UITableViewCell动态添加subView,可以在初始化UITableViewCell的时候就将所有需要展示的添加完毕,然后根据需要来设置hidden属性显示和隐藏


3.提前计算好布局


在滑动时,会不断调用heightForRowAtIndexPath:,当Cell高度需要自适应时,每次回调都要计算高度,会导致UI卡顿。为了避免重复无意义的计算,需要缓存高度。

UITableViewCell高度计算主要分为两种,一种固定高度,另外一种动态高度。


固定高度:


rowHeight高度默认44

对于固定高度直接采用self.tableView.rowHeight = 77tableView:heightForRowAtIndexPath:更高效


动态高度:


采用tableView:heightForRowAtIndexPath:这种代理方式,设置这种代理之后rowHeight则无效,需要满足以下三个条件


使用Autolayout进行UI布局约束(要求cell.contentView的四条边都与内部元素有约束关系)

指定TableView的estimatedRowHeight属性的默认值

指定TableView的rowHeight属性为UITableViewAutomaticDimension

self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 44;

除了提高cell高度的计算效率之外,对于已经计算出的高度,我们需要进行缓存

UITableView-FDTemplateLayoutCell 高度缓存库,不过这个库很久没维护了。


4.直接设置frame


Autolayout 会比直接设置 frame 消耗更多的 CPU 资源


5.图片尺寸合适


图片的 size 最好刚好跟 UIImageView 的 size 保持一致 图片通过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时,会显著影响主线程的性能。因此在使用ASIHTTPRequest时,可以用一个NSOperationQueue来维护下载请求,并将其最大线程数目maxConcurrentOperationCount

NSURLRequest可以配合 GCD进阶技巧分享 来实现,或者使用NSURLConnection的setDelegateQueue:方法。


当然在不需要响应用户请求时,也可以增加下载线程数来加快下载速度:

- (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.预渲染图像


显示图像时,解压和重采样会消耗很多CPU时间,

当有图像时,在bitmap context先将其画一遍,导出成UIImage对象,然后再绘制到屏幕,这会大大提高渲染速度,

- (void)awakeFromNib {
    if (self.image == nil) {
        self.image = [UIImage imageNamed:@"xxx"];
        UIGraphicsBeginImageContextWithOptions(imageSize, YES, 0);
        [image drawInRect:imageRect];
        self.image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
    }
}


9.异步绘制


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

1.png

// 异步绘制,切换至子线程
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;
    });
});

这篇文章 iOS-UIView异步绘制 介绍的满详细

当然还是少不了YY大神的佳作,iOS 保持界面流畅的技巧(转载)


10.按需求加载


滑动UITableView时,按需加载对应的内容

//按需加载 - 如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后指定3行加载。
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
    NSIndexPath *ip = [self indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];
    NSIndexPath *cip = [[self indexPathsForVisibleRows] firstObject];
    NSInteger skipCount = 10;
    if (labs(cip.row-ip.row) > skipCount) {
        NSArray *temp = [self indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, self.width, self.height)];
        NSMutableArray *arr = [NSMutableArray arrayWithArray:temp];
        if (velocity.y < 0) {
            NSIndexPath *indexPath = [temp lastObject];
            if (indexPath.row > 3) {
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-3 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-2 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:0]];
            }
        }
        [self.needLoadDatas addObjectsFromArray:arr];
    }
}

还需要在tableView:cellForRowAtIndexPath:方法中加入判断

if (self.needLoadDatas.count > 0 && [self.needLoadDatas indexOfObject:indexPath] == NSNotFound) {
    //TODO:清理工作
    return;
}


GPU层面优化


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


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

RunLoop小操作
当前线程是主线程时,某些UI事件,比如ScrollView正在拖动,将会RunLoop切换成NSEventTrackingRunLoopMode模式,在这个模式下默认的NSDefaultRunLoopMode模式中注册的事件是不会执行

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    if (!cell) {
        cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
    }
    cell.selectionStyle = UITableViewCellSelectionStyleNone;
    KJTestModel *model = self.datas[indexPath.row];
    if (model.iconImage) {
        cell.imageView.image = model.iconImage;
    }else{
        NSDictionary *dict = @{@"imageView":cell.imageView,@"model":model};
        [self performSelector:@selector(kj_loadImageView:) withObject:dict afterDelay:0.0 inModes:@[NSDefaultRunLoopMode]];
    }
    cell.nameLabel.text = model.name;
    cell.IDLabel.hidden = model.remarkName == nil ? YES : NO;
    cell.label.text = model.remarkName;
}
/// 下载图片,并渲染到cell上显示
- (void)kj_loadImageView:(NSDictionary*)dict{
    UIImageView *imageView = dict[@"imageView"];
    [imageView sd_setImageWithURL:model.avatar completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
        KJTestModel *model = dict[@"model"];
        model.iconImage = image;
    }];
}


2.控制尺寸


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

3.减少图层混合操作


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

UIView的backgroundColor不要设置为clearColor,最好设置和superView的backgroundColor颜色一样。

图片避免使用带alpha通道的图片


4.透明处理


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


5.避免离屏渲染


离屏渲染就是在当前屏幕缓冲区以外,新开辟一个缓冲区进行操作

离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕切换到离屏;等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上,又需要将上下文环境从离屏切换到当前屏幕


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


光栅化,layer.shouldRasterize = YES

遮罩,layer.mask

圆角,同时设置 layer.masksToBounds = YES 和 layer.cornerRadius > 0

阴影,layer.shadow

layer.allowsGroupOpacity = YES 和 layer.opacity != 1

重写drawRect方法


2 - 圆角优化


这里主要其实就是解决同时设置layer.masksToBounds = YES layer.cornerRadius > 0就会产生的离屏渲染

其实我们在使用常规视图切圆角时,可以只使用view.layer.cornerRadius = 3.0,这时是不会产生离屏渲染

但是UIImageView这家伙有点特殊,切圆角时必须上面2句同时设置,则会产生离屏渲染,所以我们考虑通过 CoreGraphics 绘制裁剪圆角,或者叫美工提供圆角图片

- (UIImage *)kj_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;
}

镂空圆形图片覆盖,此方法可以实现圆形头像效果,这个也是极为高效的方法。缺点就是对视图的背景有要求,单色背景效果就最为理想


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
  • 直接让美工把图片切成圆角进行显示,这是效率最高的一种方案
  • 很多情况下用户上传图片进行显示,可以让服务端处理圆角

性能测试


在出现图像性能问题,滑动,动画不够流畅之后,首先要做的就是定位出问题的所在。而这个过程并不是只靠经验和穷举法探索。

定位帧率,为了给用户流畅的感受,需要保持帧率在60帧左右。

定位瓶颈,究竟是CPU还是GPU。占用率越少越好,一是为了流畅性,二也节省了电力。

检查有没有做无必要的CPU渲染,例如有些地方重写drawRect:

检查有没有过多的离屏渲染,这会耗费GPU的资源,像前面已经分析的到的。离屏渲染会导致GPU需要不断地onScreenoffscreen进行上下文切换。尽量避免离屏渲染。

检查有无过多的Blending,GPU渲染一个不透明的图层更省资源。

检查图片的格式是否为常用格式,大小是否正常。如果一个图片格式不被GPU所支持,则只能通过CPU来渲染。一般在开发中都应该用PNG格式,之前阅读过的一些资料也有指出苹果特意为PNG格式做了渲染和压缩算法上的优化。

检查是否有耗费资源多的View或效果。


测试工具:

Core Animation,Instruments里的图形性能问题的测试工具

View debugging,Xcode自带的,视图层级

Reveal,视图层级


最后简单介绍TableViewCell的部分常用属性


功能 API & Property
设置分割线颜色 [tableView setSeparatorColor:UIColor.orangeColor]
设置分割线样式 tableView.separatorStyle = UITableViewCellSeparatorStyleSingleLine
是否允许多选 tableView.allowsMultipleSelection
是否响应点击操作 tableView.allowsSelection = YES
返回选中的多行 tableView.indexPathsForSelectedRows
可见的行 tableView.indexPathsForVisibleRows

UITableView性能优化介绍就到此完毕,后面有相关再补充,写文章不容易,还请点个小星星传送门


备注:本文用到的部分函数方法和Demo,均来自三方库KJCategories,如有需要的朋友可自行pod 'KJCategories'引入即可

相关实践学习
部署Stable Diffusion玩转AI绘画(GPU云服务器)
本实验通过在ECS上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。
相关文章
|
6月前
|
算法 iOS开发 UED
iOS如何进行应用的性能优化?
iOS如何进行应用的性能优化?
98 2
|
3月前
|
缓存 监控 Android开发
探索iOS与安卓开发中的性能优化策略
在移动应用开发的竞技场上,iOS和安卓这两大操作系统不断推动着技术的边界。性能优化,作为提升用户体验的关键因素,已成为开发者们关注的焦点。本文将深入探讨两大平台上的性能优化实践,揭示如何通过工具、技术和策略来提升应用的响应速度和流畅度,同时考虑到电池寿命和内存管理等关键指标。
|
18天前
|
监控 iOS开发 开发者
iOS性能优化:深入函数调用栈与符号化技术
在iOS开发中,函数调用栈是理解程序执行流程和优化性能的关键。当应用出现性能问题或崩溃时,能够准确地读取和解析调用栈信息对于快速定位问题至关重要。本文将探讨iOS中的函数调用栈,以及如何通过符号化技术进行有效的性能调优。
24 3
|
5月前
|
Java Android开发 iOS开发
深入探讨移动操作系统的性能优化:安卓与iOS的对比分析
在现代移动设备中,操作系统的性能优化至关重要。本文从系统架构、内存管理、电池续航和应用程序运行效率等多个维度,深入探讨了安卓(Android)和iOS两大主流移动操作系统的优化策略及其实际效果,旨在为开发者和用户提供更清晰的了解和选择依据。
201 0
|
2月前
|
图形学 iOS开发 Android开发
从Unity开发到移动平台制胜攻略:全面解析iOS与Android应用发布流程,助你轻松掌握跨平台发布技巧,打造爆款手游不是梦——性能优化、广告集成与内购设置全包含
【8月更文挑战第31天】本书详细介绍了如何在Unity中设置项目以适应移动设备,涵盖性能优化、集成广告及内购功能等关键步骤。通过具体示例和代码片段,指导读者完成iOS和Android应用的打包与发布,确保应用顺利上线并获得成功。无论是性能调整还是平台特定的操作,本书均提供了全面的解决方案。
153 0
|
4月前
|
算法 数据库 Android开发
探索iOS与安卓应用开发的性能优化策略
在移动应用开发领域,性能优化是提升用户体验、增强应用市场竞争力的关键因素。本文深入探讨了iOS和安卓平台下,开发者如何通过具体技术和方法有效优化应用性能。文章首先概述了性能优化的重要性,随后详细分析了iOS和安卓开发中的优化策略,包括代码优化、资源管理和异步处理等技术手段。最后,通过案例分析,展示了这些优化措施在实际开发中的应用效果,旨在为开发者提供实用的性能提升建议。
|
6月前
|
缓存 Android开发 iOS开发
打造高效移动应用:Android与iOS性能优化策略
【4月更文挑战第29天】 在移动设备日益成为用户日常互动的主要平台的今天,应用程序的性能已成为决定其成功的关键因素之一。本文将探讨针对Android和iOS平台的性能优化技巧,涵盖内存管理、多线程处理、网络请求优化以及用户界面的流畅性提升等方面。通过分析不同操作系统的架构特点,我们旨在提供一套综合性的策略,帮助开发者构建快速、响应迅捷且用户体验良好的应用。
|
6月前
|
监控 iOS开发
iOS15适配问题:viewForSupplementaryElementOfKind表头和表尾复用闪退,UITableView section header多22像素等问题
iOS15适配问题:viewForSupplementaryElementOfKind表头和表尾复用闪退,UITableView section header多22像素等问题
106 0
|
iOS开发