UICollectionView高度宽度自适应缓存框架
前言
- 演示内容:
- 1.自适应Cell
- 2.瀑布流
- 3.微信UI设计
- ......
1、2演示内容完成,后续再更新
参考资料
UICollectionViewDelegateFlowLayout
设计思路
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 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.使用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或者点亮博文左下角的星,朋友的鼓励和支持是我继续分享的动力 **