前言
在上一篇 iOS底层-内存管理之弱引用表 中我们对弱引用进行了详细的分析,与弱引用对应的是强引用。强引用会导致控制器不释放,本文将分析NSTimer
的强引用,并提出几种方案解决不释放问题。
NSTimer的强引用分析
-
首先看看如下
NSTimer
案例:// SecondViewController.m - (void)timerTest { __weak typeof(self)weakSelf = self; self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(addNumberAction) userInfo:nil repeats:true]; [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes]; } - (void)addNumberAction { num++; NSLog(@"++ %d ++", num); } - (void)dealloc { [self.timer invalidate]; self.timer = nil; NSLog(@" second dealloc 🎉🎉🎉 "); }
那么当界面
pop
时,会走dealloc
么?结果不会走。在Developer Documentation
中可以了解到,NSTimer
在启动后,会对target
进行强引用:- 虽然
target
现在是个weak
指针,但是它依然指向self
内存,况且Runloop
->timer
->weakSelf
->self内存
,也就相当于Runloop
间接持有self
,所以不释放。- 使用
scheduledTimerWithTimeInterval
方法也不会释放,它是是self
->timer
->weakSelf
->self内存
不会释放
- 使用
- 这两种不释放的情况,如下图所示:
- 虽然
解决方案
- 下面将分析几种解决方案
一、NSTimer的block方式
-
我们知道
[NSTimer scheduledTimerWithTimeInterval: repeats: block:]
方法不会产生强引用,只需要注意block
对self
的循环引用,就可以解决不释放问题。代码如下:__weak typeof(self) weakSelf = self; self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:true block:^(NSTimer * _Nonnull timer) { [weakSelf addNumberAction]; }];
此时
block
块中的weakSelf
是临时变量,因此并没有对self
进行强持有,所以界面会走dealloc
方法。
二、pop时销毁NSTimer
- 通过上面分析我们可以知道
NSTimer
会强引用target
,如果在合适的时机销毁timer
也就断开
了这一层的强引用,进而就解决了不释放问题。 - 销毁
timer
,我们通常会想到在viewWillDisappear
的时候操作,不过这样会有一个问题,如果页面需要push
此时的界面没有销毁。所以此时销毁了定时器,等回到界面时定时器任务停止了,就达不到我们想要的效果了。 - 我们可以选择在界面
pop
时销毁定时器,pop
判断有以下方式:-
- 拿到当前
navigationController.viewControllers
数组,如果数组里找不到当前控制器,则为pop
操作:
- (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; NSArray *navigations = self.navigationController.viewControllers; if ([navigations indexOfObject:self] == NSNotFound) { // pop [self.timer invalidate]; self.timer = nil; } }
- 拿到当前
-
- 重写系统的
didMoveToParentViewController
方法,然后根据parent
判断。didMoveToParentViewController
方法会在页面加载后
和页面被移除后
调用。判断方法如下:
- (void)didMoveToParentViewController:(UIViewController *)parent { if (parent == nil) { [self.timer invalidate]; self.timer = nil; } }
- 重写系统的
-
这两种方式都可以销毁NSTimer
,进而解决强引用问题
三、中介模式
-
中介模式实质就是让
timer
持有其他target
,当timer
执行时会找到target
的IMP
,进而执行方法,这样就不会对self
进行强持有。具体代码如下:self.target = [NSObject alloc]; class_addMethod([NSObject class], @selector(addNumberAction), (IMP)wushuang, "v@:"); self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.target selector:@selector(addNumberAction) userInfo:nil repeats:true]; void wushuang(id obj) { NSLog(@"__ %s __ %@", __func__, obj); } - (void)addNumberAction { num++; NSLog(@"正在打印 --- %d", num); }
运行结果如下:
- 这种方式虽然可以修复
self
强引用问题,但是执行的是wushuang
这个函数,要执行addNumberAction
这个方法不太好处理,所以这个方法并不太好
- 这种方式虽然可以修复
四、自定义NSTimer
-
自定义
NSTimer
的主要思想是将定时器要执行的方法放在self(控制器)
,然后将timer
核心方法写在自定义timer
类,再结合中介模式
和objc_msgSend
调用self(控制器)
中的核心方法,这样也不会对self(控制器)
产生强引用。 -
具体实现如下:
// SecondViewController.m @interface SecondViewController () @property (nonatomic, strong) WSTimerWrapper *wrapperTimer; @end - (void)addNumberAction { num++; NSLog(@"正在打印 --- %d", num); } - (void)timerTest { self.wrapperTimer = [[WSTimerWrapper alloc] ws_timerWithTimeInterval:1 target:self selector:@selector(addNumberAction) userInfo:nil repeats:true]; } // WSTimerWrapper.h @interface WSTimerWrapper : NSObject - (instancetype)ws_timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo; @end // WSTimerWrapper.m @interface WSTimerWrapper () @property (nonatomic, weak) id aTarget; @property (nonatomic, strong) NSTimer *timer; @property (nonatomic, assign) SEL aSelector; @end @implementation WSTimerWrapper - (instancetype)ws_timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo { if (self == [super init]) { self.aTarget = aTarget; self.aSelector = aSelector; if ([self.aTarget respondsToSelector:self.aSelector]) { Method method = class_getInstanceMethod([self.aTarget class], aSelector); const char *type = method_getTypeEncoding(method); class_addMethod([self class], aSelector, (IMP)wsAddNum, type); self.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo]; } } return self; } void wsAddNum(WSTimerWrapper *wrapper) { if (wrapper.aTarget) { void (*ws_msgSend)(void *, SEL, id) = (void *)objc_msgSend; ws_msgSend((__bridge void *)(wrapper.aTarget), wrapper.aSelector, wrapper.timer); } else { [wrapper.timer invalidate]; wrapper.timer = nil; } } - (void)dealloc { NSLog(@" WSTimerWrapper 🍉🍉🍉 "); } @end
代码的核心流程图解如下:
- 从图解中可以看出
timer
并没有对vc
产生强持有,当vc
销毁时由于target
不存在了,所以就会调用销毁定时器
代码
- 从图解中可以看出
五、NSProxy虚基类
-
NSProxy
是一个抽象的超类,和NSObject
是同级的,它的Api
比较简单:
NSObject
也有消息转发,但是要经过一些列的条件最后才是消息慢转发,而NSProxy
调用转发逻辑则要简单的多。具体实现如下:// SecondViewController.m @interface SecondViewController () @property (nonatomic, strong) NSTimer *timer; @property (nonatomic, strong) WSProxy *proxy; @end - (void)timerTest { self.proxy = [WSProxy proxyWithTransformTarget:self]; self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(addNumberAction) userInfo:nil repeats:true]; } - (void)addNumberAction { num++; NSLog(@"正在打印 --- %d", num); } // WSProxy.h @interface WSProxy : NSProxy + (instancetype)proxyWithTransformTarget:(id)target; @end // WSProxy.m @interface WSProxy () @property (nonatomic, weak) id object; @end @implementation WSProxy + (instancetype)proxyWithTransformTarget:(id)target { WSProxy *proxy = [WSProxy alloc]; proxy.object = target; return proxy; } // 消息快转发 //- (id)forwardingTargetForSelector:(SEL)aSelector { // return self.object; //} // 消息慢转发 - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { if (self.object) { NSLog(@"+++ %@ +++", NSStringFromSelector(sel)); } else { NSLog(@"error : Signature has no object ~"); } return [self.object methodSignatureForSelector:sel]; } - (void)forwardInvocation:(NSInvocation *)invocation { if (self.object) { [invocation invokeWithTarget:self.object]; } else { NSLog(@"error : invocation has no Target ~"); } } @end
- 定时器强引用的是
WSProxy
对象,而WSProxy
弱引用VC
,然后WSProxy
不能执行sel
,于是将sel
方法转发给vc
,这样就完成了timer
的调用,且不会产生对vc
的强持有 - 代码中无论是消息的
慢转发
还是快转发
都可以达到消除timer
对vc
的强持有。
- 定时器强引用的是
今天的文章iOS底层-内存管理之强引用分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/22465.html