UICollectionView高度宽度自适应缓存框架

前言

  • 演示内容:
    • 1.自适应Cell
    • 2.瀑布流
    • 3.微信UI设计
    • ......

1、2演示内容完成,后续再更新

参考资料

UITableView

UICollectionView

UICollectionViewDataSource

UICollectionViewDelegate

UICollectionViewDelegateFlowLayout

UITableViewCell自适应高度框架

关于FDTemplateLayoutCell作者博客

设计思路

UITableView.png
UICollectionView.png
UICollectionViewDelegateFlowLayout.png
UICollectionViewDataSource.png
UICollectionViewDelegate.png
LTCollectionViewLayout2.png
layout cell(self-sizing cell).png
根据key(键)来区别每个UICollectionViewCell高度
+load.png
UITableView/UICollectionView设置高度方法
UICollectionView+LTCollectionViewLayout(高度布局).png
LTKeyedHeightCache(根据Key缓存UICollectionViewCell的高度).png

技术点

  • Category Use Method Swizzling
  • Category AssociatedObject
  • systemLayoutSizeFittingSize And sizeThatFits
  • 为什么删除一个 indexPath 为 [0:5] 的最后一个 cell或item 时,[0:0] ~ [0:4] 的高度缓存不受影响,而 [0:5] 后面所有的缓存值都向前移动一个位置。

Category Use Method Swizzling

1.类中调用+load方法和-category中调用和+load方法调用顺序是怎样(类和分类同时重写load方法)?
答:+load的执行顺序是先类,后category,而category的+load执行顺序是根据编译顺序决定的。

**2.类和-category中调用和+load方法调用顺序是怎样(只有分类重写load方法)? **
答:

  • (1)先调用category +load方法,后类

  • (2)在Objective-C实现扩展方法可以使用Category来覆盖系统方法,当系统方法被覆盖后,系统会优先调用Category中的代码,然后在调用原类中的代码,如果我们在已有的Category想实现UIWebViewDelegate代理方法,往往就会使用Method Swizzling,可以通过新建UIWebView Category,在其分类使用+(load)Method Swizzling替换代理方法为自己构造的方法(构造的方法内部计算UIWebView 高度并缓存高度),再执行项目当中UIWebViewDelegate代理方法

深入理解Category

Category AssociatedObject

1.在category里面如何添加实例变量的? 
答:在category里面是无法为category添加实例变量的。但是我们很多时候需要在category中添加和对象关联的值,这个时候可以求助关联对象来实现。

2.但是关联对象又是存在什么地方呢? 如何存储? 对象销毁时候如何处理关联对象呢?
关联对象又是存在什么地方呢:AssociationsManager里面是由一个静态AssociationsHashMap来存储所有的关联对象的。

如何存储:所有对象的关联对象都存在一个全局map里面。而map的的key是这个对象的指针地址(任意两个不同对象的指针地址一定是不同的),而这个map的value又是另外一个AssociationsHashMap,里面保存了关联对象的kv对。

对象销毁时候如何处理关联对象:runtime的销毁对象函数objc_destructInstance里面会判断这个对象有没有关联对象,如果有,会调用_object_remove_assocations做关联对象的清理工作。

systemLayoutSizeFittingSize And sizeThatFits

  • LT_systemFittingHeightForConfiguratedWebView:该方法提供了两套计算高度方式,分别是框架布局和自动

  • 当enforceFrameLayout为NO时使用自动布局的步骤:

(1)在计算高度前向 contentView 加了一条和 webView 宽度相同的宽度约束,强行让 contentView 内部的控件知道了自己父 view 的宽度,再反算自己被外界约束的宽度(给contentView添加约束)
(2)调用systemLayoutSizeFittingSize api为contentView自适应高度
(3)移出contentView约束框架布局

为什么删除一个 indexPath 为 [0:5] 的最后一个 cell或item 时,[0:0] ~ [0:4] 的高度缓存不受影响,而 [0:5] 后面所有的缓存值都向前移动一个位置。

