UITableView缓存行高

UITableView是我们在日常开发中使用频率比较高的控件,TableView中不同的Cell可能因为呈现内容的不同而拥有不同的高度,如果在每一次展现Cell时都去计算Cell的高度可能会造成TableView在滚动过程中不必要的卡顿,而实际上同一行Cell展现在界面上时如果它的内容没有发生改变高度也是不会发生改变的,如果每一次都对高度进行重新计算势必会造成系统资源的浪费导致不必要的界面卡顿。如果我们能够将Cell高度进行缓存则可以很好的避免这一问题。


实现思路

TableView中每一个Cell的高度都是由其显示的数据来决定的,因此我们可以准备一个Cell,每当我们需要某一行的高度时,我们就对这个Cell赋值以获取高度并存入缓存,当我们再次需要这一行的高度时就可以直接从缓存中获取。


一、缓存管理类
缓存设计

由于TableView可以insertSection、delete、move、reload组/行,想要保持缓存数据的准确,我们必须对缓存进行相应的操作,为了方便操作我们可以将缓存设计为 NSMutableDictionary <NSString *, NSMutableDictionary <NSString *, NSNumber *> *> * 类型,这样我们可以很方便的以组或者行为最小单元对缓存数据进行操作。

//结构
{
  @"-1":maxSectionIndex,//记录当前缓存中section的最大值 insert、delete、move时我们需要以此来对当前缓存中的数据进行移动操作
  section0: {
    @"-1":maxRowIndex, //记录当前组中row的最大值 insert、delete、move时我们需要以此来对当前缓存中的数据进行移动操作
    row0: height,
    row1: height,
    ...
  },
  section1: {
    @"-1":maxRowIndex,
    row0: height,
    row1: height,
    ...
  },
  ...
}
例:
{
  @"-1":@1,
  @"0": {
    @"-1":@1,
    @"0": @45.6,
    @"1": @65,
  },
  @"1": {
    @"-1":@3,
    @"0": @45.6,
    @"1": @60,
    @"2": @65,
    @"3": @65,
  },
}

此外当我们的设备处于横屏和竖屏时同一行的高度也不相同因此我们需要两个字典来分别记录横屏状态下的行高和竖屏状态下的行高,当我们对缓存进行insert、delete、move、reload操作时需要同时对两个字典进行操作以保证横竖屏状态下的缓存正确性。

获取缓存值
- (CGFloat)heightWithSection:(NSInteger)section row:(NSInteger)row {
    //获取当前屏幕状态下的缓存高度
    NSNumber * height = cacheValue(self.currentHeight,section, row);
    if (height) {
        return height.CGFloatValue;
    }
    else {//不存在记录返回CGFLOAT_MAX通知TableView计算高度
        return CGFLOAT_MAX;
    }
}
设置缓存值
- (void)setHeight:(CGFloat)height section:(NSInteger)section row:(NSInteger)row {
    //取对应组的缓存字典
    NSMutableDictionary * sectionCache = [self.currentHeight objectForKey:key(section)];
    //如果没有取到则初始化
    if (sectionCache == nil) {
        sectionCache = [self see_initSection:section];
    }
    //初始化一行
    [self see_initRow:row cache:sectionCache];
    [sectionCache setObject:@(height) forKey:key(row)];
}

- (NSMutableDictionary *)see_initSection:(NSInteger)section {
    //创建组
    NSMutableDictionary * newSection = [NSMutableDictionary dictionaryWithObjectsAndKeys:@(-1),@"-1", nil];
    [self.currentHeight setObject:newSection forKey:key(section)];
    //如果组号大于当前记录的最大值 则修改最大值
    NSInteger sectionLastIndex = currentIndex(self.currentHeight);
    if (sectionLastIndex < section) {
        setCurrentIndex(self.currentHeight, section);
    }
    return newSection;
}

- (void)see_initRow:(NSInteger)row cache:(NSMutableDictionary *)cache {
    //如果行号大于当前记录的最大值 则修改最大值
    NSInteger rowLastIndex = currentIndex(cache);
    if (rowLastIndex < row) {
        setCurrentIndex(cache, row);
    }
}
针对缓存字典的insert、delete、move、reload

insert

key:value                              key:value
    0:0                                        0:0
    1:1     insert at index 1          2:1
    2:2    ————————>     3:2
    3:3                                        4:3
   befor                                     after

- (void)insertSection:(NSInteger)section {
    [self see_insertIndex:section cache:self.heightH];
    [self see_insertIndex:section cache:self.heightV];
}

