早些时候看过一些分析定时器内存方面的文章,但在遇到这个bug前我是不屑的。不就是定时器强引用ViewController,而ViewController再用strong属性去引用定时器,必然会导致循环引用么。解决办法也很简单,只需使用weak属性去引用定时器即可。然而这一次的经历却证明我还是图样,我单知道使用weak属性不会导致循环引用,我没注意到此时的定时器在无形中延长了ViewController的生命周期。这就为这个bug埋下了隐患。
记录一下bug的解决总是有必要的。
下午的一次自测中,偶尔发现观众端听不到主播端的声音。我先是诧异,接着是很不安,因为墨菲定律告诉我们:如果你担心某种情况发生,那么它就更有可能发生。最开始以为是主播端的问题,便仔细检查了主播端的代码,又加入了另一台手机设置为观众,作为对照。运行之后发现对照组是好的,但刚才那台手机的问题仍然偶现。这就说明主播端的推流是没问题的。接下来的工作便是在问题手机上尽可能找到复现的操作,以便根据操作路径定位大致原因。在某次频繁进入离开直播房间时,APP直接卡死了,再无任何交互的响应。问题开始变得严重,时间也一分一秒的流逝在这一次次的调试中,一晃下班时间快到了,周围开始变的嘈杂,安卓兄弟开始催我下班还说要带我上王者但我是不信的。我整理了下东西,但又不想在节前留下些许问题,便又坐了回去。等到周围开始安静时,夕阳已经西下。我努力回想之前的操作,发现只在直播预约状态下,问题才会重现。于是在页面的dealloc函数中打好断点,点击返回,果然函数没有被调用。这说明页面依然被某个对象持有而没有释放。在检查了所有Block回调都使用的是weakSelf后,最后只剩下定时器了。
问题的根源算是找到了。原来在直播预约状态下会启动一个定时器,但在点击返回时忘记invalid定时器了。这让定时器延长了ViewController的生命周期。加上invalid后,问题搞定,收工。回家的路上碰巧遇到了K君,便给K君讲述了这个问题,K君听后哈哈大笑说:加上invalid只能解决这一次的bug,却不能避免下一次又忘记,而且根据页面dealloc函数里逻辑的不同,bug的外在表现形式也必然不同,到时候又得花费不少的时间找bug啊。闻道于朝,不禁感叹K君的身经百战。
回到家后,打开谷歌又搜到了早些时候看过的那些文章,感慨颇多。系统的NSTimer
简单却又不那么简单:
- 不注意使用的话有内存泄漏的隐患
- 需要在合适的地方invalid定时器,否则定时器会一直强引用target从而延长target的生命周期。
- 使用时必须保证有一个活跃的runloop,并且需要指定mode
- 精度可能不够
- 创建和撤销必须在同一个线程上,在多线程环境下使用不便
- 不支持block,造成使用上的不便。(iOS10开始,已经支持block了)
为了从根本上避免上述问题,一个弱引用target的、能够在自身销毁时自动invalid的定时器想必是极好的,但又该如何实现呢?好在互联网在经过这么多年的发展,第三方开源库从未像现在这般丰富,唾手可得。不多时,便在GitHub上找到了MSWeakTimer。
MSWeakTimer
提供了和系统NSTimer
一致的接口,好的代码就该这样美美与共,和而不同:
@property (nonatomic, strong) MSWeakTimer *weakTimer;
self.weakTimer = [MSWeakTimer scheduledTimerWithTimeInterval:3 target:self selector:@selector(doSome) userInfo:nil repeats:YES dispatchQueue:dispatch_get_main_queue()];
//立即触发回调方法
[self.weakTimer fire];
- (void)doSome {
NSLog(@"++++++%@", self);
}
至于MSWeakTimer
的实现原理自然是和NSTimer
不同的:通过封装GCD定时器实现NSTimer
的功能,但内部却是弱引用target,不仅如此MSWeakTimer
还支持在其他线程中执行回调函数。
@interface MSWeakTimer ()
{
struct
{
uint32_t timerIsInvalidated;
} _timerFlags;
}
@property (nonatomic, assign) NSTimeInterval timeInterval;
@property (nonatomic, weak) id target; //弱引用target
@property (nonatomic, assign) SEL selector;
@property (nonatomic, strong) id userInfo;
@property (nonatomic, assign) BOOL repeats;
@property (nonatomic, ms_gcd_property_qualifier) dispatch_queue_t privateSerialQueue;
@property (nonatomic, ms_gcd_property_qualifier) dispatch_source_t timer;
@end
...
//自身销毁时invalidate掉定时器
- (void)dealloc
{
[self invalidate];
ms_release_gcd_object(_privateSerialQueue);
}
当我们使用MSWeakTimer
时,就可以避免因忘记invalid定时器,导致ViewController生命周期被延长不能及时销毁而产生的bug。从这之后,我便不再遇到和NSTimer
相关的bug了。
网友评论