UITableView緩存行高

UITableView是我們?cè)谌粘i_(kāi)發(fā)中使用頻率比較高的控件,TableView中不同的Cell可能因?yàn)槌尸F(xiàn)內(nèi)容的不同而擁有不同的高度,如果在每一次展現(xiàn)Cell時(shí)都去計(jì)算Cell的高度可能會(huì)造成TableView在滾動(dòng)過(guò)程中不必要的卡頓,而實(shí)際上同一行Cell展現(xiàn)在界面上時(shí)如果它的內(nèi)容沒(méi)有發(fā)生改變高度也是不會(huì)發(fā)生改變的,如果每一次都對(duì)高度進(jìn)行重新計(jì)算勢(shì)必會(huì)造成系統(tǒng)資源的浪費(fèi)導(dǎo)致不必要的界面卡頓。如果我們能夠?qū)ell高度進(jìn)行緩存則可以很好的避免這一問(wèn)題。


實(shí)現(xiàn)思路

TableView中每一個(gè)Cell的高度都是由其顯示的數(shù)據(jù)來(lái)決定的,因此我們可以準(zhǔn)備一個(gè)Cell,每當(dāng)我們需要某一行的高度時(shí),我們就對(duì)這個(gè)Cell賦值以獲取高度并存入緩存,當(dāng)我們?cè)俅涡枰@一行的高度時(shí)就可以直接從緩存中獲取。


一、緩存管理類
緩存設(shè)計(jì)

由于TableView可以insertSection、delete、move、reload組/行,想要保持緩存數(shù)據(jù)的準(zhǔn)確,我們必須對(duì)緩存進(jìn)行相應(yīng)的操作,為了方便操作我們可以將緩存設(shè)計(jì)為 NSMutableDictionary <NSString *, NSMutableDictionary <NSString *, NSNumber *> *> * 類型,這樣我們可以很方便的以組或者行為最小單元對(duì)緩存數(shù)據(jù)進(jìn)行操作。

//結(jié)構(gòu)
{
  @"-1":maxSectionIndex,//記錄當(dāng)前緩存中section的最大值 insert、delete、move時(shí)我們需要以此來(lái)對(duì)當(dāng)前緩存中的數(shù)據(jù)進(jìn)行移動(dòng)操作
  section0: {
    @"-1":maxRowIndex, //記錄當(dāng)前組中row的最大值 insert、delete、move時(shí)我們需要以此來(lái)對(duì)當(dāng)前緩存中的數(shù)據(jù)進(jìn)行移動(dòng)操作
    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,
  },
}

此外當(dāng)我們的設(shè)備處于橫屏和豎屏?xí)r同一行的高度也不相同因此我們需要兩個(gè)字典來(lái)分別記錄橫屏狀態(tài)下的行高和豎屏狀態(tài)下的行高,當(dāng)我們對(duì)緩存進(jìn)行insert、delete、move、reload操作時(shí)需要同時(shí)對(duì)兩個(gè)字典進(jìn)行操作以保證橫豎屏狀態(tài)下的緩存正確性。

