Tableview 性能优化
Tableview 性能优化方法总览
- Tableview 懒加载、Cell 重用
- 高度缓存(因为 heightForRowAtIndexPath: 是调用最频繁的方法)
- 当 cell 的行高固定时,使用固定行高 self.tableView.rowHeight = 88;
- 当 cell 的行高是不固定时,根据内容进行计算后缓存起来使用。第一次肯定会计算,后续使用缓存时就避免了多次计算;高度的计算方法通常写在自定义的cell中,调用时,既可以在设置 cell 高的代理方法中使用,也可以自定义的 model 中使用(且使用时,使用get方法处理)。
- 数据处理
(1)使用正确的数据结构来存储数据;
(2)数据尽量采用局部的 section,或 cellRow 的刷新,避免 reloadData;
(3)大量数据操作时,使用异步子线程处理,避免主线程中直接操作;
(4)缓存请求结果。 - 异步加载图片:SDWebImage 的使用
(1)使用异步子线程处理,然后再返回主线程操作;
(2)图片缓存处理,避免多次处理操作;
(3)图片圆角处理时,设置 layer 的 shouldRasterize 属性为 YES,可以将负载转移给 CPU。 - 按需加载内容
(1)滑动操作时,只显示目标范围内的 Cell 内容,显示过的超出目标范围内之后则进行清除;
(2)滑动过程中,不加载显示图片,停止时才加载显示图片。 - 视图层面
(1)减少 subviews 的数量,自定义的子视图可以整合在形成一个整体的就整合成一个整体的子视图;
(2)使用 drawRect 进行绘制(即将 GPU 的部分渲染转接给 CPU ),或 CALayer 进行文本或图片的绘制。在实现 drawRect 方法的时候注意减少多余的绘制操作,它的参数 rect 就是我们需要绘制的区域,在 rect 范围之外的区域我们不需要进行绘制,否则会消耗相当大的资源;
(3)异步绘制,且设置属性 self.layer.drawsAsynchronously = YES;(遇到复杂界面,遇到性能瓶颈时,可能就是突破口);
(4)定义一种(尽量少)类型的 Cell 及善用 hidden 隐藏(显示) subviews;
(5)尽量使所有的 view 的 opaque 属性为 YES,包括 cell 自身,以提高视图渲染速度(避免无用的 alpha 通道合成,降低 GPU 负载);
(6)避免渐变,图片缩放的操作;
(7)使用 shadowPath 来画阴影;
(8)尽量不使用 cellForRowAtIndexPath: ,如果你需要用到它,只用一次然后缓存结果;
(9)cellForRowAtIndexPath 不要做耗时操作:如不读取文件 / 写入文件;尽量少用 addView 给 Cell 动态添加 View,可以初始化时就添加,然后通过 hide 来控制是否显示;
(10)我们在 Cell 上添加系统控件的时候,实际上系统都会调用底层的接口进行绘制,大量添加控件时,会消耗很大的资源并且也会影响渲染的性能。当使用默认的 UITableViewCell 并且在它的 ContentView 上面添加控件时会相当消耗性能。所以目前最佳的方法还是继承 UITableViewCell,并重写 drawRect 方法;
(11)当我们需要圆角效果时,可以使用一张中间透明图片蒙上去使用 ShadowPath 指定 layer 阴影效果路径使用异步进行 layer 渲染(Facebook 开源的异步绘制框架 AsyncDisplayKit )设置 layer 的 opaque 值为 YES ,减少复杂图层合成尽量使用不包含透明(alpha)通道的图片资源尽量设置 layer 的大小值为整形值直接让美工把图片切成圆角进行显示,这是效率最高的一种方案很多情况下用户上传图片进行显示,可以让服务端处理圆角使用代码手动生成圆角 Image 设置到要显示的 View 上,利用 UIBezierPath ( CoreGraphics 框架)画出来圆角图片。
TableViewCell 复用
TableViewCell 复用介绍
- TableView 内部有一个 Cell 池,里面放的就是你之前创建过的 Cell。内存分配时会保存一些 UITableViewCell 对象放入到 cell 池,在需要调用的时候迅速的返回,而不用创建。内存吃紧时 Cell 池会自动清理一些多余的 UITableViewCell 对象。至于有多少 Cell ,这个内部会自动控制。
注意:重取出来的 Cell 是有可能捆绑过数据或者加过子视图的,所以,如果有必要,要清除数据(如 Label 的边框),从而使其显示正确的内容。 - TableviewCell 复用的方法
dequeueReusableCellWithIdentifier:forIndexPath:
(iOS6引入)
// 必须与 register 方法配套使用,否则返回的 Cell 可能为 nil,会crash
[slef.myTableView registerClass:[MyCell class] forCellReuseIdentifier:NSStringFromClass([MyCell class])];
MyCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([MyCell class]) forIndexPath:indexPath];
- 注册不同类型的 Cell 或者不复用,此处以不复用为例
@property (nonatomic, strong) NSMutableDictionary *cellDic; // 放 Cell 的标识符
// 每次先从字典中根据IndexPath取出唯一标识符
NSString *identifier = [_cellDic objectForKey:[NSString stringWithFormat:@"%@", indexPath]];
// 如果取出的唯一标示符不存在,则初始化唯一标示符,并将其存入字典中,对应唯一标示符注册Cell
if (identifier == nil) {
identifier = [NSString stringWithFormat:@"%@%@", @"cell", [NSString stringWithFormat:@"%@", indexPath]];
[_cellDic setValue:identifier forKey:[NSString stringWithFormat:@"%@", indexPath]];
// 注册Cell
[self.tableview registerClass:[MyCell class] forCellWithReuseIdentifier:identifier];
}
MyCell *cell = [tableView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath];
- UITableView 复用机制原理
查看 UITableView 头文件,会找到NSMutableArray *visiableCells,和NSMutableDictionary *reusableTableCells两个结构。其中visiableCells用来存储当前UITableView显示的cell,reusableTableCells用来存储已经用’identify’缓存的cell。当UITableView滚动的时候,会先在reusableTableCells中根据identify找是否有有已经缓存的cell,如果有直接用,没有再去初始化。(TableView显示之初,reusableTableCells为空,那么tableView dequeueReusableCellWithIdentifier:CellIdentifier返回nil。开始的cell都是通过[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]来创建,而且cellForRowAtIndexPath只是调用最大显示cell数的次数)
缓存 cell 高度
- 如果用的 frame ,则给 model 添加一个 cellH 的属性,然后在获取数据时计算好高度赋值给 cellH。
- 如果用的 AutoLayout,创建相应布局等同的 cell,计算好高度然后缓存。
@property (nonatomic, strong) NSMutableDictionary *heightAtIndexPath;//缓存高度所用字典
#pragma mark - UITableViewDelegate-(CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSNumber *height = [self.heightAtIndexPath objectForKey:indexPath]; if(height){
return height.floatValue;
}else {
return 100;
}
}
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{ NSNumber *height = @(cell.frame.size.height);
[self.heightAtIndexPath setObject:height forKey:indexPath];
}
FD 的实现:fd_heightForCellWithIdentifier: configuration:
方法会根据 identifier 以及 configuration block 提供一个和 cell 布局相同的 template layout cell,并将其传入 fd_systemFittingHeightForConfiguratedCell:
这个私有方法返回计算出的高度。主要使用技术为 runtime 。
离屏渲染
OpenGL 中,GPU 屏幕渲染有以下两种方式:
On-Screen Rendering:意思是当前屏幕渲染,指的是 GPU 的渲染操作是在当前用于显示的屏幕缓冲区进行。
Off-Screen Rendering:意思就是我们说的离屏渲染了,指的是 GPU 在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。
相比于当前屏幕渲染,离屏渲染的代价是很高的,主要体现在两个方面:
- 创建新缓冲区,要想进行离屏渲染,首先要创建一个新的缓冲区。
- 上下文切换,离屏渲染的整个过程,需要多次切换上下文环境:先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上有需要将上下文环境从离屏切换到当前屏幕。而上下文环境的切换是要付出很大代价的。
离屏渲染触发条件
- custom drawRect: (any, even if you simply fill the background with color)
- CALayer mask
- CALayer shadow
- any custom drawing using CGContext 具体表现为mask(遮罩)、 shadow(阴影)、shouldRasterize(光栅化)、edge antialiasing(抗锯齿)、group opacity(不透明)、复杂形状设置圆角等、渐变
CPU和GPU:
- CPU,负责视图相关的计算工作并告知GPU应该怎么绘图;
- GPU,进行图形的绘制、渲染等工作。
圆角优化
优化方案1:使用贝塞尔曲线 UIBezierPath 和 Core Graphics 框架画出一个圆角
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
imageView.image = [UIImage imageNamed:@"myImg"];
// 开始对 imageView 进行画图
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);
// 使用贝塞尔曲线画出一个圆形图
[[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:imageView.frame.size.width] addClip];
[imageView drawRect:imageView.bounds];
imageView.image = UIGraphicsGetImageFromCurrentImageContext();
// 结束画图
UIGraphicsEndImageContext();
[self.view addSubview:imageView];
优化方案2:使用 CAShapeLayer 和 UIBezierPath 设置圆角
UIImageView *imageView = [[UIImageViewalloc] initWithFrame:CGRectMake(100,100,100,100)];
imageView.image = [UIImageimageNamed:@"myImg"];
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imageView.boundsbyRoundingCorners:UIRectCornerAllCornerscornerRadii:imageView.bounds.size];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc]init];
// 设置大小
maskLayer.frame = imageView.bounds;
// 设置图形样子
maskLayer.path = maskPath.CGPath;
imageView.layer.mask = maskLayer;
[self.viewaddSubview:imageView];
对于 方案2 需要解释的是:
CAShapeLayer 继承于 CALayer,可以使用 CALayer 的所有属性值;CAShapeLayer 需要贝塞尔曲线配合使用才有意义(也就是说才有效果)使用 CAShapeLayer(属于CoreAnimation)与贝塞尔曲线可以实现不在 view 的 drawRect(继承于 CoreGraphics 走的是 CPU,消耗的性能较大)方法中画出一些想要的图形 CAShapeLayer 动画渲染直接提交到手机的 GPU 当中,相较于 view 的 drawRect 方法使用 CPU 渲染而言,其效率极高,能大大优化内存使用情况。
总的来说就是用 CAShapeLayer 的内存消耗少,渲染速度快,建议使用优化方案2。
异步绘制
系统绘制流程图
- CALayer 内部创建一个 backing store(CGContextRef)();
- 判断 layer 是否有代理;
- 有代理:调用 delegete 的 drawLayer:inContext, 然后在合适的实际回调代理, 在[UIView drawRect]中做一些绘制工作;
- 没有代理:调用 layer 的 drawInContext 方法,
- layer 上传 backingStore 到 GPU, 结束系统的绘制流程;
UIView 的绘制流程图
- UIView 调用 setNeedsDisplay,但是没立即进行视图的绘制工作;
- UIView 调用 setNeedDisplay 后,系统调用 view 对应 layer 的 setNeedsDisplay 方法;
- 当前 runloop 即将结束的时候调用 CALayer 的 display 方法;
- runloop 即将结束, 开始视图的绘制流程;
异步绘制
- 异步绘制的入口在 [layer.delegate displayLayer]
- 异步绘制过程中代理负责生成对应的位图(bitmap);
- 将 bitmap 赋值给 layer.content 属性;
- 某个时机调用 setNeedsDisplay;
- runloop 将要结束的时候调用 [CALayer display]
- 如果代理实现了 dispalyLayer 将会调用此方法, 在子线程中去做异步绘制的工作;
- 子线程中做的工作:创建上下文, 控件的绘制, 生成图片;
- 转到主线程, 设置 layer.contents, 将生成的视图展示在 layer 上面;
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface AsyncLabel : UIView
@property (nonatomic, copy) NSString *asynText;
@property (nonatomic, strong) UIFont *asynFont;
@property (nonatomic, strong) UIColor *asynBGColor;
@end
NS_ASSUME_NONNULL_END
#import "AsyncLabel.h"
#import <CoreText/CoreText.h>
@implementation AsyncLabel
- (void)displayLayer:(CALayer *)layer {
/**
除了在 drawRect 方法中, 其他地方获取 context 需要自己创建
*/
CGSize size = self.bounds.size;;
CGFloat scale = [UIScreen mainScreen].scale;
/// 异步绘制:切换至子线程
dispatch_async(dispatch_get_global_queue(0, 0), ^{
UIGraphicsBeginImageContextWithOptions(size, NO, scale);
/// 获取当前上下文
CGContextRef context = UIGraphicsGetCurrentContext();
/// 将坐标系反转
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
/// 文本沿着Y轴移动
CGContextTranslateCTM(context, 0, size.height);
/// 文本反转成context坐标系
CGContextScaleCTM(context, 1.0, -1.0);
/// 创建绘制区域
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));
/// 创建需要绘制的文字
NSMutableAttributedString *attStr = [[NSMutableAttributedString alloc] initWithString:self.asynText];
[attStr addAttribute:NSFontAttributeName value:self.asynFont range:NSMakeRange(0, self.asynText.length)];
[attStr addAttribute:NSBackgroundColorAttributeName value:self.asynBGColor range:NSMakeRange(0, self.asynText.length)];
/// 根据attStr生成CTFramesetterRef
CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attStr);
CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, attStr.length), path, NULL);
/// 将frame的内容绘制到content中
CTFrameDraw(frame, context);
UIImage *getImg = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
/// 子线程完成工作, 切换到主线程展示
dispatch_async(dispatch_get_main_queue(), ^{
self.layer.contents = (__bridge id)getImg.CGImage;
});
});
}
@end
#import "ViewController.h"
#import "AsyncLabel.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
AsyncLabel *asLabel = [[AsyncLabel alloc] initWithFrame:CGRectMake(50, 100, 200, 200)];
asLabel.backgroundColor = [UIColor cyanColor];
asLabel.asynBGColor = [UIColor greenColor];
asLabel.asynFont = [UIFont systemFontOfSize:16 weight:20];
asLabel.asynText = @"学习异步绘制相关知识点, 学习异步绘制相关知识点";
[self.view addSubview:asLabel];
///不调用的话不会触发 displayLayer方法
[asLabel.layer setNeedsDisplay];
}
@end
其他优化
- 子线程异步处理数据
- (void)loadData{
// 开辟子线程处理数据
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 处理数据 coding...
// 返回主线程处理
dispatch_async(dispatch_get_main_queue(), ^{
[self.mainTableView reloadData];
});
});
- 不要做多余的绘制工作在实现 drawRect: 的时候,它的rect参数就是需要绘制的区域,这个区域之外的不需要进行绘制。 例如上例中,就可以用CGRectIntersectsRect、CGRectIntersection 或 CGRectContainsRect 判断是否需要绘制 image 和 text,然后再调用绘制方法。
- 如图,这个 label 显示的内容由 model 的两个参数(时间、公里数)拼接而成,我们习惯在 cell 里 model 的 set 方法中这样赋值
//时间
NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
formatter.dateStyle = NSDateFormatterMediumStyle;
formatter.timeStyle = NSDateFormatterShortStyle;
[formatter setDateFormat:@"yyyy年MM月"];
NSDate* date = [NSDate dateWithTimeIntervalSince1970:[model.licenseTime intValue]];
NSString* licenseTimeString = [formatter stringFromDate:date];
// 公里数
NSString *travelMileageString = (model.travelMileage != nil && ![model.travelMileage isEqualToString:@""]) ? [NSString stringWithFormat:@"%@万公里",model.travelMileage] : @"里程暂无";
// 赋值给label.text
self.carDescribeLabel.text = [NSString stringWithFormat:@"%@ / %@",licenseTimeString,travelMileageString];
在 tableview 滚动的过程中,这些对象就会被来回的创建,并且这个计算过程是在主线程里被执行的。
我们可以把这些操作,移到第2步(字典转模型)来做,计算好这个 label 需要显示的内容,作为属性存进 model 中,需要的时候直接用。
而下面这个例子也是缓存思想的体现:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 15.0 + 80.0 + 15.0;
}
修改为
static float ROW_HEIGHT = 15.0 + 80.0 + 15.0;
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return ROW_HEIGHT;
}
当然这不是减少对象的创建,而是减少了计算的次数,减少了频繁调用方法里的逻辑,从而达到更快的速度。
一些优化方案对比
- Autolayout + AutomaticDimension:
由 AutoLayout 进行布局,Cell 自适应的高度使用系统UITableViewAutomaticDimension。这种实现方案用起来简单,但当TableView快速滑动时,就会出现掉帧,特别卡。 - Autolayout + CountHeight:
AutoLayout 的方式布局,Cell 的高度放在子线自己计算的,优于第一种实现方式,不过掉帧也是比较严重的。 - FrameLayout + CountHeight:
Frame 布局,Cell 高度在子线程中进行计算。比较流畅的,折中方案。 - YYKit + CountHeight:
用到了 YYKit 中的控件,并且使用 Frame 布局与 Cell 高度的计算。这种方式要优于上面的解决方案,因为 YYKit 中的一些控件做了优化。 - AsyncDisplayKit + CountHeight:
使用了 AsyncDisplayKit 中提供的相关 Note 代替系统的原生控件,这种实现方式是这 5 种实现方式中最为流畅的。