- (void)insertRow:(NSInteger)row inSection:(NSInteger)section {
    [self see_insertIndex:row cache:[self.heightV objectForKey:key(section)]];
    [self see_insertIndex:row cache:[self.heightH objectForKey:key(section)]];
}

- (void)see_insertIndex:(NSInteger)index cache:(NSMutableDictionary *)cache {
    NSInteger lastIndex = currentIndex(cache);
    if (lastIndex < 0) return;
    for (NSInteger i = lastIndex; i >= index; i--) {
        [self see_exchangeIndex:i withIndex:i + 1 cache:cache];
    }
    //如果section/row小于当前缓存坐标 则 坐标+1  如果大于则等待设置缓存值初始化组时设置
    if (lastIndex >= index) {
        setCurrentIndex(cache, lastIndex + 1);
        [cache removeObjectForKey:key(index)];
    }
}

delete

key:value                                  key:value                                  key:value
   0:0                                             0:0                                             0:0
   1:1         move 1 to last             1:2         delete last                    1:2
   2:2   ——————————>    2:3   ——————————>    2:3
   3:3                                             3:1
  befor                                                                                            after

- (void)deleteSection:(NSInteger)section {
    [self see_deleteIndex:section cache:self.heightH];
    [self see_deleteIndex:section cache:self.heightV];
}

- (void)deleteRow:(NSInteger)row inSection:(NSInteger)section {
    [self see_deleteIndex:row cache:[self.heightV objectForKey:key(section)]];
    [self see_deleteIndex:row cache:[self.heightH objectForKey:key(section)]];
}

- (void)see_deleteIndex:(NSInteger)index cache:(NSMutableDictionary *)cache {
    NSInteger lastIndex = currentIndex(cache);
    if (lastIndex < 0) return;
    //将指定index的数据移动到末尾
    [self see_moveIndex:index toIndex:lastIndex cache:cache];
    //将末尾数据删除
    [cache removeObjectForKey:key(lastIndex)];
    //更新index最大值
    NSArray * keys = [cache.allKeys sortedArrayUsingComparator:^NSComparisonResult(NSString *  _Nonnull obj1, NSString *  _Nonnull obj2) {
        if (obj1.integerValue > obj2.integerValue) {
            return NSOrderedDescending;
        }
        else if (obj1.integerValue < obj2.integerValue) {
            return NSOrderedSame;
        }
        else {
            return NSOrderedSame;
        }
    }];
    setCurrentIndex(cache, ((NSString *)keys.lastObject).integerValue);
}

move

- (void)moveSection:(NSInteger)section toSection:(NSInteger)tSection {
    [self see_moveIndex:section toIndex:tSection cache:self.heightH];
    [self see_moveIndex:section toIndex:tSection cache:self.heightV];
}

- (void)moveRow:(NSInteger)row inSection:(NSInteger)section toRow:(NSInteger)tRow inSection:(NSInteger)tSection {
    if (section == tSection) {
        //组内交换
        [self see_moveIndex:row toIndex:tRow cache:[self.heightH objectForKey:key(section)]];
        [self see_moveIndex:row toIndex:tRow cache:[self.heightV objectForKey:key(section)]];
    }
    else {
        //组外交换
        NSNumber * tempH = [[self.heightH objectForKey:key(section)] objectForKey:key(row)];
        NSNumber * tempV = [[self.heightV objectForKey:key(section)] objectForKey:key(row)];
        //1.将目标缓存删除
        [self deleteRow:row inSection:section];
        //2.在目标组中插入一行
        [self insertRow:tRow inSection:tSection];
        //3.赋值
        [[self.heightH objectForKey:key(tSection)] setObject:tempH forKey:key(tRow)];
        [[self.heightV objectForKey:key(tSection)] setObject:tempV forKey:key(tRow)];
        
    }
}

- (void)see_moveIndex:(NSInteger)index toIndex:(NSInteger)tIndex cache:(NSMutableDictionary *)cache {
    BOOL dir = tIndex > index;
    for (NSInteger i = index; i != tIndex; dir ? i++ : i--) {
        [self see_exchangeIndex:i withIndex:dir ? (i + 1) : (i - 1) cache:cache];
    }
}

reload

- (void)reloadSection:(NSInteger)section {
    [self see_reloadIndex:section cache:self.heightV];
    [self see_reloadIndex:section cache:self.heightH];
}

- (void)reloadRow:(NSInteger)row inSection:(NSInteger)section {
    [self see_reloadIndex:row cache:[self.heightV objectForKey:key(section)]];
    [self see_reloadIndex:row cache:[self.heightH objectForKey:key(section)]];
}