獲取緩存值
- (CGFloat)heightWithSection:(NSInteger)section row:(NSInteger)row {
    //獲取當(dāng)前屏幕狀態(tài)下的緩存高度
    NSNumber * height = cacheValue(self.currentHeight,section, row);
    if (height) {
        return height.CGFloatValue;
    }
    else {//不存在記錄返回CGFLOAT_MAX通知TableView計(jì)算高度
        return CGFLOAT_MAX;
    }
}
設(shè)置緩存值
- (void)setHeight:(CGFloat)height section:(NSInteger)section row:(NSInteger)row {
    //取對(duì)應(yīng)組的緩存字典
    NSMutableDictionary * sectionCache = [self.currentHeight objectForKey:key(section)];
    //如果沒(méi)有取到則初始化
    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 {
    //創(chuàng)建組
    NSMutableDictionary * newSection = [NSMutableDictionary dictionaryWithObjectsAndKeys:@(-1),@"-1", nil];
    [self.currentHeight setObject:newSection forKey:key(section)];
    //如果組號(hào)大于當(dāng)前記錄的最大值 則修改最大值
    NSInteger sectionLastIndex = currentIndex(self.currentHeight);
    if (sectionLastIndex < section) {
        setCurrentIndex(self.currentHeight, section);
    }
    return newSection;
}

- (void)see_initRow:(NSInteger)row cache:(NSMutableDictionary *)cache {
    //如果行號(hào)大于當(dāng)前記錄的最大值 則修改最大值
    NSInteger rowLastIndex = currentIndex(cache);
    if (rowLastIndex < row) {
        setCurrentIndex(cache, row);
    }
}
針對(duì)緩存字典的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小于當(dāng)前緩存坐標(biāo) 則 坐標(biāo)+1  如果大于則等待設(shè)置緩存值初始化組時(shí)設(shè)置
    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的數(shù)據(jù)移動(dòng)到末尾
    [self see_moveIndex:index toIndex:lastIndex cache:cache];
    //將末尾數(shù)據(jù)刪除
    [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) {
        //組內(nèi)交換
        [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.將目標(biāo)緩存刪除
        [self deleteRow:row inSection:section];
        //2.在目標(biāo)組中插入一行
        [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 {//刪除對(duì)應(yīng)index的緩存并重新設(shè)置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分類

為了方便集成減少對(duì)現(xiàn)有代碼的改動(dòng)我們選用category為UITableView類添加一個(gè)方法用于返回高度。

- (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];
        //沒(méi)有緩存則計(jì)算
        if (height == CGFLOAT_MAX) {
            height = [self see_calculateForCellWithIdentifier:identifier configuration:configuration];
            //存入緩存
            [self.heightCache setHeight:height section:indexPath.section row:indexPath.row];
        }
    }
    return height;
}

identifier:在實(shí)際開(kāi)發(fā)中,TableView中可能會(huì)注冊(cè)幾種不同類型的Cell用于展示,不同類型的Cell使用identifier區(qū)分,而我們?cè)谟?jì)算高度時(shí)需要使用相同類型的Cell,因此我們需要外界傳入對(duì)應(yīng)行要展示的Cell的identifier。
indexPath:用于查找對(duì)應(yīng)的緩存值。
configuration:當(dāng)沒(méi)有查找到對(duì)應(yīng)indexPath的緩存時(shí)需要計(jì)算高度,這時(shí)我們根據(jù)identifier從Cell緩存中獲取到對(duì)應(yīng)類型的Cell并將Cell返回給外界,由外界對(duì)其進(jìn)行賦值。只有在外界對(duì)Cell賦值之后才能計(jì)算Cell高度。

每當(dāng)?shù)谝淮尾檎夷骋恍械母叨葧r(shí)緩存中一定是沒(méi)有記錄的,這時(shí)候我們就需要計(jì)算這一行的高度并存入緩存。

高度計(jì)算
//計(jì)算高度
- (CGFloat)see_calculateForCellWithIdentifier:(NSString *)identifier configuration:(void(^)(UITableViewCell * cell))configuration {
    //根據(jù)identifier獲取cell
    UITableViewCell * cell = [self see_cellWithidentifier:identifier];
    if (cell)
        configuration(cell);
    else
        return 0;
    CGFloat width = self.bounds.size.width;
    //根據(jù)輔助視圖校正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計(jì)算
    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計(jì)算失敗則使用sizeThatFits
    if (height == 0) {
        height = [cell sizeThatFits:CGSizeMake(width, CGFLOAT_MAX)].height;
    }
    //如果使用sizeThatFits計(jì)算失敗則返回默認(rèn)
    if (height == 0) {
        height = 44;
    }
    if (self.separatorStyle != UITableViewCellSeparatorStyleNone) {//如果不為無(wú)分割線模式則添加分割線高度
        height += 1.0 /[UIScreen mainScreen].scale;
    }
    return height;
}