答:

  • 1.+(load)Method Swizzling替换方法代理,reloadData、insertSections、deleteSections、reloadSections、moveSection、insertRowsAtIndexPaths、deleteRowsAtIndexPaths、reloadRowsAtIndexPaths、moveRowAtIndexPath

  • 2.当调用如-reloadData,-deleteRowsAtIndexPaths:withRowAnimation:
    等任何一个触发 UITableView 刷新机制的方法时,已有的高度缓存将以最小的代价执行失效,其内部会重绘分组高度并重新设置缓存器,具体过程是这样的。

    • 2.1调用reloadData方法,使用迭代器清空之前横屏或竖屏所缓存的高度数组
    • 2.2遍历每一个cell时,一个一个的将高度缓存该思想类似于masonry框架:mas_makeConstraints执行流程:
      • 2.2.1创建约束制造者MASConstraintMaker,绑定控件,生成了一个保存约束的数组 执行mas_makeConstraints传入进行的block让约束制造者安装约束
      • 2.2.2请空之前的所有约束
      • 2.2.3遍历约束数组,一个一个的安装

几个问题

** 1.UITableView与UICollectionView从缓存池中取出Cell的区别? **

1.1- (nullable __kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier; 

1.2- (__kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(6_0);


2.1- (__kindof UICollectionViewCell *)dequeueReusableCellWithReuseIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath;
  • UITableView提供1.1、1.2方法指定重用标识来获取缓存池cell
  • UICollectionView只能通过2.1方法指定重用标识来获取缓存池cell

** 问题发现和总结:**

1.3- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
                                            VS
2.2- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath;
  • UITableView与UICollectionView分别调用1.3、2.2方法来设置其对应的cell高度
  • UITableView在调用1.3方法时会将每组所对应的所有cell的高度一次性设置完成并其内部不会缓存,当滑动过程中会根据其可视Cell来再次调用该1.3方法设置高度;UICollectionView在调用2.2方法时会将每组所对应的所有cell的高度一次性设置完成并其内部会缓存,在滑动过程中不会再次调用2.2方法,只有调用reloadData、insertSections、deleteSections、reloadSections、moveSection、insertItemsAtIndexPaths、deleteItemsAtIndexPaths、reloadItemsAtIndexPaths、moveItemAtIndexPath触发高度缓存失效的方法才会被再次调用.(** 2.UITableView与UICollectionView设置高度的区别? **)

  • 使用UITableView的1.3方法中调用1.1方法通过指定重用标识来获得重用池Cell,如果该重用池中没有可用的Cell时,内部会其指定的重用标识来创建一个自定义UITableViewCell或UITableViewCell;使用UITableView的1.3方法中调用1.2方法通过制定重用标识来获得重用池Cell时,该方法内会出现死循环;

  • 使用UICollectionView的2.1方法中调用2.2方法通过重用标识来获得重用池Cell,会出现数组越界造成崩溃现象;

  • 使用UITableView的1.3方法中调用1.1方法通过指定重用标识来获得重用池Cell,如果该重用池中没有可用的Cell时,内部会其指定的重用标识来创建一个自定义UITableViewCell;使用UITableView的1.3方法中调用1.2方法通过制定重用标识来获得重用池Cell时,1.2方法会出现死循环;

1.4- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;

2.3- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath;
  • 当UITableView/UICollectionView分别调用1.3、2.2方法后,此时调用1.4、2.3方法分别调用1.1和1.2、2.1方法获取重用Cell不会出现死循环或数组越界崩溃现象

** 解决办法:**

  • 在读FDTemplateLayoutCell框架源码时,你可以从UITableView+FDTemplateLayoutCell文件,找到fd_templateCellForReuseIdentifier方法,该方法通过指定重用标识返回一个 UITableViewCell 或 UITableViewCell 子类的实例,UITableView可以在heightForRowAtIndexPath方法中调用dequeueReusableCellWithIdentifier方法并且如果该重用池中没有可用的Cell内部会根据重用标识创建一个UITableViewCell子类或UITableViewCell如果传入的是子类会加载UITableViewCell属性附件,注意不能调用dequeueReusableCellWithIdentifier: forIndexPath方法否则会出现死循环 。
  • 通过以上的分析猜想内部具体实现:假如底层数据存储结构使用的是链表,用NSMutableArray来保存数据在heightForRowAtIndexPath的方法中调用dequeueReusableCellWithIdentifier方法会为NSMutableArray数组初始化并分配内存空间;在heightForRowAtIndexPath方法中调用dequeueReusableCellWithIdentifier: forIndexPath,该内部NSMutableArray没有来得及对数据初始化,而是先用传入的forIndexPath查找有无对应的数值,由于内部采取了一些错误处理机制导致调用该方法后造成死循环而UICollectionView内部并没有采取一些错误处理机制直接报出数组越界程序崩溃退出,那么可以认为heightForRowAtIndexPath方法实质是对数据存储结构初始化,还有实现UITableViewDelegate代理方法时,代理方法一开始就出现重复调用,我的理解可能是为了初始化数据存储结构(关于链表+字典数据存储文章,感兴趣的可以看一下);这两个方法的根本区别个人觉得在于存储数据结构的内存分配是放在该方法内部还是外部(有不对的地方请指出改正),核心代码
templateCell = [self dequeueReusableCellWithIdentifier:identifier];

而UICollectionView想要从重用池用获取UICollectionViewCell只能通过

- (__kindof UICollectionViewCell *)dequeueReusableCellWithReuseIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath;

方法,而且UICollectionView不能在sizeForItemAtIndexPath方法调用dequeueReusableCellWithReuseIdentifier:forIndexPath否则会出现数组越界崩溃现象,所以不能直接在sizeForItemAtIndexPath调用dequeueReusableCellWithReuseIdentifier:forIndexPath方法,来获取高度并缓存。

针对上面提到的问题发现和总结:,需要解决通过重用标识创建一个UICollectionViewCell,并设置相应的属性附件,解决思路:

  • 通过identifier获取Cell名称

  • 创建一个UICollectionViewCell的子类或UICollectionViewCell

      Class class = NSClassFromString(identifier);
      templateCell = [[class alloc] init];
    
  • UICollectionViewCell子类中实现initWithFrame方法,目的是加载XIB属性附件,这部分后面更新会继续优化

- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame: frame]) {
        NSString *className = NSStringFromClass([self class]);
        
        return [[[NSBundle mainBundle] loadNibNamed:className owner:nil options:nil] firstObject];;
    }
    return self;
}

