- tableview 性能优化方法总览
- tableViewCell 复用
- 缓存 cell 高度
- 圆角优化
- 异步绘制
- 其他优化
- 一些优化方案的对比
heightForRowAtIndexPath:
是调用最频繁的方法)
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数的次数)
@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_heightForCellWithIdentifier: configuration:
方法会根据 identifier 以及 configuration block 提供一个和 cell 布局相同的 template layout cell,并将其传入 fd_systemFittingHeightForConfiguratedCell:
这个私有方法返回计算出的高度。主要使用技术为 runtime 。OpenGL中,GPU屏幕渲染有以下两种方式: On-Screen Rendering:意思是当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区进行。 Off-Screen Rendering:意思就是我们说的离屏渲染了,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。
相比于当前屏幕渲染,离屏渲染的代价是很高的,主要体现在两个方面:
离屏渲染触发条件
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]; 复制代码
UIImageView *imageView = [[UIImageViewalloc]initWithFrame:CGRectMake(100,100,100,100)]; imageView.image = [UIImageimageNamed:@"myImg"]; UIBezierPath *maskPath = [UIBezierPathbezierPathWithRoundedRect: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。
#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需要自己创建[https://www.jianshu.com/p/86f025f06d62] coreText用法简介:[https://www.cnblogs.com/purple-sweet-pottoes/p/5109413.html] */ 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]; 复制代码
- (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; } 复制代码当然这不是减少对象的创建,而是减少了计算的次数,减少了频繁调用方法里的逻辑,从而达到更快的速度。
文章推荐:
VVeboTableViewDemo
性能优化-UITableView的优化使用
iOS 保持界面流畅的技巧