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 16 更新版本时,我们听到了困扰 Apple...
欧版/美版 特别说一下,美版选错了 可能会永久丧失4G,不过只...
一般在接外包的时候, 通常第三方需要安装你的app进行测...
前言为了让更多的人永远记住12月13日,各大厂都在这一天将应...