一、强引用问题分析

  • 现在有两个控制器 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 = 0x00000001085a5fc8p &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;// 修改targetself.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];// impvoid 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]) {// 传入vcself.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;}// 一直执行 runloopvoid 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的强引用问题相关推荐

  1. iOS之深入解析内存管理的引用计数retainCount的底层原理

    一.简介 ① 引用计数概念 OC 在创建对象时,不会直接返回该对象,而是返回一个指向对象的指针. OC 在内存管理上采用了引用计数,它是一个简单而有效管理对象生命周期的方式. 在对象内部保存一个用来表 ...

  2. iOS之深入解析内存管理Tagged Pointer的底层原理

    一.前言 ① Tagged Pointer 概念 iOS 开发者对"引用计数"这个名词肯定不陌生,引用计数是苹果为了方便开发者管理内存而引入的一个概念.当引用计数为 0 时,对象就 ...

  3. iOS之深入解析内存管理retain与release的底层原理

    一.内存管理 ① 内存管理原理 iOS 的每个对象内部都保存了一个与之相关联的整数,称为引用计数器(auto reference count): 每当使用 alloc.new 或者 copy 创建一个 ...

  4. iOS之深入解析内存管理MRC与ARC机制

    一.内存管理 ① 什么是内存管理? 当我们编写程序的时候,会声明各种各样的变量,编写各种各样的代码,它们都会占用内存,但是并不是所有的代码和内存都是由我们进行释放. 内存分为 5 个区域:栈.堆.bs ...

  5. iOS之深入解析内存管理散列表SideTables和弱引用表weak_table的底层原理

    一.SideTables 和 weak_table 的关系 在 runtime 中,有四个数据结构非常重要,分别是 SideTables,SideTable,weak_table_t 和 weak_e ...

  6. 理解 iOS 和 macOS 的内存管理

    在 iOS 和 macOS 应用的开发中,无论是使用 Objective-C 还是使用 swift 都是通过引用计数策略来进行内存管理的,但是在日常开发中80%(这里,我瞎说的,8020 原则嘛?)以 ...

  7. 关于IOS的多任务以及内存管理

    看了很多FY为自己的可用内存是350MB还是380MB纠结.为了多优化出一点可用内存费脑筋.  IOS的任务管理和内存管理,跟windows是有很大差别的.很多FY习惯于用 windows的思维去看待 ...

  8. iOS底层原理之内存管理

    文章目录 定时器 CADisplayLink.NSTimer GCD定时器 内存管理 iOS程序的内存布局 Tagged Pointer OC对象的内存管理 拷贝 引用计数的存储 dealloc 自动 ...

  9. Redis源码解析——内存管理

    在<Redis源码解析--源码工程结构>一文中,我们介绍了Redis可能会根据环境或用户指定选择不同的内存管理库.在linux系统中,Redis默认使用jemalloc库.当然用户可以指定 ...

最新文章

  1. 某度网盘转存限制500个文件?这个软件帮你搞定!
  2. python基础——元组、文件及其它
  3. Dockerd docker-containerd docker-containerd-shim runC
  4. 10分钟上线 - API网关 + 函数计算实现图片处理服务
  5. linux-基本开发环境搭建
  6. 95-24-020-Future-Future简介
  7. android studio gradle 更新方法。
  8. DOS批处理全面教程
  9. HTML5的离线存储有几种方式?
  10. Android之输入银行卡号判断属于哪个银行
  11. 孔雀东南飞用mysql存储_孔雀东南飞的故事简介800字(孔雀东南飞主要内容介绍)...
  12. 计算机学院迎条幅,会计学院迎新标语条幅
  13. MySQL auto_increment介绍及自增键断层的原因分析
  14. Python在命令行模式下如何退出命令行
  15. 网络与信息安全产品(一)
  16. 内存和FLASH的区别
  17. 计算机b级及格线,计算机一级B考试有那些内容?理论部分不几个就算总分及格了也不算合格吗?...
  18. 一、自动化RPA大纲
  19. Window10 防火墙设置端口访问
  20. ok6410linux开发环境搭建,飞凌嵌入式知识汇021期:OK6410裸机程序之开始模板(Linux环境)...

热门文章

  1. C# WinForm程序退出的方法比较
  2. iOS 控制屏幕横竖屏旋转
  3. android menu 小红点,Android自定义ActionProvider ToolBar实现Menu小红点
  4. rg1 蓝光危害rg0_LED(护眼)台灯|蓝光那些事
  5. 箭头函数特殊性与普通函数的区别
  6. Java黑皮书课后题第5章:*5.47(商业:检测ISBN-13)ISBN-13是标识书籍的新标准。它使用13位数字d1d2d3~d12d13,d13是校验和。如果校验和为10,则替换为0。求所有数字
  7. MongoDB 索引-9
  8. 自制一个 简易jQuery 的 API
  9. websocket实现单聊
  10. 使用 scikit-learn 实现多类别及多标签分类算法