开源库UITableView+FDTemplateLayoutCell学习

摘自:优化UITableViewCell高度计算Swift版优化UITableViewCell高度计算的那些事

 

本文带大家详细探索那篇文章所对应的库(1.2版),这个库就是利用缓存tableviewcell的高度提高滑动的流畅性。

主要是利用Runloop在空闲状态时,后台计算tableviewcell的高度并缓存起来。然后在使用的时候就直接从缓存中去,这里都放在一个数组里存在内存。

对Runloop以及几个mode不懂的可以看sunnyxx blog中的视频 视频可戳 , 文章的话可以看看 深入理解RunLoop、 【iOS程序启动与运转】- RunLoop个人小结

其实就是在kCFRunLoopDefaultMode模式下BeforWaitting状态去执行计算的。

下面来探究源码。首先在UITableView+FDTemplateLayoutCell 下载源码,下载1.2版本。

然后你得到的库就只有两个文件:

.m文件大概只有500行代码。

下面看下作者的视线思路:

1.  创建了一个_FDTemplateLayoutCellHeightCache类,就是管理Cache的一个类,里面有两个属性四个方法。

属性:

  • sections 这个变量就是用来存储缓存的height的一个二维数组。(因为tableview有section和row组成所以必须二维)

  • _FDTemplateLayoutCellHeightCacheAbsentValue 这个是一个静态常量,就是用来标记没有缓存高度的row 。

方法:

  • buildHeightCachesAtIndexPathsIfNeeded:indexPaths
    这个方法传入indexPaths数组来给sections中还没有初始化的元素进行初始化
  • hasCachedHeightAtIndexPath:indexPath 根据下标索引判断是否有缓存(其实就是判断是否等于上面那个静态常量)
  • cacheHeight:height:byIndexPath 根据indexPath给sections赋值。
  • cachedHeightAtIndexPath:indexPath 根据indexPath取值

这个类主要是操作和存储缓存的。这个类的代码如下:

@interface _FDTemplateLayoutCellHeightCache : NSObject
@property (nonatomic, strong) NSMutableArray *sections;  
@end

static CGFloat const _FDTemplateLayoutCellHeightCacheAbsentValue = -1;

@implementation _FDTemplateLayoutCellHeightCache

- (void)buildHeightCachesAtIndexPathsIfNeeded:(NSArray *)indexPaths {
    if (indexPaths.count == 0) {
        return;
    }
    
    if (!self.sections) {
        self.sections = @[].mutableCopy;
    }
    
    // Build every section array or row array which is smaller than given index path.
    [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) {
        for (NSInteger section = 0; section <= indexPath.section; ++section) {
            if (section >= self.sections.count) {
                self.sections[section] = @[].mutableCopy;
            }
        }
        NSMutableArray *rows = self.sections[indexPath.section];
        for (NSInteger row = 0; row <= indexPath.row; ++row) {
            if (row >= rows.count) {
                rows[row] = @(_FDTemplateLayoutCellHeightCacheAbsentValue);
            }
        }
    }];
}

- (BOOL)hasCachedHeightAtIndexPath:(NSIndexPath *)indexPath
{
    [self buildHeightCachesAtIndexPathsIfNeeded:@[indexPath]];
    NSNumber *cachedNumber = self.sections[indexPath.section][indexPath.row];
    return ![cachedNumber isEqualToNumber:@(_FDTemplateLayoutCellHeightCacheAbsentValue)];
}

- (void)cacheHeight:(CGFloat)height byIndexPath:(NSIndexPath *)indexPath
{
    [self buildHeightCachesAtIndexPathsIfNeeded:@[indexPath]];
    self.sections[indexPath.section][indexPath.row] = @(height);
}

- (CGFloat)cachedHeightAtIndexPath:(NSIndexPath *)indexPath
{
    [self buildHeightCachesAtIndexPathsIfNeeded:@[indexPath]];
#if CGFLOAT_IS_DOUBLE
    return [self.sections[indexPath.section][indexPath.row] doubleValue];
#else
    return [self.sections[indexPath.section][indexPath.row] floatValue];
#endif
}

@end

2. 接下来是UITableView的一个扩展UITableView + FDTemplateLayoutCellPrivate

  • 第一个方法fd_templateCellForReuseIdentifier:identifier,这个方法主要是通过你传入的一个identifier(就是复用的id)获取cell。

    第一句是这样的 NSMutableDictionary *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd);

    OC中的 _cmd 代表的就是本方法,objc_getAssociatedObject 获取一个关联对象的属性。

  • 接下来提供了一个方法来获取管理Cache的_FDTemplateLayoutCellHeightCache的对象fd_cellHeightCache。
  • 属性:fd_autoCacheInvalidationEnabled 记录是否自动缓存高度

  • 属性:fd_precacheEnabled