- (void)reloadAll {
    [self see_reloadIndex:NSIntegerMax cache:self.heightV];
    [self see_reloadIndex:NSIntegerMax cache:self.heightH];
}

- (void)see_reloadIndex:(NSInteger)index cache:(NSMutableDictionary *)cache {
    if (index == NSIntegerMax) {//删除全部缓存
        [cache removeAllObjects];
        setCurrentIndex(cache, -1);
    }
    else {//删除对应index的缓存并重新设置section/row最大值
        [cache removeObjectForKey:key(index)];
        NSArray * keys = [cache.allKeys sortedArrayUsingComparator:^NSComparisonResult(NSString *  _Nonnull obj1, NSString *  _Nonnull obj2) {
            if (obj1.integerValue > obj2.integerValue) {
                return NSOrderedDescending;
            }
            else if (obj1.integerValue < obj2.integerValue) {
                return NSOrderedSame;
            }
            else {
                return NSOrderedSame;
            }
        }];
        setCurrentIndex(cache, ((NSString *)keys.lastObject).integerValue);
    }
}

二、UITableView分类

为了方便集成减少对现有代码的改动我们选用category为UITableView类添加一个方法用于返回高度。

- (CGFloat)heightForCellWithIdentifier:(NSString *)identifier indexPath:(NSIndexPath *)indexPath configuration:(void (^)(UITableViewCell *))configuration {
    CGFloat height = 0;
    if (indexPath && identifier.length != 0) {
        //查找缓存
        height = [self.heightCache heightWithSection:indexPath.section row:indexPath.row];
        //没有缓存则计算
        if (height == CGFLOAT_MAX) {
            height = [self see_calculateForCellWithIdentifier:identifier configuration:configuration];
            //存入缓存
            [self.heightCache setHeight:height section:indexPath.section row:indexPath.row];
        }
    }
    return height;
}

identifier:在实际开发中,TableView中可能会注册几种不同类型的Cell用于展示,不同类型的Cell使用identifier区分,而我们在计算高度时需要使用相同类型的Cell,因此我们需要外界传入对应行要展示的Cell的identifier。
indexPath:用于查找对应的缓存值。
configuration:当没有查找到对应indexPath的缓存时需要计算高度,这时我们根据identifier从Cell缓存中获取到对应类型的Cell并将Cell返回给外界,由外界对其进行赋值。只有在外界对Cell赋值之后才能计算Cell高度。

每当第一次查找某一行的高度时缓存中一定是没有记录的,这时候我们就需要计算这一行的高度并存入缓存。

高度计算
//计算高度
- (CGFloat)see_calculateForCellWithIdentifier:(NSString *)identifier configuration:(void(^)(UITableViewCell * cell))configuration {
    //根据identifier获取cell
    UITableViewCell * cell = [self see_cellWithidentifier:identifier];
    if (cell)
        configuration(cell);
    else
        return 0;
    CGFloat width = self.bounds.size.width;
    //根据辅助视图校正width
    if (cell.accessoryView) {
        width -= cell.accessoryView.bounds.size.width + 16;
    }
    else
    {
        static const CGFloat accessoryWidth[] = {
            [UITableViewCellAccessoryNone] = 0,
            [UITableViewCellAccessoryDisclosureIndicator] = 34,
            [UITableViewCellAccessoryDetailDisclosureButton] = 68,
            [UITableViewCellAccessoryCheckmark] = 40,
            [UITableViewCellAccessoryDetailButton] = 48
        };
        width -= accessoryWidth[cell.accessoryType];
    }
    CGFloat height = 0;
    //使用autoLayout计算
    if (width > 0) {
        BOOL autoresizing = cell.contentView.translatesAutoresizingMaskIntoConstraints; cell.contentView.translatesAutoresizingMaskIntoConstraints = NO;
        //为cell的contentView添加宽度约束
        NSLayoutConstraint * constraint = [NSLayoutConstraint constraintWithItem:cell.contentView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1 constant:width];
        [cell.contentView addConstraint:constraint];
        height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
        [cell.contentView removeConstraint:constraint];
        cell.contentView.translatesAutoresizingMaskIntoConstraints = autoresizing;
    }
    //如果使用autoLayout计算失败则使用sizeThatFits
    if (height == 0) {
        height = [cell sizeThatFits:CGSizeMake(width, CGFLOAT_MAX)].height;
    }
    //如果使用sizeThatFits计算失败则返回默认
    if (height == 0) {
        height = 44;
    }
    if (self.separatorStyle != UITableViewCellSeparatorStyleNone) {//如果不为无分割线模式则添加分割线高度
        height += 1.0 /[UIScreen mainScreen].scale;
    }
    return height;
}

