又谈UITableView的dequeueReusableCellWithIdentifier机制

半年多前,写过一篇《再谈UITableView的dequeueReusableCellWithIdentifier机制》说了重用的机制和一些视图重复覆盖的问题,现在从内存方面再说下更合理的复用机制。

一、cell复用情况的分类

如果整个列表每个cell都不相同,其实也就是复用也派不上用场的。

1.PNG

这种之前说过的复用机制是要么清除视图重新复用,要么专门为固定的行去复用其实都无所谓,只是不用再创建新的cell对象而已。而如果列表是两类或者三类等多种的cell类型,那么复用就会起到很明显的优化效果,这种时候谈复用才是复用的精髓,也就是说cell的复用是去用根据内容分类去复用,而不是为了哪一行去复用。

二、复用的原理

首先复用的原理就是给每个cell一个对应的标记indentifier,在消失的时候也不去销毁而是放到内存池中,当又要显示的时候,直接从内存池里面取出来,而不用去创建新的对象,从而达到节省内存的目的。内存的变化就是第一次看列表的时候内存依旧是上升的,但是当每个类型都在内存池中有一份的时候,即使再怎么滑动,内存也不会因为cell出现增加了。

三、复用的实现

以demo为例,在整个cell创建的时候,分为三种cell的展示类型,分别为图片在左边、图片在右边、没有图片,同时图片在左边的时候分为有介绍文字和没有介绍文字两种,也就是总共有四种情况。所以在demo中写了三种cell类,分别为TableViewCellImgLeftTableViewCellImgRightTableViewCellNoImg,通过创建的model内容去填充cell内容。

model的内容分为下面几项、其中介绍有的有,有的没有

typedef NS_ENUM(NSUInteger, ItemType) {
    ItemTypeLeftImg,
    ItemTypeRightImg,
    ItemTypeNoImg,
};

@interface ItemModel : NSObject
@property (strong,nonatomic) NSString *itemTitle;   //标题
@property (strong,nonatomic) NSString *itemDetail;  //介绍
@property (strong,nonatomic) NSString *price;       //价格
@property (strong,nonatomic) NSString *image;       //预览图
@property (strong,nonatomic) NSString *payUrl;      //支付链接
@property (assign,nonatomic) ItemType itemType;     //展示类型

在cell中,通过下面函数获取内容布局。使用mas_remakeConstraints而不去使用mas_makeConstraints在下面说

-(void)configWithModel:(ItemModel*)model
{
    _model = model;
    [self.itemImageview mas_remakeConstraints:^(MASConstraintMaker *make) {
        make.centerY.equalTo(self.contentView);
        make.width.height.mas_equalTo(102);
        make.left.equalTo(self.contentView).offset(14);
    }];
    [self.itemTitleLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(_itemImageview);
        make.left.equalTo(_itemImageview.mas_right).offset(10.5);
    }];
    [self.itemDesLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(_itemTitleLabel.mas_bottom).offset(9.5);
        make.left.equalTo(_itemTitleLabel);
    }];
    [self.itemPriceLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
        make.bottom.equalTo(self.contentView).offset(-20);
        make.left.equalTo(_itemTitleLabel);
    }];
    [self.itemBuyButton mas_remakeConstraints:^(MASConstraintMaker *make) {
        make.right.equalTo(self.contentView).offset(-14.5);
        make.bottom.equalTo(self.contentView).offset(-20);
        make.width.mas_equalTo(80);
        make.height.mas_equalTo(30);
    }];
}

3.1、cell内部控件的复用

在cell中,因为cell需要复用,所以内部的视图变量也要做到可以复用,不去每次创建又要防止视图被提前删除,这样才不会造成视图覆盖的问题,对内存的使用也更合理,所以可以使用懒加载的方案,当然也可以使用viewWithTag去处理,但是综合来看,还是懒加载的代码量和逻辑比较清晰。

以model的介绍为例,因为有可能为空,所以这里在初始化和赋值的时候可以去判断下,当为空的时候隐藏掉,不为空的时候显示介绍,其他的图片和标题是一样的原理。

-(UILabel*)itemDesLabel{
    if (!_itemDesLabel) {
        _itemDesLabel = [[UILabel alloc] init];
        [self.contentView addSubview:_itemDesLabel];
        [_itemDesLabel setTextColor:UIColorFromRGB(0x666666)];
        [_itemDesLabel setFont:[UIFont systemFontOfSize:14]];
    }
    if (_model.itemDetail) {
        [_itemDesLabel setHidden:NO];
       [_itemDesLabel setText:_model.itemDetail];
    }
    else{
        [_itemDesLabel setHidden:YES];
    }
    [_itemDesLabel sizeToFit];
    return _itemDesLabel;
}