这是一个私有类,下面给出这个类的完整代码:

@interface UITableView (FDTemplateLayoutCellPrivate)

/// Returns a template cell created by reuse identifier, it has to be registered to table view.
/// Lazy getter, and associated to table view.
- (id)fd_templateCellForReuseIdentifier:(NSString *)identifier;

/// A private height cache data structure.
@property (nonatomic, strong, readonly) _FDTemplateLayoutCellHeightCache *fd_cellHeightCache;

/// This is a private switch that I don't think caller should concern.
/// Auto turn on when you use "-fd_heightForCellWithIdentifier:cacheByIndexPath:configuration".
@property (nonatomic, assign) BOOL fd_autoCacheInvalidationEnabled;

/// It helps to improve scroll performance by "pre-cache" height of cells that have not
/// been displayed on screen. These calculation tasks are collected and performed only
/// when "RunLoop" is in "idle" time.
///
/// Auto turn on when you use "-fd_heightForCellWithIdentifier:cacheByIndexPath:configuration".
@property (nonatomic, assign) BOOL fd_precacheEnabled;

/// Debug log controlled by "fd_debugLogEnabled".
- (void)fd_debugLog:(NSString *)message;

@end

@implementation UITableView (FDTemplateLayoutCellPrivate)

- (id)fd_templateCellForReuseIdentifier:(NSString *)identifier
{
    NSAssert(identifier.length > 0, @"Expects a valid identifier - %@", identifier);
    
    NSMutableDictionary *templateCellsByIdentifiers = objc_getAssociatedObject(self, _cmd);
    if (!templateCellsByIdentifiers) {
        templateCellsByIdentifiers = @{}.mutableCopy;
        objc_setAssociatedObject(self, _cmd, templateCellsByIdentifiers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    UITableViewCell *templateCell = templateCellsByIdentifiers[identifier];
    
    if (!templateCell) {
        templateCell = [self dequeueReusableCellWithIdentifier:identifier];
        NSAssert(templateCell != nil, @"Cell must be registered to table view for identifier - %@", identifier);
        templateCell.fd_isTemplateLayoutCell = YES;
        templateCellsByIdentifiers[identifier] = templateCell;
        [self fd_debugLog:[NSString stringWithFormat:@"layout cell created - %@", identifier]];
    }
    
    return templateCell;
}

- (_FDTemplateLayoutCellHeightCache *)fd_cellHeightCache {
    _FDTemplateLayoutCellHeightCache *cache = objc_getAssociatedObject(self, _cmd);
    if (!cache) {
        cache = [_FDTemplateLayoutCellHeightCache new];
        objc_setAssociatedObject(self, _cmd, cache, OBJC_ASSOCIATION_RETAIN);
    }
    return cache;
}

- (BOOL)fd_autoCacheInvalidationEnabled
{
    return [objc_getAssociatedObject(self, _cmd) boolValue];
}

- (void)setFd_autoCacheInvalidationEnabled:(BOOL)enabled
{
    objc_setAssociatedObject(self, @selector(fd_autoCacheInvalidationEnabled), @(enabled), OBJC_ASSOCIATION_RETAIN);
}

- (BOOL)fd_precacheEnabled
{
    return [objc_getAssociatedObject(self, _cmd) boolValue];
}

- (void)setFd_precacheEnabled:(BOOL)precacheEnabled
{
    objc_setAssociatedObject(self, @selector(fd_precacheEnabled), @(precacheEnabled), OBJC_ASSOCIATION_RETAIN);
}

- (void)fd_debugLog:(NSString *)message
{
    if (!self.fd_debugLogEnabled) {
        return;
    }
    NSLog(@"** FDTemplateLayoutCell ** %@", message);
}

@end

3. 下面又是一个分类,(这个是重点计算高度,调用缓存管理方法的分类)UITableView + FDTemplateLayoutCellPrecache

这个里面的方法在他blog中也有提到就是在NSDefaultRunLoopMode下当状态将要进入休眠的时候把计算方法分解成多个RunLoop Source任务(source0) 

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;

这个方法将创建一个 Source 0 任务,分发到指定线程的 RunLoop 中,在给定的 Mode 下执行,若指定的 RunLoop 处于休眠状态,则唤醒它处理事件.

主要逻辑就是先通过遍历所有section和row找到还没有缓存的row,然后加入到待缓存数组 ,创建一个observer去监听Runloop的状态 ,如果空闲了去创建source0任务,执行计算方法并缓存起来。如果预缓存任务完成了就把监听的Observer移除了。

下面给出这个类的代码:

@implementation UITableView (FDTemplateLayoutCellPrecache)

- (void)fd_precacheIfNeeded
{
    if (!self.fd_precacheEnabled) {
        return;
    }
    
    // Delegate could use "rowHeight" rather than implements this method.
    if (![self.delegate respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {
        return;
    }
    
    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    
    // This is a idle mode of RunLoop, when UIScrollView scrolls, it jumps into "UITrackingRunLoopMode"
    // and won't perform any cache task to keep a smooth scroll.
    CFStringRef runLoopMode = kCFRunLoopDefaultMode;
    
    // Collect all index paths to be precached.
    NSMutableArray *mutableIndexPathsToBePrecached = self.fd_allIndexPathsToBePrecached.mutableCopy;
    
    // Setup a observer to get a perfect moment for precaching tasks.
    // We use a "kCFRunLoopBeforeWaiting" state to keep RunLoop has done everything and about to sleep
    // (mach_msg_trap), when all tasks finish, it will remove itself.
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler
    (kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) {
        // Remove observer when all precache tasks are done.
        if (mutableIndexPathsToBePrecached.count == 0) {
            CFRunLoopRemoveObserver(runLoop, observer, runLoopMode);
            return;
        }
        // Pop first index path record as this RunLoop iteration's task.
        NSIndexPath *indexPath = mutableIndexPathsToBePrecached.firstObject;
        [mutableIndexPathsToBePrecached removeObject:indexPath];
        
        // This method creates a "source 0" task in "idle" mode of RunLoop, and will be
        // performed in a future RunLoop iteration only when user is not scrolling.
        [self performSelector:@selector(fd_precacheIndexPathIfNeeded:)
                     onThread:[NSThread mainThread]
                   withObject:indexPath
                waitUntilDone:NO
                        modes:@[NSDefaultRunLoopMode]];
    });
    
    CFRunLoopAddObserver(runLoop, observer, runLoopMode);
}

- (void)fd_precacheIndexPathIfNeeded:(NSIndexPath *)indexPath
{
    if (![self.fd_cellHeightCache hasCachedHeightAtIndexPath:indexPath]) {
        CGFloat height = [self.delegate tableView:self heightForRowAtIndexPath:indexPath];
        [self.fd_cellHeightCache cacheHeight:height byIndexPath:indexPath];
        [self fd_debugLog:[NSString stringWithFormat:
                           @"precached - [%@:%@] %@",
                           @(indexPath.section),
                           @(indexPath.row),
                           @(height)]];
    }
}

- (NSArray *)fd_allIndexPathsToBePrecached
{
    NSMutableArray *allIndexPaths = @[].mutableCopy;
    for (NSInteger section = 0; section < [self numberOfSections]; ++section) {
        for (NSInteger row = 0; row < [self numberOfRowsInSection:section]; ++row) {
            NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section];
            if (![self.fd_cellHeightCache hasCachedHeightAtIndexPath:indexPath]) {
                [allIndexPaths addObject:indexPath];
            }
        }
    }
    return allIndexPaths.copy;
}

@end

4. 下面又是一个分类UITableView + FDTemplateLayoutCellAutomaticallyCacheInvalidation 

  因为我们会有一些操作导致cell的改变,所以这里作者要保证在每次cell改变的时候把sections数组改掉,然后如果新增或者修改了 需要重新计算高度。用到了methodSwizzle 黑魔法。这里作者把swizzle放在了UITableView的load类方法中。需要使用methodSwizzle的方法有:

  SEL selectors[] = {
        @selector(reloadData),
        @selector(insertSections:withRowAnimation:),
        @selector(deleteSections:withRowAnimation:),
        @selector(reloadSections:withRowAnimation:),
        @selector(moveSection:toSection:),
        @selector(insertRowsAtIndexPaths:withRowAnimation:),
        @selector(deleteRowsAtIndexPaths:withRowAnimation:),
        @selector(reloadRowsAtIndexPaths:withRowAnimation:),
        @selector(moveRowAtIndexPath:toIndexPath:)
    };

这个类的代码:

@implementation UITableView (FDTemplateLayoutCellAutomaticallyCacheInvalidation)

+ (void)load
{
    // All methods that trigger height cache's invalidation
    SEL selectors[] = {
        @selector(reloadData),
        @selector(insertSections:withRowAnimation:),
        @selector(deleteSections:withRowAnimation:),
        @selector(reloadSections:withRowAnimation:),
        @selector(moveSection:toSection:),
        @selector(insertRowsAtIndexPaths:withRowAnimation:),
        @selector(deleteRowsAtIndexPaths:withRowAnimation:),
        @selector(reloadRowsAtIndexPaths:withRowAnimation:),
        @selector(moveRowAtIndexPath:toIndexPath:)
    };
    
    for (NSUInteger index = 0; index < sizeof(selectors) / sizeof(SEL); ++index) {
        SEL originalSelector = selectors[index];
        SEL swizzledSelector = NSSelectorFromString([@"fd_" stringByAppendingString:NSStringFromSelector(originalSelector)]);
        
        Method originalMethod = class_getInstanceMethod(self, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
        
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

- (void)fd_reloadData
{
    if (self.fd_autoCacheInvalidationEnabled) {
        [self.fd_cellHeightCache.sections removeAllObjects];
    }
    [self fd_reloadData]; // Primary call
    [self fd_precacheIfNeeded];
}

- (void)fd_insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
{
    if (self.fd_autoCacheInvalidationEnabled) {
        [sections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
            [self.fd_cellHeightCache.sections insertObject:@[].mutableCopy atIndex:idx];
        }];
    }
    [self fd_insertSections:sections withRowAnimation:animation]; // Primary call
    [self fd_precacheIfNeeded];
}

- (void)fd_deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
{
    if (self.fd_autoCacheInvalidationEnabled) {
        [sections enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
            [self.fd_cellHeightCache.sections removeObjectAtIndex:idx];
        }];
    }
    [self fd_deleteSections:sections withRowAnimation:animation]; // Primary call
}

- (void)fd_reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation
{
    if (self.fd_autoCacheInvalidationEnabled) {
        [sections enumerateIndexesUsingBlock: ^(NSUInteger idx, BOOL *stop) {
            NSMutableArray *rows = self.fd_cellHeightCache.sections[idx];
            for (NSInteger row = 0; row < rows.count; ++row) {
                rows[row] = @(_FDTemplateLayoutCellHeightCacheAbsentValue);
            }
        }];
    }
    [self fd_reloadSections:sections withRowAnimation:animation]; // Primary call
    [self fd_precacheIfNeeded];
}

- (void)fd_moveSection:(NSInteger)section toSection:(NSInteger)newSection
{
    if (self.fd_autoCacheInvalidationEnabled) {
        [self.fd_cellHeightCache.sections exchangeObjectAtIndex:section withObjectAtIndex:newSection];
    }
    [self fd_moveSection:section toSection:newSection]; // Primary call
}

- (void)fd_insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
{
    if (self.fd_autoCacheInvalidationEnabled) {
        [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) {
            NSMutableArray *rows = self.fd_cellHeightCache.sections[indexPath.section];
            [rows insertObject:@(_FDTemplateLayoutCellHeightCacheAbsentValue) atIndex:indexPath.row];
        }];
    }
    [self fd_insertRowsAtIndexPaths:indexPaths withRowAnimation:animation]; // Primary call
    [self fd_precacheIfNeeded];
}

- (void)fd_deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
{
    if (self.fd_autoCacheInvalidationEnabled) {
        [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) {
            [self.fd_cellHeightCache.sections[indexPath.section] removeObjectAtIndex:indexPath.row];
        }];
    }
    [self fd_deleteRowsAtIndexPaths:indexPaths withRowAnimation:animation]; // Primary call
}

- (void)fd_reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
{
    if (self.fd_autoCacheInvalidationEnabled) {
        [indexPaths enumerateObjectsUsingBlock:^(NSIndexPath *indexPath, NSUInteger idx, BOOL *stop) {
            NSMutableArray *rows = self.fd_cellHeightCache.sections[indexPath.section];
            rows[indexPath.row] = @(_FDTemplateLayoutCellHeightCacheAbsentValue);
        }];
    }
    [self fd_reloadRowsAtIndexPaths:indexPaths withRowAnimation:animation]; // Primary call
    [self fd_precacheIfNeeded];
}

- (void)fd_moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath
{
    if (self.fd_autoCacheInvalidationEnabled) {
        NSMutableArray *sourceRows = self.fd_cellHeightCache.sections[sourceIndexPath.section];
        NSMutableArray *destinationRows = self.fd_cellHeightCache.sections[destinationIndexPath.section];
        
        NSNumber *sourceValue = sourceRows[sourceIndexPath.row];
        NSNumber *destinationValue = destinationRows[destinationIndexPath.row];
        
        sourceRows[sourceIndexPath.row] = destinationValue;
        destinationRows[destinationIndexPath.row] = sourceValue;
    }
    [self fd_moveRowAtIndexPath:sourceIndexPath toIndexPath:destinationIndexPath]; // Primary call
}

@end

5. 下面还有一个分类UITableView + FDTemplateLayoutCell,这个类提供外界获取cell高度的方法

  • fd_heightForCellWithIdentifier:configuration:configuration
  • fd_heightForCellWithIdentifier:cacheByIndexPath:configuration:configuration

  这个类的方法如下:

@implementation UITableView (FDTemplateLayoutCell)

- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier configuration:(void (^)(id))configuration
{
    if (!identifier) {
        return 0;
    }
    
    // Fetch a cached template cell for `identifier`.
    UITableViewCell *cell = [self fd_templateCellForReuseIdentifier:identifier];
    
    // Manually calls to ensure consistent behavior with actual cells (that are displayed on screen).
    [cell prepareForReuse];
    
    // Customize and provide content for our template cell.
    if (configuration) {
        configuration(cell);
    }
    
    // Add a hard width constraint to make dynamic content views (like labels) expand vertically instead
    // of growing horizontally, in a flow-layout manner.
    NSLayoutConstraint *tempWidthConstraint =
    [NSLayoutConstraint constraintWithItem:cell.contentView
                                 attribute:NSLayoutAttributeWidth
                                 relatedBy:NSLayoutRelationEqual
                                    toItem:nil
                                 attribute:NSLayoutAttributeNotAnAttribute
                                multiplier:1.0
                                  constant:CGRectGetWidth(self.frame)];
    [cell.contentView addConstraint:tempWidthConstraint];
    
    // Auto layout engine does its math
    CGSize fittingSize = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
    
    [cell.contentView removeConstraint:tempWidthConstraint];
    
    // Add 1px extra space for separator line if needed, simulating default UITableViewCell.
    if (self.separatorStyle != UITableViewCellSeparatorStyleNone) {
        fittingSize.height += 1.0 / [UIScreen mainScreen].scale;
    }
    
    return fittingSize.height;
}

- (CGFloat)fd_heightForCellWithIdentifier:(NSString *)identifier cacheByIndexPath:(NSIndexPath *)indexPath configuration:(void (^)(id))configuration
{
    if (!identifier || !indexPath) {
        return 0;
    }
    
    // Enable auto cache invalidation if you use this "cacheByIndexPath" API.
    if (!self.fd_autoCacheInvalidationEnabled) {
        self.fd_autoCacheInvalidationEnabled = YES;
    }
    // Enable precache if you use this "cacheByIndexPath" API.
    if (!self.fd_precacheEnabled) {
        self.fd_precacheEnabled = YES;
        // Manually trigger precache only for the first time.
        [self fd_precacheIfNeeded];
    }
    
    // Hit the cache
    if ([self.fd_cellHeightCache hasCachedHeightAtIndexPath:indexPath]) {
        [self fd_debugLog:[NSString stringWithFormat:
                           @"hit cache - [%@:%@] %@",
                           @(indexPath.section),
                           @(indexPath.row),
                           @([self.fd_cellHeightCache cachedHeightAtIndexPath:indexPath])]];
        return [self.fd_cellHeightCache cachedHeightAtIndexPath:indexPath];
    }
    
    // Do calculations
    CGFloat height = [self fd_heightForCellWithIdentifier:identifier configuration:configuration];
    [self fd_debugLog:[NSString stringWithFormat:
                       @"calculate - [%@:%@] %@",
                       @(indexPath.section),
                       @(indexPath.row),
                       @(height)]];
    
    // Cache it
    [self.fd_cellHeightCache cacheHeight:height byIndexPath:indexPath];
    
    return height;
}

- (BOOL)fd_debugLogEnabled
{
    return [objc_getAssociatedObject(self, _cmd) boolValue];
}

- (void)setFd_debugLogEnabled:(BOOL)debugLogEnabled
{
    objc_setAssociatedObject(self, @selector(fd_debugLogEnabled), @(debugLogEnabled), OBJC_ASSOCIATION_RETAIN);
}

@end

 

posted @ 2017-08-18 18:22  紫洁  阅读(636)  评论(0编辑  收藏  举报