//返回用于计算高度的cell
- (__kindof UITableViewCell *)see_cellWithidentifier:(NSString *)identifier {
    //从cell缓存中使用指定identifier获取cell
    NSMutableDictionary * cellCache = objc_getAssociatedObject(self, cellCacheKey);
    if (cellCache == nil) {
        cellCache = [NSMutableDictionary dictionary];
        objc_setAssociatedObject(self, cellCacheKey, cellCache, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    UITableViewCell * cell = [cellCache objectForKey:identifier];
    //如果cell缓存中没有 则从重用池中获取 并加入cell缓存
    if (!cell) {
        ////从重用池中取一个cell用来计算,必须以本方式从重用池中取,若以indexPath方式取由于-heightForRowAtIndexPath方法会造成循环
        cell = [self dequeueReusableCellWithIdentifier:identifier];
        [cellCache setObject:cell forKey:identifier];
    }
    return cell;
}

此处需要注意的是我们的Cell如果是使用autoLayout布局,那么在垂直方向的约束要能够从父视图的顶部延续到父视图的底部。


Cell约束

每当我们对TableView进行insert、delete、move、reload时只需要调用缓存管理类的相应方法对缓存进行相同的操作即可。我们可以使用method swizling对TableView的相应方法进行替换,在替换的方法中对缓存进行操作:

+ (void)cacheEnabled:(BOOL)enabled {
    if (tableViewCacheEnabled == enabled) return;
    tableViewCacheEnabled = enabled;
    //move
    Method m1 = class_getInstanceMethod([self class], @selector(moveRowAtIndexPath:toIndexPath:));
    Method m2 = class_getInstanceMethod([self class], @selector(see_moveRowAtIndexPath:toIndexPath:));
    method_exchangeImplementations(m1, m2);
    m1 = class_getInstanceMethod([self class], @selector(moveSection:toSection:));
    m2 = class_getInstanceMethod([self class], @selector(see_moveSection:toSection:));
    method_exchangeImplementations(m1, m2);
    //delete
    m1 = class_getInstanceMethod([self class], @selector(deleteSections:withRowAnimation:));
    m2 = class_getInstanceMethod([self class], @selector(see_deleteSections:withRowAnimation:));
    method_exchangeImplementations(m1, m2);
    m1 = class_getInstanceMethod([self class], @selector(deleteRowsAtIndexPaths:withRowAnimation:));
    m2 = class_getInstanceMethod([self class], @selector(see_deleteRowsAtIndexPaths:withRowAnimation:));
    method_exchangeImplementations(m1, m2);
    //insert
    m1 = class_getInstanceMethod([self class], @selector(insertRowsAtIndexPaths:withRowAnimation:));
    m2 = class_getInstanceMethod([self class], @selector(see_insertRowsAtIndexPaths:withRowAnimation:));
    method_exchangeImplementations(m1, m2);
    m1 = class_getInstanceMethod([self class], @selector(insertSections:withRowAnimation:));
    m2 = class_getInstanceMethod([self class], @selector(see_insertSections:withRowAnimation:));
    method_exchangeImplementations(m1, m2);
    //reload
    m1 = class_getInstanceMethod([self class], @selector(reloadSections:withRowAnimation:));
    m2 = class_getInstanceMethod([self class], @selector(see_reloadSections:withRowAnimation:));
    method_exchangeImplementations(m1, m2);
    m1 = class_getInstanceMethod([self class], @selector(reloadRowsAtIndexPaths:withRowAnimation:));
    m2 = class_getInstanceMethod([self class], @selector(see_reloadRowsAtIndexPaths:withRowAnimation:));
    method_exchangeImplementations(m1, m2);
    m1 = class_getInstanceMethod([self class], @selector(reloadData));
    m2 = class_getInstanceMethod([self class], @selector(see_reloadData));
    method_exchangeImplementations(m1, m2);
}

- (void)see_insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation {
    [sections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) {
        [self.heightCache insertSection:idx];
    }];
    [self see_insertSections:sections withRowAnimation:animation];
}

- (void)see_insertRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation {
    //对indexPath进行升序排序
    NSArray * sortResult = [indexPaths sortedArrayUsingComparator:^NSComparisonResult(NSIndexPath *  _Nonnull obj1, NSIndexPath *  _Nonnull obj2) {
        if (obj1.section == obj2.section) {
            if (obj1.row == obj2.row) {
                return NSOrderedSame;
            }
            else if (obj1.row > obj2.row) {
                return NSOrderedDescending;
            }
            else {
                return NSOrderedAscending;
            }
        }
        else if (obj1.section > obj2.section) {
            return NSOrderedDescending;
        }
        else {
            return NSOrderedAscending;
        }
    }];
    [sortResult enumerateObjectsUsingBlock:^(NSIndexPath * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        [self.heightCache insertRow:obj.row inSection:obj.section];
    }];
    [self see_insertRowsAtIndexPaths:indexPaths withRowAnimation:animation];
}
- (void)see_deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation {
    //倒序删除对应section
    [sections enumerateIndexesWithOptions:NSEnumerationReverse usingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) {
        [self.heightCache deleteSection:idx];
    }];
    [self see_deleteSections:sections withRowAnimation:animation];
}

