iOS之深入解析内存管理NSTimer的强引用问题
Posted Forever_wj
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS之深入解析内存管理NSTimer的强引用问题相关的知识,希望对你有一定的参考价值。
一、强引用问题分析
- 现在有两个控制器 A、B,从 A push 到 B 控制器,在 B 控制器中有如下代码:
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(popHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
- 当从控制器 B pop 回到控制器 A 时,我们发现定时器没有停止,其 popHome 方法仍然在执行,这是为什么呢?
- 在控制器 B 的 dealloc 方法打上断点,可以看到程序并没有执行。因此可以得出,控制器 B 没有被释放,即控制器 B 没有执行 dealloc 方法,从而导致 timer 也无法停止运行和释放。
- 重写 didMoveToParentViewController 方法,可以看到:当控制器 B 退出到上层控制器的时候消除了引用,dealloc 方法被调用,timer 被销毁:
- (void)didMoveToParentViewController:(UIViewController *)parent {
if (parent == nil) {
[self.timer invalidate];
self.timer = nil;
NSLog(@"timer 被释放");
}
}
- 定义 timer 时,可以采用闭包的形式,不需要指定 target,就不会产生 timer 无法被释放的问题:
- (void)blockTimer {
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer pop - %@", timer);
}];
}
- 经过上面的两种方式,都可以正常处理 timer 释放的问题,那么这又是为什么呢?
- 通过查看官方文档对 timerWithTimeInterval:target:selector:userInfo:repeats: 方法中对 target 的描述:
The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to this object until it (the timer) is invalidated.
timer强引用了target,直接对target所指向的内存地址强引用
- 从文档中描述可以看出,timer 对传入的 target 具有强持有,即 timer 持有 self,又由于 timer 是定义在控制器 B 中,所以 self 也持有 timer,因此 self -> timer -> self 构成了循环引用。
- 我们知道:循环引用可以通过 __weak 即弱引用来解决,那么我们代码修改如下:
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(popHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
- 再次运行程序,进行 push-pop 跳转,却发现问题还是存在,即定时器方法仍然在执行,并没有执行 B 的 dealloc 方法,这是为什么呢?
- 使用 __weak 虽然打破了 self -> timer -> self 之前的循环引用,即引用链变成了 self -> timer -> weakSelf -> self,但是我们遗漏了一个点,Runloop 对 timer 也强持有,因为 Runloop 的生命周期比控制器 B 更长,所以导致了 timer 无法被释放,同时也导致了控制器 B 的 self 也无法被释放。
- 没有添加 weakSelf 之前的引用链如下:
- 添加 weakSelf 之后的引用链变成了如下所示:
二、weakSelf 与 self
- 对于 weakSelf 和 self,我们关心的是:
-
- weakSelf 会对引用计数进行 +1 操作吗?
-
- weakSelf 和 self 的指针地址相同吗,是指向同一片内存吗?
- 在添加 weakSelf 前后打印 self 的引用计数:
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
__weak typeof(self) weakSelf = self;
NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
- 运行程序,可以看到前后 self 的引用计数都是 8,因此可以判定 weakSelf 没有对内存进行 +1 操作。
- 继续打印 weakSelf 和 self 对象,以及指针地址:
po weakSelf
<ViewController: 0x7fea4f024200>
po self
<ViewController: 0x7fea4f024200>
p &self
(ViewController **) $4 = 0x00000001085a5fc8
p &weakSelf
(ViewController *const *) $5 = 0x00007ffeeb06b648
- 可以看出,当前 self 取地址和 weakSelf 取地址的值是不一样的,意味着有两个指针地址,指向的是同一片内存空间,即 weakSelf 和 self 的内存地址是不一样,都指向同一片内存空间的。
- 此时 timer 捕获的是 <ViewController: 0x7fea4f024200>,是一个对象,所以无法通过 weakSelf 来解决强持有,即引用链关系为:NSRunLoop -> timer -> weakSelf(<ViewController: 0x7fea4f024200>),所以 RunLoop 对整个对象的空间强持有,runloop 没停,timer 和 weakSelf 就无法被释放。
- block 的循环引用,与 timer 的是有区别的,通过 block 底层原理的方法 __Block_object_assign 可知,block 捕获的是对象的指针地址,即 weakself 是临时变量的指针地址,与 self 无关,因为 weakSelf 是新的地址空间,所以此时的 weakSelf 相当于中间值,其引用关系链为 self -> block -> weakSelf(临时变量的指针地址),可以通过地址拿到指针。
- block 和 timer 循环引用的模型如下:
-
- timer 模型:self -> timer -> weakSelf -> self,当前的 timer 捕获的是控制器 B 的内存,即 vc 对象的内存,即 weakSelf 表示的是 vc 对象;
-
- Block 模型:self -> block -> weakSelf -> self,当前的 block 捕获的是指针地址,即 weakSelf 表示的是指向 self 的临时变量的指针地址。
三、强引用的解决方案
① 当 controller 界面 pop 到上层界面的消除引用
- 根据上文中的分析中,由于 Runloop 对 timer 的强持有,导致 Runloop 间接的强持有了self(因为 timer 中捕获的是 vc 对象),所以导致 dealloc 方法无法执行,需要查看在 pop 时,是否还有其他方法可以销毁 timer,这个方法就是 didMoveToParentViewController。
- didMoveToParentViewController 方法,是用于当一个视图控制器中添加或者移除 viewController 后,必须调用的方法,目的是为了告诉系统,已经完成添加/删除子控制器的操作。
- (void)didMoveToParentViewController:(UIViewController *)parent {
if (parent == nil) {
[self.timer invalidate];
self.timer = nil;
NSLog(@"timer 被释放");
}
}
② 中介者模式,不直接使用 self
- 在 timer 模式中,主要是 popHome 能执行,并不用管 timer 捕获的 target 是谁,由于这里不能使用self(因为会有强持有问题),所以可以将 target 换成其他对象,例如将 target 换成 NSObject 对象,将 popHome 交给 target 执行:
// 定义其他对象
@property (nonatomic, strong) id target;
// 修改target
self.target = [[NSObject alloc] init];
class_addMethod([NSObject class], @selector(popHome), (IMP)popHomeObjc, "v@:");
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.target selector:@selector(popHome) userInfo:nil repeats:YES];
// imp
void popHomeObjc(id obj){
NSLog(@"%s -- %@", __func__, obj);
}
- 运行程序,发现程序执行 dealloc 之后,timer 还是会继续执行,这是因为虽然解决了中介者的释放,但是没有解决中介者的回收,即 self.target 的回收。
- 继续通过在 dealloc 方法中,取消定时器来解决,代码如下:
- (void)dealloc{
[self.timer invalidate];
self.timer = nil;
NSLog(@"%s", __func__);
}
- 再次运行程序如下,发现 pop 之后,timer 被释放,从而中介者也会进行回收释放。
③ 自定义封装 timer
- 自定义 timerWapper:
-
- 在初始化方法中,定义一个 timer,其 target 是自己,即 timerWapper 中的 timer,一直监听自己,判断 selector,此时的 selector 已交给了传入的 target(即 vc 对象),此时有一个方法 popHomeWapper,在方法中,判断 target 是否存在;
-
-
- 如果 target 存在,则需要让 vc 知道,即向传入的 target 发送 selector 消息,并将此时的 timer 参数也一并传入,所以 vc 就可以得知 popHome 方法,就这事这种方式定时器方法能够执行的原因 ;
-
-
-
- 如果 target 不存在,已经释放了,则释放当前的 timerWrapper,即打破了 RunLoop 对 timeWrapper 的强持有 (timeWrapper <-×- RunLoop);
-
-
- 自定义 ydw_invalidate 方法中释放 timer,这个方法在 vc 的 dealloc 方法中调用,即 vc 释放,从而导致 timerWapper 释放,打破了 vc 对 timeWrapper 的强持有( vc -×-> timeWrapper);
// .h文件
@interface YDWTimerWapper : NSObject
- (instancetype)ydw_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
- (void)ydw_invalidate;
@end
// .m文件
#import "YDWTimerWapper.h"
#import <objc/message.h>
@interface YDWTimerWapper ()
@property(nonatomic, weak) id target;
@property(nonatomic, assign) SEL aSelector;
@property(nonatomic, strong) NSTimer *timer;
@end
@implementation YDWTimerWapper
- (instancetype)ydw_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo {
if (self == [super init]) {
// 传入vc
self.target = aTarget;
// 传入的定时器方法
self.aSelector = aSelector;
if ([self.target respondsToSelector:self.aSelector]) {
Method method = class_getInstanceMethod([self.target class], aSelector);
const char *type = method_getTypeEncoding(method);
// 给timerWapper添加方法
class_addMethod([self class], aSelector, (IMP)popHomeWapper, type);
// 启动一个timer,target是self,即监听自己
self.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo];
}
}
return self;
}
// 一直执行 runloop
void popHomeWapper(YDWTimerWapper *wapper){
// 判断target是否存在
if (wapper.target) {
// 如果存在则需要让vc知道,即向传入的target发送selector消息,并将此时的timer参数也一并传入,所以vc就可以得知`popHome`方法,就这事这种方式定时器方法能够执行的原因
// objc_msgSend发送消息,执行定时器方法
void (*lg_msgSend)(void *,SEL, id) = (void *)objc_msgSend;
lg_msgSend((__bridge void *)(wapper.target), wapper.aSelector,wapper.timer);
} else {
// 如果target不存在,已经释放了,则释放当前的timerWrapper
[wapper.timer invalidate];
wapper.timer = nil;
}
}
// 在vc的dealloc方法中调用,通过vc释放,从而让timer释放
- (void)ydw_invalidate {
[self.timer invalidate];
self.timer = nil;
}
- (void)dealloc {
NSLog(@"%s",__func__);
}
@end
- timerWapper 的使用:
// 定义
self.timerWapper = [[YDWTimerWapper alloc] ydw_initWithTimeInterval:1 target:self selector:@selector(popHome) userInfo:nil repeats:YES];
// 释放
- (void)dealloc {
[self.timerWapper ydw_invalidate];
}
④ 利用 NSProxy 虚基类的子类
- 定义一个继承自 NSProxy 的子类:
// NSProxy子类
@interface YDWProxy : NSProxy
+ (instancetype)proxyWithTransformObject:(id)object;
@end
@interface YDWProxy()
@property (nonatomic, weak) id object;
@end
@implementation YDWProxy
+ (instancetype)proxyWithTransformObject:(id)object{
YDWProxy *proxy = [YDWProxy alloc];
proxy.object = object;
return proxy;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return self.object;
}
- 将 timer 中的 target 传入 NSProxy 子类对象,即 timer 持有 NSProxy 子类对象:
// 解决timer强持有问题
self.proxy = [YDWProxy proxyWithTransformObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(popHome) userInfo:nil repeats:YES];
// 在dealloc中将timer正常释放
- (void)dealloc {
[self.timer invalidate];
self.timer = nil;
}
- 这样将强引用的注意力转移成了消息转发,虚基类只负责消息转发,即使用 NSProxy 作为中间代理和中间者。
- 那么定义的 proxy 对象,在 dealloc 释放时,还存在吗?其实,proxy 对象会正常被释放,因为 vc 被释放,所以可以释放其持有者,即 timer 和 proxy,timer 的释放也打破了 runLoop 对 proxy 的强持有,完美的达到了两层释放,即 vc -×-> proxy <-×- runloop。
以上是关于iOS之深入解析内存管理NSTimer的强引用问题的主要内容,如果未能解决你的问题,请参考以下文章
iOS之深入解析内存管理Tagged Pointer的底层原理
iOS之深入解析内存管理retain与release的底层原理