//返回用于計(jì)算高度的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緩存中沒(méi)有 則從重用池中獲取 并加入cell緩存
    if (!cell) {
        ////從重用池中取一個(gè)cell用來(lái)計(jì)算,必須以本方式從重用池中取,若以indexPath方式取由于-heightForRowAtIndexPath方法會(huì)造成循環(huán)
        cell = [self dequeueReusableCellWithIdentifier:identifier];
        [cellCache setObject:cell forKey:identifier];
    }
    return cell;
}

此處需要注意的是我們的Cell如果是使用autoLayout布局,那么在垂直方向的約束要能夠從父視圖的頂部延續(xù)到父視圖的底部。


Cell約束

每當(dāng)我們對(duì)TableView進(jìn)行insert、delete、move、reload時(shí)只需要調(diào)用緩存管理類的相應(yīng)方法對(duì)緩存進(jìn)行相同的操作即可。我們可以使用method swizling對(duì)TableView的相應(yīng)方法進(jìn)行替換,在替換的方法中對(duì)緩存進(jìn)行操作:

+ (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 {
    //對(duì)indexPath進(jìn)行升序排序
    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 {
    //倒序刪除對(duì)應(yīng)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 {
    //對(duì)indexPath進(jìn)行升序排序
    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;
        }
    }];
    //倒序刪除對(duì)應(yīng)indexPath數(shù)據(jù)
    [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];
}

這樣當(dāng)我們使用時(shí)只需要調(diào)用 + (void)cacheEnabled:(BOOL)enabled; 方法并且將enabled參數(shù)設(shè)置為YES,然后在 - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath 方法中調(diào)用 - (CGFloat)heightForCellWithIdentifier:(NSString *)identifier indexPath:(NSIndexPath *)indexPath configuration:(void(^)(__kindof UITableViewCell * cell))configuration; 并且在block中對(duì)Cell進(jìn)行賦值操作即可

- (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地址

文中如果有任何錯(cuò)誤之處請(qǐng)大家多多指正。


補(bǔ)充 2018-3-27

在runloop空閑時(shí)進(jìn)行行高計(jì)算

如果對(duì)runloop知識(shí)不了解可以先閱讀以下兩篇文章
深入理解RunLoop
[iOS程序啟動(dòng)與運(yùn)轉(zhuǎn)]- RunLoop個(gè)人小結(jié)

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
  kCFRunLoopEntry = (1UL << 0), //進(jìn)入
  kCFRunLoopBeforeTimers = (1UL << 1), //處理timer
  kCFRunLoopBeforeSources = (1UL << 2), //處理source
  kCFRunLoopBeforeWaiting = (1UL << 5), //即將進(jìn)入休眠
  kCFRunLoopAfterWaiting = (1UL << 6), //即將喚醒
  kCFRunLoopExit = (1UL << 7), //退出
  kCFRunLoopAllActivities = 0x0FFFFFFFU 
};

我們需要監(jiān)聽(tīng)runloop即將休眠的activity,在回調(diào)中調(diào)用 -tableView:heightForRowAtIndexPath: 將所有cell高度遍歷一遍,在遍歷的過(guò)程中將所有高度緩存。

添加observer:
//獲取當(dāng)前runloop
CFRunLoopRef runloop = CFRunLoopGetCurrent();
__weak typeof(self) weakSelf = self;
//創(chuàng)建字典 記錄當(dāng)前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);
//創(chuàng)建context
CFRunLoopObserverContext context = {.info = m_dict};
//創(chuàng)建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, 10, &runLoopObserverCallBack, &context);
//添加observer 在runloop空閑時(shí)計(jì)算高度
CFRunLoopAddObserver(runloop, observer, kCFRunLoopDefaultMode);

在這里筆者選擇將添加observer的操作放在 _- (CGFloat)heightForCellWithIdentifier:(NSString *)identifier indexPath:(NSIndexPath *)indexPath configuration:(void(^)(_kindof UITableViewCell * cell))configuration; 中計(jì)算cell高度的位置,這樣每當(dāng)我們獲取的高度的indexPath在緩存中不存在時(shí),在計(jì)算所需行的基礎(chǔ)上會(huì)進(jìn)行當(dāng)前runloop的監(jiān)聽(tīng),當(dāng)runloop即將休眠時(shí)對(duì)該行之后所有行的高度進(jìn)行計(jì)算緩存。緩存完畢后移除observer。