- (void)see_deleteRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation {
    //对indexPath进行升序排序
    NSArray * sortResult = [indexPaths sortedArrayUsingComparator:^NSComparisonResult(NSIndexPath *  _Nonnull obj1, NSIndexPath *  _Nonnull obj2) {
        if (obj1.section == obj2.section) {
            if (obj1.row == obj2.row) {
                return NSOrderedSame;
            }
            else if (obj1.row > obj2.row) {
                return NSOrderedDescending;
            }
            else {
                return NSOrderedAscending;
            }
        }
        else if (obj1.section > obj2.section) {
            return NSOrderedDescending;
        }
        else {
            return NSOrderedAscending;
        }
    }];
    //倒序删除对应indexPath数据
    [sortResult enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(NSIndexPath *  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        [self.heightCache deleteRow:obj.row inSection:obj.section];
    }];
    [self see_deleteRowsAtIndexPaths:indexPaths withRowAnimation:animation];
}

- (void)see_reloadRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation {
    [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        [self.heightCache reloadRow:obj.row inSection:obj.section];
    }];
    [self see_reloadRowsAtIndexPaths:indexPaths withRowAnimation:animation];
}

- (void)see_reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation {
    [sections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) {
        [self.heightCache reloadSection:idx];
    }];
    [self see_reloadSections:sections withRowAnimation:animation];
}

- (void)see_reloadData {
    [self.heightCache reloadAll];
    [self see_reloadData];
}

- (void)see_moveSection:(NSInteger)section toSection:(NSInteger)newSection {
    [self.heightCache moveSection:section toSection:newSection];
    [self see_moveSection:section toSection:newSection];
}

- (void)see_moveRowAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath {
    [self.heightCache moveRow:indexPath.row inSection:indexPath.section toRow:newIndexPath.row inSection:newIndexPath.section];
    [self see_moveRowAtIndexPath:indexPath toIndexPath:newIndexPath];
}

这样当我们使用时只需要调用 + (void)cacheEnabled:(BOOL)enabled; 方法并且将enabled参数设置为YES,然后在 - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath 方法中调用 - (CGFloat)heightForCellWithIdentifier:(NSString *)identifier indexPath:(NSIndexPath *)indexPath configuration:(void(^)(__kindof UITableViewCell * cell))configuration; 并且在block中对Cell进行赋值操作即可

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return [tableView heightForCellWithIdentifier:@"cell" indexPath:indexPath configuration:^(UITableViewCell *cell) {
        [(TableViewCell *)cell configureWithText:self.data[indexPath.section][indexPath.row]];
    }];
}

Demo地址

文中如果有任何错误之处请大家多多指正。


补充 2018-3-27

在runloop空闲时进行行高计算

如果对runloop知识不了解可以先阅读以下两篇文章
深入理解RunLoop
[iOS程序启动与运转]- RunLoop个人小结

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
  kCFRunLoopEntry = (1UL << 0), //进入
  kCFRunLoopBeforeTimers = (1UL << 1), //处理timer
  kCFRunLoopBeforeSources = (1UL << 2), //处理source
  kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
  kCFRunLoopAfterWaiting = (1UL << 6), //即将唤醒
  kCFRunLoopExit = (1UL << 7), //退出
  kCFRunLoopAllActivities = 0x0FFFFFFFU 
};

我们需要监听runloop即将休眠的activity,在回调中调用 -tableView:heightForRowAtIndexPath: 将所有cell高度遍历一遍,在遍历的过程中将所有高度缓存。