3.2、model的类型标记

为了后面分类的去复用,所以在model的初始化的时候去设置类型

NSDictionary *dic3 = [[NSDictionary alloc] initWithObjectsAndKeys:
                          @"玩偶抱枕",@"itemTitle",
                          @"玩偶抱枕diy包邮",@"itemDetail",
                          @"¥100",@"price",
                          @"",@"payUrl",
                          @(ItemTypeNoImg),@"itemType",
                          nil];
ItemModel *itemModel3 = [[ItemModel alloc] initWithDictionary:dic3];

3.3、通过model的类型去创建对应的cell

根据model已经设置好的类型,去设置不同的复用identifier,做到没类对应自己的复用,这样cell的内容才不会因为复用出现视图的覆盖。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *identifier = @"ItemLeftImg";
    static NSString *identifier2 = @"ItemRightImg";
    static NSString *identifier3 = @"ItemNoImg";
    UITableViewCell *cell;
    switch (((ItemModel *)[_dataArray objectAtIndex:indexPath.row]).itemType) {
        case ItemTypeLeftImg:
            cell = [tableView dequeueReusableCellWithIdentifier:identifier];
            if (!cell) {
                cell = [[TableViewCellImgLeft alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
                [cell setSelectionStyle:UITableViewCellSelectionStyleNone];
            }
            [(TableViewCellImgLeft*)cell configWithModel:[_dataArray objectAtIndex:indexPath.row]];
            break;
        case ItemTypeRightImg:
            cell = [tableView dequeueReusableCellWithIdentifier:identifier2];
            if (!cell) {
                cell = [[TableViewCellImgRight alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier2];
                [cell setSelectionStyle:UITableViewCellSelectionStyleNone];
            }
            [(TableViewCellImgRight*)cell configWithModel:[_dataArray objectAtIndex:indexPath.row]];
            break;
        case ItemTypeNoImg:
            cell = [tableView dequeueReusableCellWithIdentifier:identifier3];
            if (!cell) {
                cell = [[TableViewCellNoImg alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier3];
                [cell setSelectionStyle:UITableViewCellSelectionStyleNone];
            }
            [(TableViewCellNoImg*)cell configWithModel:[_dataArray objectAtIndex:indexPath.row]];
            break;
        default:
            break;
    }
    return cell;
}

四、内存控制

4.1、使用mas_makeConstraints布局内存上升

DamonScreenShot 2017-11-24 16.11.37.png

布局的时候,最初是使用mas_makeConstraints去布局,发现内存占用在快速滚动的时候一直上升。

4.png

通过使用Instruments测试并没有发现内存的泄露,但是内存在滚动和刷新布局的时候都会出现上升,上升曲线滚动越快,上升的越快,最初想法是考虑是不是懒加载的问题,在Instruments的列表发现了内存的消耗痕迹。

2.png

这里说明在使用masonry布局的时候,一直在创建叠加占用内存,所以需要使用mas_remakeConstraints而不去使用mas_makeConstraints布局,这样才会去消除之前的布局占用。调整之后在运行滚动,这时候内存才是平稳正常的。

3.png

4.2、懒加载注意事项

以标题label为例,在布局的时候

[self.itemTitleLabel mas_remakeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(_itemImageview);
        make.left.equalTo(_itemImageview.mas_right).offset(10.5);
}];

需要注意的是只有在self.itemTitleLabel这句才会去执行懒加载的函数,而_itemTitleLabel这个只是代表的变量,并不会去使用懒加载,这样的话,在block里面布局的时候,注意要使用变量_itemImageview,尽量不要再去使用self.itemImageview,这是因为这样会调用两次懒加载,白白设置了两次一样的内容,如果是出现在多线程的话,还有可能会将_itemImageview这个变量去alloc两次,所以使用的时候要注意。

五、demo下载

github下载地址:https://github.com/DamonHu/CellDequeueReusableDemo

gitee下载地址:https://gitee.com/DamonHoo/CellDequeueReusableDemo

六、最后放上一张暴力滚动测试内存的动图

Last modification:November 24th, 2017 at 05:25 pm
如果看了这个文章可以让你少加会班,可以请我喝杯可乐
已打赏名单
微信公众号

Leave a Comment