** 3.使用代理方法实现布局和自定义UICollectionViewLayout方法执行顺序? **

第一点清楚方法执行顺序:

  • 1.单栏Cell布局执行顺序(遵循UICollectionViewDataSource, UICollectionViewDelegateFlowLayout协议),与UITableView代理方法执行顺序是一致的,唯一不同在于在设置高度方法时是一次性完成之后只要不触发高度缓存失效方法(reloadData...)就不会被调用,UICollectionView只提供了一个方法获取重用池Cell,而且不能在sizeForItemAtIndexPath方法中调用。
  • 2.多栏布局或自定义Cell(继承自UICollectionViewLayout)
    使用xib或storyboard需要设置CollectionView,Layout为Custom并且设置其对应的Class,如图1-1所示,整个过程类的执行顺序是这样的:
    • 2.1UICollectionViewController的在运行时显示界面调用的几个方法viewWillAppear..... ViewDidLoad
    • 2.2调用UICollectionViewDataSource的两个代理方法
      numberOfSectionsInCollectionView、numberOfItemsInSection
    • 2.3调用继承自UICollectionViewLayout类的 - (void)prepareLayout方法
    • 2.4调用该控制器实现UICollectionViewLayout类的代理方法
    • 2.5.调用layoutAttributesForElementsInRect方法为每个Cell绑定一个Layout属性之后调用collectionViewContentSize方法返回UICollectionView的ContentSize大小
图1-1

使用过程:

** 1.使用XIB需要将该Cell的ReuseIdentifier注册到UICollectionView,可以使用registerClass或registerNib **

 [self.collectionView registerClass:[WallterCollectionViewCell class] forCellWithReuseIdentifier:@"WallterCollectionViewCell"];

** 2.使用XIB用到UICollectionViewLayout自定义布局时,需要设置XIB如图1.2所示 **

图1-2

** 3.高度宽度自适应实现LTCollectionViewDynamicHeightCellLayout代理方法**

@protocol LTCollectionViewDynamicHeightCellLayoutDelegate <NSObject>