- (CGFloat)heightForCellWithIdentifier:(NSString *)identifier indexPath:(NSIndexPath *)indexPath configuration:(void (^)(__kindof UITableViewCell *))configuration {
    if (!tableViewCacheEnabled) @throw [NSException exceptionWithName:@"高度返回錯(cuò)誤" reason:@"無(wú)法再未開(kāi)啟緩存的情況下調(diào)用該方法 請(qǐng)使用 + (void)cacheEnabled:(BOOL)enabled 并設(shè)置enabled為YES" userInfo:nil];
    CGFloat height = 0;
    if (indexPath && identifier.length != 0) {
        //查找緩存
        height = [self.heightCache heightWithSection:indexPath.section row:indexPath.row];
        //緩存沒(méi)有則計(jì)算
        if (height == CGFLOAT_MAX) {
            height = [self see_calculateForCellWithIdentifier:identifier configuration:configuration];
            [self.heightCache setHeight:height section:indexPath.section row:indexPath.row];
            //添加observer 當(dāng)runloop即將休眠時(shí)計(jì)算行高,tableViewRunloopCacheEnabled為YES時(shí)說(shuō)明正在緩存行高,此時(shí)我們應(yīng)該避免再次添加observer到runLoop。(這里是一個(gè)大坑)
            if (((NSNumber *)objc_getAssociatedObject(self, tableViewRunloopCacheEnabled)).boolValue == NO){
                objc_setAssociatedObject(self, tableViewRunloopCacheEnabled, @(YES), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
                //獲取當(dāng)前runloop
                CFRunLoopRef runloop = CFRunLoopGetCurrent();
                __weak typeof(self) weakSelf = self;
                //創(chuàng)建字典 記錄當(dāng)前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的問(wèn)題可能出在這里,這里我們引用的tableView是使用weak關(guān)鍵字修飾的,因此極端情況下,當(dāng)observer回調(diào)方法正在計(jì)算行高時(shí)我們的tableView已經(jīng)被釋放了,所以導(dǎo)致回調(diào)方法中獲取到nil
                CFDictionaryAddValue(m_dict, "tableView", (__bridge void *)weakSelf);
                //創(chuàng)建context
                CFRunLoopObserverContext context = {.info = m_dict};
                //創(chuàng)建observer
                CFRunLoopObserverRef observer = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, 10, &runLoopObserverCallBack, &context);
                //添加observer 在runloop即將休眠時(shí)計(jì)算高度
                CFRunLoopAddObserver(runloop, observer, kCFRunLoopDefaultMode);
            }
        }
    }
    return height;
}
遍歷:
void runLoopObserverCallBack (CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    if (activity == kCFRunLoopBeforeWaiting) {
        CFMutableDictionaryRef m_dict = info;
        //獲取tableView
        //注意:這里有可能會(huì)獲取不到tableView導(dǎo)致崩潰,修復(fù)方法在下文
        UITableView * tableView = (__bridge UITableView *)CFDictionaryGetValue(m_dict, "tableView");
        //獲取當(dāng)前需要計(jì)算的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 ++;
            }
            //設(shè)置下一次需要獲取的高度indexPath
            CFDictionarySetValue(m_dict, "section", (const void *)section);
            CFDictionarySetValue(m_dict, "row", (const void *)row);
            //喚醒runloop  目的:在每緩存一行高度之后喚醒runloop處理timer、source等事件防止遍歷過(guò)程中阻塞主線程
            CFRunLoopWakeUp(CFRunLoopGetCurrent());
        }
        else {
            //高度緩存全部完成 將observer移除
            CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
            objc_setAssociatedObject(tableView, tableViewRunloopCacheEnabled, @(NO), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        }
    }
}

