自定义TableViewCell中回调(Blocks)的正确使用方式
嘿,这俩问题真是问到点子上了——TableViewCell用Block回调时,这两个点几乎是必踩的坑,我来给你掰扯清楚:
1. 必须用[weak self]避免强引用循环吗?
答案是肯定要加,而且是必须重视的核心点。
咱们来捋清循环引用的链条:
- 你的自定义Cell会持有这两个Block(因为按钮的回调Block是绑定在Cell上的,Cell作为按钮的父视图,会间接持有Block)
- 如果Block里直接引用了
self(也就是你的ViewController),Block会对self产生强引用 - 而ViewController又持有TableView,TableView在复用机制下会持有当前显示的Cell
- 这就形成了闭环:
ViewController → TableView → Cell → Block → ViewController,内存根本无法释放,妥妥的内存泄漏
正确的做法是在Block捕获列表里加上[weak self],如果需要在Block内多次使用self,还可以转成局部强引用(避免中途self被释放),比如Swift写法:
cell.button1TapHandler = { [weak self] in guard let self = self else { return } // 在这里处理点击逻辑,比如跳转、更新数据 }
OC写法:
cell.button1TapHandler = ^(void) { __weak typeof(self) weakSelf = self; __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf) return; // 处理逻辑 };
这样就能打破循环引用链条,确保内存正常释放。
2. 捕获indexPath和cell是否合理?
答案是非常不合理,完全踩中了TableView复用的坑。
为什么不能捕获cell?
TableView的Cell是复用池机制,当你滚动列表时,当前Cell会被回收,重新配置给新的行。如果Block里捕获了cell,用户点击按钮时,这个cell可能已经被复用给其他indexPath了,你拿到的cell对应的行根本不是用户点击的那一行,逻辑直接出错。
为什么不能捕获indexPath?
同样的问题:配置Cell时捕获的indexPath,后续如果列表数据变化(比如插入、删除、排序行),这个indexPath对应的模型已经不是原来的了;或者Cell被复用后,旧的indexPath也不再对应当前显示的内容,点击时用旧indexPath取数据,肯定出问题。
正确的做法是什么?
最稳妥的方式是让Cell持有对应的模型数据,配置Cell时把模型传给它,Block回调时把模型传递出来,ViewController拿到模型后再处理逻辑,或者通过模型找到对应的indexPath(如果需要的话)。比如:
// 自定义Cell里定义模型属性和回调 class CustomCell: UITableViewCell { var model: YourModel? var buttonTapHandler: ((YourModel) -> Void)? // 按钮点击事件触发Block @IBAction func buttonTapped(_ sender: UIButton) { guard let model = model else { return } buttonTapHandler?(model) } } // 在ViewController的cellForRow里配置 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell let model = dataArray[indexPath.row] cell.model = model cell.buttonTapHandler = { [weak self] model in guard let self = self else { return } // 用model处理逻辑,比如找到实时indexPath if let currentIndexPath = self.tableView.indexPath(for: cell) { // 执行操作 } // 或者直接用model跳转详情页 self.pushDetailVC(with: model) } return cell }
如果确实需要用到当前indexPath,也不要提前捕获,而是在回调时通过tableView.indexPath(for: cell)获取实时的indexPath,同时要注意判断返回值是否为nil(比如cell已被移出屏幕时)。
内容的提问来源于stack exchange,提问作者Narek Simonyan