@required
- (NSInteger)numberOfColumnWithCollectionView:(UICollectionView *)collectionView
                         collectionViewLayout:( LTCollectionViewDynamicHeightCellLayout *)collectionViewLayout;
@required
- (CGFloat)marginOfCellWithCollectionView:(UICollectionView *)collectionView
                     collectionViewLayout:( LTCollectionViewDynamicHeightCellLayout *)collectionViewLayout;
@required
- (NSMutableArray <NSMutableArray *> *)indexHeightOfCellWithCollectionView:(UICollectionView *)collectionView collectionViewLayout:( LTCollectionViewDynamicHeightCellLayout *)collectionViewLayout;

GIF演示内容

两栏布局,实现LTCollectionViewDynamicHeightCellLayout代理方法

单栏布局图文布局,可选实现UICollectionView代理方法或LTCollectionViewDynamicHeightCellLayout代理方法

单栏布局图片布局,可选实现UICollectionView或LTCollectionViewDynamicHeightCellLayout代理方法

两栏图文布局,实现LTCollectionViewDynamicHeightCellLayout代理方法

实现LTCollectionViewDynamicHeightCellLayout代理方法,多栏布局只需要修改一行代码实现

演示图

使用介绍

1.单栏布局自适应高度(不使用UICollectionViewLayout)

主要代码:

#pragma mark UICollectionViewDataSource

- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
    return self.feedEntitySections.count;
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return [self.feedEntitySections[section] count];
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    LTFeedCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:reuseIdentitier forIndexPath:indexPath];
    [self configureCell:cell atIndexPath:indexPath];
    return cell;
}

- (void)configureCell:(LTFeedCell *)cell atIndexPath:(NSIndexPath *)indexPath {
    cell.lt_enforceFrameLayout = NO;
    
    cell.entity = self.feedEntitySections[indexPath.section][indexPath.row];
}

#define SCREEN_WIDTH [[UIScreen mainScreen] bounds].size.width

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
    CGFloat height = [collectionView lt_heightForCellWithIdentifier:reuseIdentitier cacheByIndexPath:indexPath configuration:^(LTFeedCell *cell) {
        
        [self configureCell:cell atIndexPath:indexPath];
    }];
    return CGSizeMake(SCREEN_WIDTH, height);
    
}

**2.多栏布局自适应高度(使用UICollectionViewLayout需要实现LTCollectionViewDynamicHeightCellLayout代理方法) **

主要代码

#pragma mark - LTCollectionViewDynamicHeightCellLayoutDelegate
- (NSInteger) numberOfColumnWithCollectionView:(UICollectionView *)collectionView
                          collectionViewLayout:( LTCollectionViewDynamicHeightCellLayout *)collectionViewLayout{
    return _cellColumn;
}

- (CGFloat) marginOfCellWithCollectionView:(UICollectionView *)collectionView
                      collectionViewLayout:(LTCollectionViewDynamicHeightCellLayout *)collectionViewLayout{
    return _cellMargin;
}


- (NSMutableArray<NSMutableArray *> *)indexHeightOfCellWithCollectionView:(UICollectionView *)collectionView collectionViewLayout:(LTCollectionViewDynamicHeightCellLayout *)collectionViewLayout {
    return _indexCountBySectionForHeight;
}


#pragma mark UICollectionViewDataSource
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
    
    return self.FeedEntitySections.count;
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    NSMutableArray<NSNumber *> *indexHeightArray = @[].mutableCopy;
    for (NSInteger i = 0; i < [self.FeedEntitySections[section] count]; i++) {
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:section];
        CGFloat height= [collectionView lt_heightForCellWithIdentifier:reuseIdentitier cacheByIndexPath:indexPath configuration:^(LTFeedCell *cell) {
            [self configureCell:cell atIndexPath:indexPath];
        }];
        
        [indexHeightArray addObject:@(height)];
    }
    _indexCountBySectionForHeight[section] = indexHeightArray;
    return [self.FeedEntitySections[section] count];
}

最后

** 如果能帮助到您,希望能给一个小小的️Star或者点亮博文左下角的星,朋友的鼓励和支持是我继续分享的动力 **

源码下载地址

感谢:

sunnyxx
青玉伏案

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

推荐阅读更多精彩内容