iOS_缓存Cell行高的基本思路

在许多关于 UITableview 性能优化的文章里都提到了缓存行高的优化方式,这也是苹果工程师提出的改进建议.
正常情况下,heigtForRowAtIndexPath: 方法会被调用很多次,在 UITableview 滚动的过程中也会不断的调用,这时如果我们只计算一次 Cell的高度,之后每次调用时都返回缓存的高度,就能让 UITableview 的滑动更加流畅,尤其是对高度计算特别耗时的复杂的 Cell 来说.


这篇文章中,我们来打造一个极简的行高缓存工具类 ModelSizeCache
这样命名是有原因的,我们来慢慢分析.

基本思路

下图是我为了辅助说明缓存行高而制作的 Demo, 源码在 Github, 建议结合源码来看下面的博文

demo.gif

cell 主要有3个控件

UIImageView *demoImageView;
UILabel *demoLabel;
UIStepper *demoStepper;

每一个展示一个 Joke Model ,模型类主要有4个属性,

NSString *objectID;
NSString *content;
NSString *imageName;
NSInteger repeatCount; //文字内容重复次数,模拟 Model 中数据变化,重新计算高度的情况

ModelSizeCache 使用

相比没有缓存高度的情况,只需修改一个方法:

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    //先从缓存根据 Model 的 hash 值取缓存的行高,如果没有就调用后面的 orCalc Block计算行高,将计算结果存入缓存,然后返回行高.
    return [self.modelSizeCache getHeightForModel:self.jokes[indexPath.row] withTableView:tableView orCalc:^CGFloat(id model, UITableView *tableView) {
        return [self.prototypeCell calcCellHeightWithJoke:self.jokes[indexPath.row] tableView:tableView];
    }];
}

基本思路

  • 首先我们要计算一次 Cell 的高度,之后每次都返回缓存的高度
  • 我们的 Cell 的高度根据 Model, 这里是 Joke 模型类来计算的,所以我们缓存的高度应该说是填充完 Model 数据后 Cell的高度
  • 如果 Model 的内容变化了,比如上图中的文字长度变化了,就要重新计算行高,并缓存起来.

由上面的说明我们得出以下结论:

  1. Cell 来计算高度最合适, Cell 知道自己的 View 是怎样布局的,然后在传入 Model ,就能计算出行高,所以我们在 Cell 中添加
    -(CGFloat)calcCellHeightWithJoke:(Joke *)joke tableView:(UITableView *)tableView 方法来计算行高.
  2. Cell 的行高根据它填充的数据模型 Model 而计算出来的,所以我们要根据 Model 来缓存行高.