以上通過(guò)修改section,row的值并且不斷喚醒runloop使runloop進(jìn)入即將休眠狀態(tài)而觸發(fā)observer回調(diào)來(lái)代替直接使用for循環(huán)遍歷,防止在使用for循環(huán)遍歷的過(guò)程中阻塞主線程造成界面卡死。

bug! 2018-10-14

近期在使用該工具類的過(guò)程中發(fā)現(xiàn),極端情況下在runloop的observer回調(diào)方法中獲取不到TableView導(dǎo)致崩潰。以下是修正方法:

如果各位有仔細(xì)閱讀上面的代碼的話會(huì)發(fā)現(xiàn)我已經(jīng)將崩潰的地方以及導(dǎo)致崩潰的原因做了注釋,在此就不在廢話了,讓我們直接來(lái)看解決方案。

筆者首先想到的是,我們是否可以在回調(diào)中增加對(duì)tableView是否為NULL的判斷,如果為NULL則我們直接喚醒runLoop,重新獲取,為了防止卡死runLoop我們還需要在失敗次數(shù)超過(guò)某一數(shù)值時(shí)放棄緩存高度,這個(gè)方案聽(tīng)起來(lái)不錯(cuò),但是我們遇到了其他問(wèn)題。

當(dāng)我們放棄緩存高度時(shí),需要將tableView中tableViewRunloopCacheEnabled的值設(shè)置為NO,以允許tableViewCell發(fā)生變化時(shí)能夠重新喚起runLoop進(jìn)行計(jì)算,但是我們之所以會(huì)放棄就是因?yàn)楂@取不到tableView,而這里我們又想要設(shè)置tableView中tableViewRunloopCacheEnabled的值,好像這里進(jìn)入了一個(gè)死循環(huán),完全沒(méi)有讓我們操作的空間。(當(dāng)時(shí)還是太年輕啊?。?/p>

怎么辦呢?

既然這樣,那只能考慮有沒(méi)有別的不借助tableView的方式來(lái)限制observer的添加。

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

說(shuō)道這里也許我們需要翻翻runLoop的文檔,畢竟runLoop在我們?nèi)粘5木幋a過(guò)程中可能一輩子也寫(xiě)不了100行很多方法根本不知道。

but!

先別急著 command + shift + 0 我們這里使用另一種快捷的方法,畢竟比較懶(我說(shuō)的是我自己)能不看文檔就不看文檔,英語(yǔ)比較差真的會(huì)看到懷疑人生。

以下是筆者解決這種問(wèn)題的一般操作,請(qǐng)謹(jǐn)慎模仿!

這里不得不說(shuō)Xcode的代碼提示功能,真的是大贊,每當(dāng)這個(gè)時(shí)候筆者首先做的就是敲下相關(guān)的單詞,是的你沒(méi)有看錯(cuò)!

既然要通過(guò)runLoop解決,那就大膽的敲下runLoopObserver吧。

image.png

大聲的告訴我,你發(fā)現(xiàn)了什么?
沒(méi)有錯(cuò) CFRunLoopObserverIsValid ,我們發(fā)現(xiàn)了判斷observer是否有效的方法。這比翻文檔快多了。(手動(dòng)滑稽)

有了判斷observer是否有效的方法接下來(lái)的操作就很簡(jiǎn)單了,每次添加完observer把observer存儲(chǔ)在tableView中,下一次添加之前先判斷之前添加的observer是否有效,如果observer有效就不添加。

到這里bug修復(fù)完成!

以下是新的代碼:

- (CGFloat)heightForCellWithIdentifier:(NSString *)identifier indexPath:(NSIndexPath *)indexPath configuration:(void (^)(__kindof UITableViewCell *))configuration {
    if (!tableViewCacheEnabled) @throw [NSException exceptionWithName:@"高度返回錯(cuò)誤" reason:@"無(wú)法再未開(kāi)啟緩存的情況下調(diào)用該方法 請(qǐng)使用 + (void)cacheEnabled:(BOOL)enabled 并設(shè)置enabled為YES" userInfo:nil];
    CGFloat height = 0;
    if (indexPath && identifier.length != 0) {
        //查找緩存
        height = [self.heightCache heightWithSection:indexPath.section row:indexPath.row];
        //緩存沒(méi)有則計(jì)算
        if (height == CGFLOAT_MAX) {
            height = [self see_calculateForCellWithIdentifier:identifier configuration:configuration];
            [self.heightCache setHeight:height section:indexPath.section row:indexPath.row];
            //添加observer 當(dāng)runloop即將休眠時(shí)計(jì)算行高
            id observer = objc_getAssociatedObject(self, tableViewRunloopObserver);
            if (!observer || !CFRunLoopObserverIsValid(((__bridge CFRunLoopObserverRef)observer))) {
                //獲取當(dāng)前runloop
                CFRunLoopRef runloop = CFRunLoopGetCurrent();
                __weak typeof(self) weakSelf = self;
                //創(chuàng)建字典 記錄當(dāng)前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);
                //創(chuàng)建context
                CFRunLoopObserverContext context = {.info = m_dict};
                //創(chuàng)建observer
                CFRunLoopObserverRef observer = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, 10, &runLoopObserverCallBack, &context);
                objc_setAssociatedObject(self, tableViewRunloopObserver, (__bridge id)observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
                //添加observer 在runloop空閑時(shí)計(jì)算高度
                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());
            }
        }
        //獲取當(dāng)前需要計(jì)算的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 ++;
            }
            //設(shè)置下一次需要獲取的高度indexPath
            CFDictionarySetValue(m_dict, "section", (const void *)section);
            CFDictionarySetValue(m_dict, "row", (const void *)row);
            //喚醒runloop  目的:在每緩存一行高度之后喚醒runloop處理timer、source等事件防止遍歷過(guò)程中阻塞主線程
            CFRunLoopWakeUp(CFRunLoopGetCurrent());
        }
        else {
            //高度緩存全部完成 將observer移除
            CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
        }
    }
}

近三個(gè)多月一直忙于開(kāi)發(fā)自己自小項(xiàng)目,沒(méi)有時(shí)間更新文章,最近項(xiàng)目馬上就要上線了,上線后簡(jiǎn)書(shū)恢復(fù)正常更新,另外我也會(huì)將這三個(gè)月中遇到到問(wèn)題以及一些新學(xué)的技術(shù)點(diǎn)發(fā)上來(lái)。

另外Swift和OC混編真的蛋疼?。。?沒(méi)事不要隨便嘗試,像我這種比較懶的又不想重寫(xiě)一份Swift的代碼,所以就直接把OC中的代碼拉進(jìn)Swift項(xiàng)目,結(jié)果導(dǎo)致配置項(xiàng)目配了很久,OC調(diào)Swift,Swift調(diào)OC,搞得我暈頭轉(zhuǎn)向的,原先一頭飄逸的長(zhǎng)發(fā)現(xiàn)在已經(jīng)變成了三毫米??

好吧,我認(rèn)輸 2019-07-01

自從上次添加了判斷observer是否有效的操作之后很開(kāi)心的使用了大半年都沒(méi)有出過(guò)問(wèn)題,就在今天又TM崩潰了,還是老問(wèn)題。

在極端極端情況下添加observer之后回調(diào)之前tableView銷毀了。這TM就尷尬了,依然是之前的原因崩潰。

左思右想,撓斷了十幾根頭發(fā)之后終于...我找到了究極解決方法。

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

在tableView銷毀時(shí)檢查是否注冊(cè)過(guò)observer,如果注冊(cè)過(guò)手動(dòng)移除。

以上應(yīng)該就是終極版本了吧。

如果大家在使用的過(guò)程中又因?yàn)橄嗤脑虮罎?,?qǐng)留言。

如果是其他原因崩潰請(qǐng)附上崩潰代碼以及崩潰信息。

參考

TableView優(yōu)化之高度緩存
深入理解RunLoop
[iOS程序啟動(dòng)與運(yùn)轉(zhuǎn)]- RunLoop個(gè)人小結(jié)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容