详情页内存持续上升问题排查

起因

项目中详情页中可以切换到前一个详情页或者下一个详情页,有使用类似于轮播图的方案,一共只创建3个详情页,切换后状态复,复用了原来的页面,没有销毁或者新建页面;测试反馈切换多次后页面切换会变卡;同事试了一下的确是这样,让帮忙排查一下 _config.yml 前后切换详情的时候发现内存持续上涨,切换一次涨了10~20M,通过xCode->Debug Memory Graph发现cell没有释放,都是tableView持有了cell

怀疑点:

  • 详情页中有UITableView,但cell的复用方式有点怪,没有用系统的复用方式,而是自己写了个字典来复用cell
  • 项目中用了ReactiveObjC,怀疑是什么地方使用不当导致了内存泄漏

一句话真因

  • 详情页中UITableViewCell通过initWithStyle: reuseIdentifier:的方式创建,详情页前后切换时有重新创建且缓存cell导致内存持续上涨
  • ReactiveObjC使用不当导致了内存泄漏

UITableViewCell重用机制

当UITableView展示大量数据时,屏幕可视区域能同时展示的cell数量有限;如果每个数据项都创建一个新的cell,会导致 1、内存占用急剧增加 2、滚动时频繁创建/销毁对象,引发性能卡顿等问题。

重用机制通过复用已离开屏幕的cell,避免重复创建,从而优化性能。

工作原理
  • 重用池

UITableView内部维护一个“重用池”,即一个存储可复用UITableViewCell的集合

  • cell生命周期
  1. 当cell滚动出屏幕时,系统不会销毁它,而是将其“回收”到重用池,等待复用
  2. 当新的cell需要展示时(如滚动时新内容进入屏幕),UITableView会先从重用池查询是否有可复用的cell,如果有,则取出并更新数据;如果没有,则创建新的cell(自动创建需要提前注册)。
  • 复用cell的两种方式
    1. 手动创建新的cell
//设置cell
(UITableViewCell *)tableView:(UITableView *)tableViewcellForRowAtIndexPath:(NSIndexPath *)indexPath {
    //1.根据标识去缓存池中去取cell
    UITableViewCell *cell = [_tableView dequeueReussableCellWithIdentifier:identifying];
    //2.根据是否取到了可用的cell来判断是否需要重新创建cell (手动创建新的单元格)
    if (cell == nil){
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifying];
    }
    //3.设置单元格的显示数据
    cell.textLabel.text = [NSString stringWithFormat:@"第%ld行", indexPath.row];
    return cell;
}
  1. 通过注册cell的方式,有tableview自己创建cell

//注册单元格的类型
[_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:identifying];

// 设置cell
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    //1.根据标识去缓存池中去取cell
    UITableViewCell *cell
    [_tableView dequeueReusableCellWithIdentifier:identifying];
    //2.根据ID标识来判断有没有对应的cell类型,当缓存池中没有可以复用的cell的时候,会根据注册的类型自动创建cell
    //3.设置单元格的显示数据
    cell.textLabel.text = [NSString stringWithFormat:@"第%ld行",indexPath.row];
    return cell;
}

  • 注册与非注册cell方法对比
    1. 非注册cell dequeueReusableCellWithIdentifier:

当需要新cell时,有可用cell就从复用池中取出cell,没有则方法返回nil.

因此使用此方法需要手动判断返回的cell是否为nil,若为nil,则需要手动创建cell

  1. 注册cell dequeueReusableCellWithIdentifier:forIndexPath:

不要在DataSource的 tableView:cellForRowAtIndexPath:之外调用此方法。如 果需要在其他时间创建cell,请改为调用dequeueReusableCellWithIdentifier:方法。

如果已经注册了一个类,那么调用此方法,且要在使用方法之前注册。 该方法会自动为你创建一个该类的cell并初始化,不需要手动判断nil并创建了

  • 复用注意点
  1. 避免在cell中存储状态

cell状态(如是否展开、选中)应存储在数据源中,而不是cell本身中,否则复用后状态会丢失或错乱

  1. prepareForReuse

当单元格即将从重用池被取出并复用(如滚动时重新进入屏幕)时,系统会自动调用此方法,让开发者有机会清除旧数据、重置状态,避免复用前的内容或状态影响新数据的展示

必须调用 super.prepareForReuse() 父类(UITableViewCell)的实现可能包含系统级的重置逻辑(如清除默认子视图状态),忽略会导致潜在问题

调用时机为 dequeueReusableCell(withIdentifier:) → prepareForReuse() → tableView(_:cellForRowAt:)

即:从重用池取出单元格后,在 cellForRowAt 配置新数据前调用

  1. 与 cellForRowAt 的分工

prepareForReuse():负责 “清零”(通用重置);

cellForRowAt:负责 “赋值”(配置当前行数据);


引用源

Written on July 12, 2025