添加observer:
//获取当前runloop
CFRunLoopRef runloop = CFRunLoopGetCurrent();
__weak typeof(self) weakSelf = self;
//创建字典 记录当前tableView 以及缓存indexPath
CFMutableDictionaryRef m_dict = CFDictionaryCreateMutable(NULL, 3, NULL, NULL);
NSInteger section = indexPath.section;
NSInteger row = indexPath.row;
CFDictionaryAddValue(m_dict, "section", (const void *)section);
CFDictionaryAddValue(m_dict, "row", (const void *)row);
CFDictionaryAddValue(m_dict, "tableView", (__bridge void *)weakSelf);
//创建context
CFRunLoopObserverContext context = {.info = m_dict};
//创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, 10, &runLoopObserverCallBack, &context);
//添加observer 在runloop空闲时计算高度
CFRunLoopAddObserver(runloop, observer, kCFRunLoopDefaultMode);

在这里笔者选择将添加observer的操作放在 _- (CGFloat)heightForCellWithIdentifier:(NSString *)identifier indexPath:(NSIndexPath *)indexPath configuration:(void(^)(_kindof UITableViewCell * cell))configuration; 中计算cell高度的位置,这样每当我们获取的高度的indexPath在缓存中不存在时,在计算所需行的基础上会进行当前runloop的监听,当runloop即将休眠时对该行之后所有行的高度进行计算缓存。缓存完毕后移除observer。

