Tencent / QMUI_iOS

QMUI iOS——致力于提高项目 UI 开发效率的解决方案
http://qmuiteam.com/ios
Other
7.05k stars 1.37k forks source link

UITableView 先 setDelegate: 再 setDataSource: 可能引发 crash #1427

Closed MoLice closed 1 year ago

MoLice commented 1 year ago

背景

考虑以下代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    // ...
    self.tableView.delegate = self;
    self.tableView.dataSource = self;
    self.tableView.tableHeaderView = xxx;
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 0;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    id sectionData = data[section];// 假定此时 data 为一个 NSArray 实例,但 count 为 0
    return ...;
}

预期会先询问 numberOfSectionsInTableView,得知是0,就不会再继续去询问 numberOfRowsInSection,于是没问题。

但实际上系统会跳过 numberOfSectionsInTableView,直接询问 numberOfRowsInSection,导致此时我们长度为0的数组被访问了一个下标为0的元素,命中 assert,crash。

原因解释

初看这段代码平平无奇,但这里有个前提,系统的 setDelegate: 会自动 reload 一次,而 setDataSource: 却不会 reload。于是实际的时序是:

  1. 先执行 setDelegate:,触发自动 reload,此时 tableView.dataSource 尚未被赋值,所以 numberOfSections 为系统默认值1。
  2. 再执行 setDataSource:,没有自动 reload,tableView 依然认为自己 numberOfSections 为 1。
  3. setTableHeaderView: 会触发 tableView 计算 contentSize,进而触发它询问数据源,由于到此为止依然没人再去 reload,所以 tableView 认为 numberOfSections 是1,所以直接去询问第一个 section 里的 numberOfRowsInSection,但对业务来说此时数据源是空的,于是越界。
MoLice commented 1 year ago

已发布 4.5.0 修复该问题。