NSTimer 不释放问题分析及解决
NSTimer 不释放问题
@interface ViewController () @property (nonatomic, weak) NSTimer *timer; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; __weak typeof(self) weakSelf = self; self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(timerRun) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes]; }复制代码
如上代码所示,我们创建一个定时器,并添加到当前 runLoop
,也通过 __weak
修饰了 weakSelf
,但是当我们运行计时器离开页面的时候发现计时器并没有销毁,依然在执行。这里有个比较疑惑的点,我们用 block
的时候用 __weak
修饰的时候是可以解决循环引用的问题,但是为什么这里不可以呢?
通过官方文档我们可以看到,我们传入的 target
会被 timer
强持有,这里传入的 target
就是 weakSelf
,所以 weakSelf
指向的内存空间引用计数会被加 1。而 block
传入 weakSelf
是被弱引用捕获,不会对引用计数加 1。然后这里就会造成一种现象,[NSRunLoop currentRunLoop]
是常驻的,[NSRunLoop currentRunLoop]
持有 timer
,timer
强持有 weakSelf
,而 weakSelf
指向的内存空间就是 self
,引用计数会被加 1,所以会出现不释放的问题。
NSTimer 不释放问题解决方案
针对不释放问题我们这里有几种解决方案如下,以供参考。
使用 block 的形式
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"timer 执行"); }];复制代码
离开页面时对 timer 进行处理
- (void)viewWillDisappear:(BOOL)animated{ [super viewWillDisappear:animated]; [self.timer invalidate]; self.timer = nil; }复制代码
类似这种,在离开页面的时候调用 invalidate
,并把 timer
设置为 nil
,但是这里会出现一个问题,push
到下一个页面的时候 timer
也会被释放。所以推荐下面这种方法,只在 pop
离开页面时进行释放。
- (void)didMoveToParentViewController:(UIViewController *)parent { if (parent == nil) { [self.timer invalidate]; self.timer = nil; } }复制代码
中介者模式
- (void)viewDidLoad { [super viewDidLoad]; self.target = [[NSObject alloc] init]; class_addMethod([NSObject class], @selector(timerRun), (IMP) timerRunObjc, "v@:"); self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.target selector:@selector(timerRun) userInfo:nil repeats:YES]; } void timerRunObjc(id obj){ NSLog(@"%s -- %@",__func__,obj); } - (void)dealloc{ [self.timer invalidate]; self.timer = nil; }复制代码
类似这种,我们可以使用一个中间者来作为 target
,来打破这种不释放问题,这种思维来自于 FBKVO
,但是这种虽然能解决问题,但是不够简洁,逻辑代码都写在了控制器中,我们可以对此进行优化。
@interface CXTimerViewController () @property (nonatomic, strong) CXTimerWapper *timerWapper; @end - (void)viewDidLoad { [super viewDidLoad]; self.timerWapper = [[CXTimerWapper alloc] cx_initWithTimeInterval:1 target:self selector:@selector(timerRun) userInfo:nil repeats:YES]; }复制代码
@interface CXTimerWapper : NSObject - (instancetype)cx_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo; @end #import "CXTimerWapper.h" #import <objc/message.h> @interface CXTimerWapper() @property (nonatomic, weak) id target; @property (nonatomic, assign) SEL aSelector; @property (nonatomic, strong) NSTimer *timer; @end @implementation CXTimerWapper - (instancetype)cx_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{ if (self == [super init]) { self.target = aTarget; // vc self.aSelector = aSelector; // 方法 -- vc 释放 if ([self.target respondsToSelector:self.aSelector]) { Method method = class_getInstanceMethod([self.target class], aSelector); const char *type = method_getTypeEncoding(method); class_addMethod([self class], aSelector, (IMP)fireHomeWapper, type); self.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo]; } } return self; } // 一直跑 runloop void fireHomeWapper(CXTimerWapper *warpper){ if (warpper.target) { // vc - dealloc void (*cx_msgSend)(void *,SEL, id) = (void *)objc_msgSend; cx_msgSend((__bridge void *)(warpper.target), warpper.aSelector,warpper.timer); }else{ // warpper.target [warpper.timer invalidate]; warpper.timer = nil; } } - (void)dealloc{ NSLog(@"%s",__func__); } @end复制代码
如上代码,这里采用分层思想,把 timer
跟控制器进行了隔离,timer
的所有逻辑处理都放到了 CXTimerWapper
类中,这里做的比较好的一点就是没有把 selector
进行写死,而是由外界传入的 target
调用 selector
,这样就会比较灵活,而且在 fireHomeWapper
函数中做了对 warpper.target
的判断,warpper.target
为空说明外界调用者已经被释放了,所以就会对 timer
进行释放。
虚基类的方式
@interface ViewController () @property (nonatomic, strong) CXProxy *proxy; @end - (void)viewDidLoad { [super viewDidLoad]; self.proxy = [CXProxy proxyWithTransformObject:self]; self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(timerRun) userInfo:nil repeats:YES]; } - (void)dealloc{ [self.timer invalidate]; self.timer = nil; }复制代码
@interface CXProxy : NSProxy + (instancetype)proxyWithTransformObject:(id)object; @end @interface CXProxy() @property (nonatomic, weak) id object; @end @implementation CXProxy + (instancetype)proxyWithTransformObject:(id)object{ CXProxy *proxy = [CXProxy alloc]; proxy.object = object; return proxy; } // 消息快速转发 -(id)forwardingTargetForSelector:(SEL)aSelector { return self.object; } // 消息转发 self.object //- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{ // // if (self.object) { // }else{ // NSLog(@"麻烦收集 stack111"); // } // return [self.object methodSignatureForSelector:sel]; // //} // //- (void)forwardInvocation:(NSInvocation *)invocation{ // // if (self.object) { // [invocation invokeWithTarget:self.object]; // }else{ // NSLog(@"麻烦收集 stack"); // } // //} @end复制代码
这里我们定义了一个继承于 NSProxy
的类 CXProxy
,这里需要先了解一下 NSProxy
,NSProxy
是一个实现了 NSObject
协议的根类,苹果的官方文档是这样描述它的:NSProxy
是一个抽象基类,它为一些表现的像是其它对象替身或者并不存在的对象定义API
。一般的,发送给代理的消息被转发给一个真实的对象或者代理本身引起加载(或者将本身转换成)一个真实的对象。NSProxy
的基类可以被用来透明的转发消息或者耗费巨大的对象的 lazy
初始化。
在 CXProxy
类中我们实现了 forwardingTargetForSelector
方法,返回 self.object
,这里 self.object
就是外部的控制器,所有发送给 CXProxy
的方法都会被转发给 self.object
。这里跟上面讲的中介者模式类似,但是这里需要注意一点就是在控制器释放的时候需要在 dealloc
方法中对 timer
进行释放。
趁着中秋放假这几天更新了几篇博客,有不足的地方还请多多指正。
作者:晨曦_iOS
链接:https://juejin.cn/post/7010245818380714015