- (CGFloat)heightForCellWithIdentifier:(NSString *)identifier indexPath:(NSIndexPath *)indexPath configuration:(void (^)(__kindof UITableViewCell *))configuration {
    if (!tableViewCacheEnabled) @throw [NSException exceptionWithName:@"高度返回错误" reason:@"无法再未开启缓存的情况下调用该方法 请使用 + (void)cacheEnabled:(BOOL)enabled 并设置enabled为YES" userInfo:nil];
    CGFloat height = 0;
    if (indexPath && identifier.length != 0) {
        //查找缓存
        height = [self.heightCache heightWithSection:indexPath.section row:indexPath.row];
        //缓存没有则计算
        if (height == CGFLOAT_MAX) {
            height = [self see_calculateForCellWithIdentifier:identifier configuration:configuration];
            [self.heightCache setHeight:height section:indexPath.section row:indexPath.row];
            //添加observer 当runloop即将休眠时计算行高,tableViewRunloopCacheEnabled为YES时说明正在缓存行高,此时我们应该避免再次添加observer到runLoop。(这里是一个大坑)
            if (((NSNumber *)objc_getAssociatedObject(self, tableViewRunloopCacheEnabled)).boolValue == NO){
                objc_setAssociatedObject(self, tableViewRunloopCacheEnabled, @(YES), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
                //获取当前runloop
                CFRunLoopRef runloop = CFRunLoopGetCurrent();
                __weak typeof(self) weakSelf = self;
                //创建字典 记录当前tableView 以及缓存indexPath
                CFMutableDictionaryRef m_dict = CFDictionaryCreateMutable(NULL, 3, NULL, NULL);
                NSInteger section = indexPath.section;
                NSInteger row = indexPath.row;
                CFDictionaryAddValue(m_dict, "section", (const void *)section);
                CFDictionaryAddValue(m_dict, "row", (const void *)row);
                //以上谈到的获取不到tableView的问题可能出在这里,这里我们引用的tableView是使用weak关键字修饰的,因此极端情况下,当observer回调方法正在计算行高时我们的tableView已经被释放了,所以导致回调方法中获取到nil
                CFDictionaryAddValue(m_dict, "tableView", (__bridge void *)weakSelf);
                //创建context
                CFRunLoopObserverContext context = {.info = m_dict};
                //创建observer
                CFRunLoopObserverRef observer = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, 10, &runLoopObserverCallBack, &context);
                //添加observer 在runloop即将休眠时计算高度
                CFRunLoopAddObserver(runloop, observer, kCFRunLoopDefaultMode);
            }
        }
    }
    return height;
}
遍历:
void runLoopObserverCallBack (CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    if (activity == kCFRunLoopBeforeWaiting) {
        CFMutableDictionaryRef m_dict = info;
        //获取tableView
        //注意:这里有可能会获取不到tableView导致崩溃,修复方法在下文
        UITableView * tableView = (__bridge UITableView *)CFDictionaryGetValue(m_dict, "tableView");
        //获取当前需要计算的indexPath
        NSInteger section = (NSInteger)CFDictionaryGetValue(m_dict, "section");
        NSInteger row = (NSInteger)CFDictionaryGetValue(m_dict, "row");
        if (tableView.dataSource == nil) return;
        if (section < [tableView.dataSource numberOfSectionsInTableView:tableView]) {
            if (row < [tableView.dataSource tableView:tableView numberOfRowsInSection:section]) {
                //获取高度并缓存
                [tableView.delegate tableView:tableView heightForRowAtIndexPath:[NSIndexPath indexPathForRow:row inSection:section]];
                row ++;
            }
            else {
                row = 0;
                section ++;
            }
            //设置下一次需要获取的高度indexPath
            CFDictionarySetValue(m_dict, "section", (const void *)section);
            CFDictionarySetValue(m_dict, "row", (const void *)row);
            //唤醒runloop  目的:在每缓存一行高度之后唤醒runloop处理timer、source等事件防止遍历过程中阻塞主线程
            CFRunLoopWakeUp(CFRunLoopGetCurrent());
        }
        else {
            //高度缓存全部完成 将observer移除
            CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
            objc_setAssociatedObject(tableView, tableViewRunloopCacheEnabled, @(NO), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
    }
}

以上通过修改section,row的值并且不断唤醒runloop使runloop进入即将休眠状态而触发observer回调来代替直接使用for循环遍历,防止在使用for循环遍历的过程中阻塞主线程造成界面卡死。

bug! 2018-10-14

近期在使用该工具类的过程中发现,极端情况下在runloop的observer回调方法中获取不到TableView导致崩溃。以下是修正方法:

如果各位有仔细阅读上面的代码的话会发现我已经将崩溃的地方以及导致崩溃的原因做了注释,在此就不在废话了,让我们直接来看解决方案。

笔者首先想到的是,我们是否可以在回调中增加对tableView是否为NULL的判断,如果为NULL则我们直接唤醒runLoop,重新获取,为了防止卡死runLoop我们还需要在失败次数超过某一数值时放弃缓存高度,这个方案听起来不错,但是我们遇到了其他问题。

当我们放弃缓存高度时,需要将tableView中tableViewRunloopCacheEnabled的值设置为NO,以允许tableViewCell发生变化时能够重新唤起runLoop进行计算,但是我们之所以会放弃就是因为获取不到tableView,而这里我们又想要设置tableView中tableViewRunloopCacheEnabled的值,好像这里进入了一个死循环,完全没有让我们操作的空间。(当时还是太年轻啊!)

怎么办呢?

既然这样,那只能考虑有没有别的不借助tableView的方式来限制observer的添加。

以上我们使用到的只有runLoopObserver和tableView,既然tableView不可以那就只能寄希望于runLoopObserver了。

说道这里也许我们需要翻翻runLoop的文档,毕竟runLoop在我们日常的编码过程中可能一辈子也写不了100行很多方法根本不知道。

but!

先别急着 command + shift + 0 我们这里使用另一种快捷的方法,毕竟比较懒(我说的是我自己)能不看文档就不看文档,英语比较差真的会看到怀疑人生。

以下是笔者解决这种问题的一般操作,请谨慎模仿!

这里不得不说Xcode的代码提示功能,真的是大赞,每当这个时候笔者首先做的就是敲下相关的单词,是的你没有看错!

既然要通过runLoop解决,那就大胆的敲下runLoopObserver吧。

image.png

大声的告诉我,你发现了什么?
没有错 CFRunLoopObserverIsValid ,我们发现了判断observer是否有效的方法。这比翻文档快多了。(手动滑稽)

有了判断observer是否有效的方法接下来的操作就很简单了,每次添加完observer把observer存储在tableView中,下一次添加之前先判断之前添加的observer是否有效,如果observer有效就不添加。

到这里bug修复完成!

以下是新的代码:

- (CGFloat)heightForCellWithIdentifier:(NSString *)identifier indexPath:(NSIndexPath *)indexPath configuration:(void (^)(__kindof UITableViewCell *))configuration {
    if (!tableViewCacheEnabled) @throw [NSException exceptionWithName:@"高度返回错误" reason:@"无法再未开启缓存的情况下调用该方法 请使用 + (void)cacheEnabled:(BOOL)enabled 并设置enabled为YES" userInfo:nil];
    CGFloat height = 0;
    if (indexPath && identifier.length != 0) {
        //查找缓存
        height = [self.heightCache heightWithSection:indexPath.section row:indexPath.row];
        //缓存没有则计算
        if (height == CGFLOAT_MAX) {
            height = [self see_calculateForCellWithIdentifier:identifier configuration:configuration];
            [self.heightCache setHeight:height section:indexPath.section row:indexPath.row];
            //添加observer 当runloop即将休眠时计算行高
            id observer = objc_getAssociatedObject(self, tableViewRunloopObserver);
            if (!observer || !CFRunLoopObserverIsValid(((__bridge CFRunLoopObserverRef)observer))) {
                //获取当前runloop
                CFRunLoopRef runloop = CFRunLoopGetCurrent();
                __weak typeof(self) weakSelf = self;
                //创建字典 记录当前tableView 以及缓存indexPath
                CFMutableDictionaryRef m_dict = CFDictionaryCreateMutable(NULL, 3, NULL, NULL);
                NSInteger section = indexPath.section;
                NSInteger row = indexPath.row;
                CFDictionaryAddValue(m_dict, "section", (const void *)section);
                CFDictionaryAddValue(m_dict, "row", (const void *)row);
                CFDictionaryAddValue(m_dict, "tableView", (__bridge void *)weakSelf);
                //创建context
                CFRunLoopObserverContext context = {.info = m_dict};
                //创建observer
                CFRunLoopObserverRef observer = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, 10, &runLoopObserverCallBack, &context);
                objc_setAssociatedObject(self, tableViewRunloopObserver, (__bridge id)observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
                //添加observer 在runloop空闲时计算高度
                CFRunLoopAddObserver(runloop, observer, kCFRunLoopDefaultMode);
            }
        }
    }
    return height;
}

void runLoopObserverCallBack (CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    if (activity == kCFRunLoopBeforeWaiting) {
        static int failCount = 0;
        CFMutableDictionaryRef m_dict = info;
        //获取tableView
        UITableView * tableView;
        const void * table = CFDictionaryGetValue(m_dict, "tableView");
        if (table != NULL) {
            tableView = (__bridge UITableView *)table;
        }
        else {
            if (failCount > 10) {
                CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
                failCount = 0;
            }
            else {
                
                failCount += 1;
                CFRunLoopWakeUp(CFRunLoopGetCurrent());
            }
        }
        //获取当前需要计算的indexPath
        NSInteger section = (NSInteger)CFDictionaryGetValue(m_dict, "section");
        NSInteger row = (NSInteger)CFDictionaryGetValue(m_dict, "row");
        if (tableView.dataSource == nil) return;
        if (section < [tableView.dataSource numberOfSectionsInTableView:tableView]) {
            if (row < [tableView.dataSource tableView:tableView numberOfRowsInSection:section]) {
                //获取高度并缓存
                [tableView.delegate tableView:tableView heightForRowAtIndexPath:[NSIndexPath indexPathForRow:row inSection:section]];
                row ++;
            }
            else {
                row = 0;
                section ++;
            }
            //设置下一次需要获取的高度indexPath
            CFDictionarySetValue(m_dict, "section", (const void *)section);
            CFDictionarySetValue(m_dict, "row", (const void *)row);
            //唤醒runloop  目的:在每缓存一行高度之后唤醒runloop处理timer、source等事件防止遍历过程中阻塞主线程
            CFRunLoopWakeUp(CFRunLoopGetCurrent());
        }
        else {
            //高度缓存全部完成 将observer移除
            CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
        }
    }
}

近三个多月一直忙于开发自己自小项目,没有时间更新文章,最近项目马上就要上线了,上线后简书恢复正常更新,另外我也会将这三个月中遇到到问题以及一些新学的技术点发上来。

另外Swift和OC混编真的蛋疼!!! 没事不要随便尝试,像我这种比较懒的又不想重写一份Swift的代码,所以就直接把OC中的代码拉进Swift项目,结果导致配置项目配了很久,OC调Swift,Swift调OC,搞得我晕头转向的,原先一头飘逸的长发现在已经变成了三毫米👀

好吧,我认输 2019-07-01

自从上次添加了判断observer是否有效的操作之后很开心的使用了大半年都没有出过问题,就在今天又TM崩溃了,还是老问题。

在极端极端情况下添加observer之后回调之前tableView销毁了。这TM就尴尬了,依然是之前的原因崩溃。

左思右想,挠断了十几根头发之后终于...我找到了究极解决方法。

- (void)dealloc {
  id observer = objc_getAssociatedObject(self, tableViewRunloopObserver);
  if (observer && CFRunLoopObserverIsValid(((__bridge CFRunLoopObserverRef)observer))) {
    CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), (__bridge CFRunLoopObserverRef)observer, kCFRunLoopDefaultMode);
  }
}

在tableView销毁时检查是否注册过observer,如果注册过手动移除。

以上应该就是终极版本了吧。

如果大家在使用的过程中又因为相同的原因崩溃,请留言。

如果是其他原因崩溃请附上崩溃代码以及崩溃信息。

参考

TableView优化之高度缓存
深入理解RunLoop
[iOS程序启动与运转]- RunLoop个人小结

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

推荐阅读更多精彩内容