第一点: 计算 cell 高度

  • 本 Demo 为了简洁使用 AutoLayout + Storyboard布局,使用
    [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
    来根据 Cell 的约束来计算 Cell 高度

  • 如果你使用 [NSAttributedString boundingRectWithSize:options:context:] 来计算文字高度,在加上图片的高度等的方式得出 Cell 高度,那么这个 Cell 高度计算过程可以在从网络加载完 JSON 数据就在后台执行,并将计算结果缓存起来,在 UITableview请求 cell 高度时,直接返回缓存的高度就好了,这样就避免了在主线程计算 Cell 高度,达到了UITableview滑动优化目的.

  • 但由于我使用 [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]; 方法来计算 Cell 高度,需要访问 View, 所以不能在后台先执行计算,就将计算过程放在UITableviewheightForRowAtIndexPath方法,第一次请求该 Model 对应的 Cell 行高时完成.

前文:在后台计算 Model 对应的行高思路来自于
YYKit 作者,ibireme iOS 保持界面流畅的技巧一文

预排版:
当获取到 API JSON 数据后,我会把每条 Cell 需要的数据都在后台线程计算并封装为一个布局对象 CellLayout。CellLayout 包含所有文本的 CoreText 排版结果、Cell 内部每个控件的高度、Cell 的整体高度。每个 CellLayout 的内存占用并不多,所以当生成后,可以全部缓存到内存,以供稍后使用。这样,TableView 在请求各个高度函数时,不会消耗任何多余计算量;当把 CellLayout 设置到 Cell 内部时,Cell 内部也不用再计算布局了

对于通常的 TableView 来说,提前在后台计算好布局结果是非常重要的一个性能优化点。为了达到最高性能,你可能需要牺牲一些开发速度,不要用 Autolayout 等技术,少用 UILabel 等文本控件。但如果你对性能的要求并不那么高,可以尝试用 TableView 的预估高度的功能,并把每个 Cell 高度缓存下来。这里有个来自百度知道团队的开源项目可以很方便的帮你实现这一点:FDTemplateLayoutCell

第二点:缓存 Model 高度

我们将计算好的 Model 高度存入 NSCache 类中,这个集合类很像 NSMutableDictionary,主要有下面2个方法

- (nullable ObjectType)objectForKey:(KeyType)key;
- (void)setObject:(ObjectType)obj forKey:(KeyType)key; 
  • 将高度存入 NSCache中需要一个 Key, 一般我们的模型类,比如一条微博,一句聊天信息,都有一个唯一 ID, 我们可以使用它作为 Key
  • 但如果模型类中的数据变化了,比如上面 Demo Gif 中的Joke模型类的文字长度变化了,就要让这个缓存的高度失效,根据 Model 的数据重新计算行高.

ModelSizeCache 具体实现

ModelSizeCache 定义了一个 protocol

@protocol ModelSizeCacheProtocol <NSObject>

-(NSString*)modelID;

@end

任何需要缓存高度的模型类都应该遵守这个协议,返回 Model 的唯一 ID,这个 ID 作为在 NSCache 中存取缓存行高的 Key.

ModelSizeCache 继承自 NSObject, 有2个属性

@property (strong,nonatomic) NSCache *cacheLandscape;
@property (strong,nonatomic) NSCache *cachePortrait;

分别缓存 Model 在横竖屏状态下的 Cell 高度,
主要的方法只有一个

-(CGFloat)getHeightForModel:(id<ModelSizeCacheProtocol>)model withTableView:(UITableView *)tableView orCalc:(CalcModelHeightBlock)block{
    //先从缓存中取行高
    CGSize modelSize= [self getCacheSizeForModel:model];
    //没有就计算一下
    if( CGSizeEqualToSize(modelSize, NilCacheSize)){
        modelSize.height= block(model,tableView);
        
        //计算完成存入缓存中
        [self setOrientationSize:modelSize forModel:model];
        NSLog(@"计算行高 :%@",@(modelSize.height));
    }
    return modelSize.height;
}

其中 const CGSize NilCacheSize ={-1,-1};

然后使用时修改一个方法即可

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    //先从缓存根据 Model 的 hash 值取缓存的行高,如果没有就调用后面的 orCalc Block计算行高,将计算结果存入缓存,然后返回行高.
    return [self.modelSizeCache getHeightForModel:self.jokes[indexPath.row] withTableView:tableView orCalc:^CGFloat(id model, UITableView *tableView) {
        return [self.prototypeCell calcCellHeightWithJoke:self.jokes[indexPath.row] tableView:tableView];
    }];
}

最后在我们点击 UIStepper 时,更改模型类的数据,并让缓存的高度失效即可,这样会重新计算这个 Model 的高度,并存入缓存,其它的 Model 直接读取缓存,因为他们的数据没有变化,Cell 的高度也就没有变化.

-(void)cell:(Cell *)cell didStepperValueChanged:(NSInteger)value{
    NSIndexPath *indexPath= [self.tableView indexPathForCell:cell];
    Joke *joke= self.jokes[indexPath.row];
    joke.repeatCount=value; //修改 Model 的数据
    [self.modelSizeCache invalidateCacheForModel:joke]; //让缓存失效
    [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
}

这样就达到了缓存行高,优化UITableview 滑动性能的作用.
完整代码在 Github

关于用 ModelSizeCache存储行高:

其实也可以用 Category + Associated Objects 为模型类添加@property CGFloat height 属性来存储模型的高度,但是我觉得存储在一个单独的 ModelSizeCache中更合适,不污染模型类的代码,方便集中管理缓存数据.

Ref

其它缓存行高的第三方库:

forkingdog/UITableView-FDTemplateLayoutCell
Raizlabs/RZCellSizeManager

UITableview 性能优化的文章

iOS 保持界面流畅的技巧

NSCache

Autolayout 计算行高

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353

推荐阅读更多精彩内容