关键词: UIViewController, UITableViewCell, NSTimer, 释放,资源清理,RAC, rac_willDeallocSignal, 响应链
发现问题
上图是一个活动列表,其中第二个Cell中有一个距开始多久的提示,这就是一个倒计时,用来提示该活动还有多久开始。我们满足这个需求的做法是在Cell中添加了一个NSTimer来每秒倒计时时间。但当点击返回按钮返回的时候,含有NSTimer的Cell是不会自动调用dealloc释放资源的。Timer会持有这个Cell,如果要释放Cell就需要在一个适当时间去invalidate这个Timer,来解除他对Cell的持有。
解决方法
跟随上面的问题,最好是在点击返回按钮,退出该页面的时候。这时UIViewController会自动释放掉,相应的去释放Subview应该是顺理成章的事。沿着这个思路我们来演进一下解决方法。
方法演进版本1
最简单直接的方法当然是想办法在UIViewController的delloc函数里面去停止掉所有Subview(这里是UITableViewCell)的Timer。为了能在上层获取到Cell中的NSTimer,我们不得不把Cell自相关的timer从.m文件移动到.h中。类似下面这样
@interface ActivityTableViewCell : UITableViewCell
@property (nonatomic)NSTimer *countDownTimer;
@end
但UITableView没有提供一个接口来获取它持有的所有Cell的接口,我们就只有另外想办法来记录下所有的Timer。一个简单的办法就是在UIViewController中添加一个NSSet,然后在cellForRowAtIndexPath中将所有的Timer添加到这个集合中,这里为什么用set呢,当然是为了去掉重复的Timer。然后在UIViewController的dealloc中遍历这个set,并依次调用invalidate。代码片段如下
//在UIViewController中添加set
@interface ActivityViewController()
@property (nonatomic)NSMutableSet *timerSet;
@end
//将Timer添加到set中
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
ActivityTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ActivityTableViewCell" forIndexPath:indexPath];
[self.timerSet addObject:countDownTimer];
return cell;
}
//最后在dealloc中遍历set
- (void)dealloc
{
for (NSTimer *timer in self.timerSet){
[timer invalidate];
}
}
方法演进版本2
上面的解决办法直接,简单但存在几个比较明显的问题:
- 在Cell滚动复用过程中,会重新创建Timer,但上面的解决办法没有移除原来不用Timer的方法,这样当滚动次数过多的时候,set里面就会有很多无用的对象。
- 在UIViewControler中增加了一个timerSet,这个本和UIViewController无关的属性,使逻辑看起来变得复杂了,别人咋看之下没法明白这个set是干嘛的,这不是一份易维护的代码。
- 在Cell中把本来应该隐藏在.m实现文件中的countDownTimer放到了.h文件中,所有这个类的使用者都可以看到。这不符合封装的原则,也不是好的代码风格。
为了解决上面这些问题,我们应该把主要工作移到相关的Cell里面去做,这样就可以在最小影响UIViewController的情况下,实现相应的功能。
这时候如果你接触过RAC就会有些想法。RAC为NSObject添加了一个rac_willDeallocSignal的信号,它在一个对象将要被dealloc的时候发送。那么我们可以在Cell里面订阅上层UIViewController的rac_willDeallocSignal信号,在信号里面invalidate相应的timer。但我们要怎么获取到这个UIViewController呢,最直接简单的办法当然是从上层传下去。
@interface ActivityTableViewCell : UITableViewCell
//这里需要使用weak,不然会造成循环引用,你懂得!
@property (nonatomic, weak)UIViewController *controller;
- (void)configCellWithModel:(NSObject *)model;
@end
@interface ActivityTableViewCell ()
//将timer隐藏在.m文件中
@property (nonatomic)NSTimer *countDownTimer;
@end
@implementation ActivityTableViewCell
//使用Model更新cell的函数
- (void)configCellWithModel:(NSObject *)model
{
if (self.controller){
@weakify(self)
//这里需要注意rac_willDeallocSignal没有next,只能订阅Completed
[self.controller.rac_willDeallocSignal
subscribeCompleted:^{
@strongify(self)
[self.countDownTimer invalidate];
self.countDownTimer = nil;
}];
}
}
@end
//在UIViewController去掉set属性,并在提供cell的函数里面如下调用
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
ActivityTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ActivityTableViewCell" forIndexPath:indexPath];
cell.controller = self;
[cell configCellWithModel: someModel];
return cell;
}
上面说的三点问题都避免了,是不是很完美!但是...我们接着看!
方法演进版本3
还有哪里看着不舒服,当然有!
把UIViewController传入Cell里面是我们很少见到的做法,细想之下也觉得不妥。这本来是两个独立的部分,如果采用上面的方式就是两个独立模块存在了一定耦合,不妥。
那有什么办法在Cell中直接获取到对应的UIViewController吗?当然有!那就是响应链的妙用了。如果你还不清楚响应链是什么,请自行google。这里我添加了一个UIView的扩展来获取对应的UIViewController。
@implementation UIView (GetViewController)
- (UIViewController*)getViewController
{
for (UIView* next = [self superview]; next; next = next.superview)
{
UIResponder* nextResponder = [next nextResponder];
if ([nextResponder isKindOfClass:[UIViewController class]])
{
return (UIViewController*)nextResponder;
}
}
return nil;
}
@end
哈哈,有了这个神器上面的耦合不就自然解除了!但是还有一个地方需要注意,那就是订阅的时机。使用上面的扩展需要Cell被添加到视图树之后才能获取到需要的UIViewController,不然得到会是一个空。那么怎么保证Cell一定被添加到视图树呢。UIView有个方法叫didMoveToSuperview,它会在该视图的父视图改变的时候被调用,我们在这里面判断一个就可以保证一定获取到了对应的UIViewController。
- (void)didMoveToSuperview
{
UIViewController *controller = [self getViewController];
//这里需要判断相应的controller是否存在
if (controller){
@weakify(self)
[controller.rac_willDeallocSignal
subscribeCompleted:^{
@strongify(self)
[self.countDownTimer invalidate];
self.countDownTimer = nil;
}];
}
}
哈哈,终于完美了!
总结
经过努力,让原来一坨丑陋的代码最后简化成了一个函数。虽然这是一个小问题,但深究之下东西还不少。对比几种解决办法,基本都是依据通用的代码规范和好的编程原则来优化!该场景下使用的是Cell,其实可以推广到任何的Subview。其他的清理工作也可以使用类似的思路来实现!还有一点就是了解一下其他的编程方式一定会在某些时候影响你的代码。这里使用了RAC,其实也是响应式编程的一点简单应用。