iOS之深入解析内存管理NSTimer的强引用问题
2021/6/20 7:27:06
本文主要是介绍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 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。
这篇关于iOS之深入解析内存管理NSTimer的强引用问题的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-08-29Axios库资料:新手入门指南与基本使用教程
- 2024-03-14system bios shadowed
- 2024-03-14gabios
- 2024-02-07iOS应用提交上架的最新流程
- 2024-02-06打包 iOS 的 IPA 文件
- 2023-12-07uniapp打包iOS应用并通过审核:代码混淆的终极解决方案 ?
- 2023-11-25uniapp IOS从打包到上架流程(详细简单) 原创
- 2023-11-10【iOS开发】iOS App的加固保护原理:使用ipaguard混淆加固
- 2023-09-30最强大的iOS应用源码保护工具:Ipa Guard,保护你的商业机密代码
- 2023-09-07iOS安全加固探讨:代码混淆、类名方法名混淆等方法