iOS之深入解析内存管理NSTimer的强引用问题
发布时间:2023-04-06 11:31:51 所属栏目:教程 来源:
导读:强引用问题分析现在有两个控制器A、B,从Apush到B控制器,在B控制器中有如下代码: self.timer=[NSTimertimerWithTimeInterval:1target:selfselector:@selector(popHome)userInfo:nilrepeats:YES]; [[NSRunLoopcurre
|
强引用问题分析现在有两个控制器A、B,从Apush到B控制器,在B控制器中有如下代码: self.timer=[NSTimertimerWithTimeInterval:1target:selfselector:@selector(popHome)userInfo:nilrepeats:YES]; [[NSRunLoopcurrentRunLoop]addTimer:self.timerforMod 一、强引用问题分析 现在有两个控制器 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 fire - %@", 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)fireHomeWapper, type); // 启动一个timer,target是self,即监听自己 self.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo]; } } return self; } // 一直执行 runloop void fireHomeWapper(YDWTimerWapper *wapper){ // 判断target是否存在 if (wapper.target) { // 如果存在则需要让vc知道,即向传入的target发送selector消息,并将此时的timer参数也一并传入,所以vc就可以得知`fireHome`方法,就这事这种方式定时器方法能够执行的原因 // 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] cjl_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。 (编辑:汽车网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
推荐文章
站长推荐
