本文主要是通过定时器
来梳理强引用
的几种解决方案
强应用(强持有)
假设此时有两个界面A、B,从A push
到B界面,在B界面中有如下定时器代码。当从B pop
回到A界面[图片上传中...(E70D3F5D-8815-4138-BFDD-017B1BFCE0E7.png-6861f8-1609331145410-0)]
时,发现定时器没有停止,其方法仍然在执行,为什么?
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
其主要原因是B界面没有释放
,即没有执行dealloc
方法,导致timer也无法停止和释放
解决方式一
- 重写
didMoveToParentViewController
方法
- (void)didMoveToParentViewController:(UIViewController *)parent{ // 无论push 进来 还是 pop 出去 正常跑 // 就算继续push 到下一层 pop 回去还是继续 if (parent == nil) { [self.timer invalidate]; self.timer = nil; NSLog(@"timer 走了"); } }
解决方式二
- 定义timer时,采用
闭包
的形式,因此不需要指定target
- (void)blockTimer{ self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"timer fire - %@",timer); }]; }
现在,我们从底层来深入研究,为什么B
界面有了timer
之后,导致B界面释放不掉,即不会走到dealloc
方法。我们可以通过官方文档查看timerWithTimeInterval:target:selector:userInfo:repeats:
方法中对target的描述
从文档中可以看出,timer对传入的target具有强持有,即timer
持有self
。由于timer是定义在B界面中,所以self也持有timer
,因此 self -> timer -> self
构成了循环引用
在iOS-底层原理 30:Block底层原理文章中,针对循环应用提供了几种解决方式。我们我们尝试通过__weak
即弱引用
来解决,代码修改如下
__weak typeof(self) weakSelf = self; self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) 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 与 self
对于weakSelf
和 self
,主要有以下两个疑问
- 1、
weakSelf
会对引用计数进行+1
操作吗? - 2、
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 po self po &weakSelf po &self
结果如下
从打印结果可以看出,当前self
取地址 和 weakSelf
取地址的值是不一样的。意味着有两个指针地址,指向的是同一片内存空间
,即weakSelf 和 self 的内存地址是不一样,都指向同一片内存空间
的
- 从上面打印可以看出,此时
timer
捕获的是<LGTimerViewController: 0x7f890741f5b0>
,是一个对象
,所以无法通过weakSelf来解决强持有
。即引用链关系为:NSRunLoop -> timer -> weakSelf(<LGTimerViewController: 0x7f890741f5b0>)
。所以RunLoop对整个 对象的空间有强持有
,runloop没停,timer 和 weakSelf是无法释放的 - 而我们在
Block
原理中提及的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的临时变量的指针地址
解决 强引用(强持有)
以下几种方法的思路均是:依赖中介者模式
,打破强持有
,其中推荐思路四
思路一:pop时在其他方法中销毁timer
根据前面的解释,我们知道由于Runloop对timer的强持有
,导致了Runloop间接的强持有了self
(因为timer中捕获的是vc对象
)。所以导致dealloc
方法无法执行。需要查看在pop
时,是否还有其他方法可以销毁timer
。这个方法就是didMoveToParentViewController
didMoveToParentViewController
方法,是用于当一个视图控制器中添加或者移除viewController后,必须调用的方法。目的是为了告诉iOS,已经完成添加/删除子控制器的操作。- 在B界面中重写
didMoveToParentViewController
方法
- (void)didMoveToParentViewController:(UIViewController *)parent{ // 无论push 进来 还是 pop 出去 正常跑 // 就算继续push 到下一层 pop 回去还是继续 if (parent == nil) { [self.timer invalidate]; self.timer = nil; NSLog(@"timer 走了"); } }
思路二:中介者模式,即不使用self,依赖于其他对象
在timer模式中,我们重点关注的是fireHome
能执行,并不关心timer捕获的target
是谁,由于这里不方便使用self
(因为会有强持有问题),所以可以将target换成其他对象
,例如将target换成NSObject对象
,将fireHome
交给target
执行
- 将timer的target 由self改成objc
//**********1、定义其他对象********** @property (nonatomic, strong) id target; //**********1、修改target********** self.target = [[NSObject alloc] init]; class_addMethod([NSObject class], @selector(fireHome), (IMP)fireHomeObjc, "v@:"); self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.target selector:@selector(fireHome) userInfo:nil repeats:YES]; //**********3、imp********** void fireHomeObjc(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
这种方式是根据思路二的原理,自定义封装timer,其步骤如下
- 自定义timerWapper
- 在初始化方法中,定义一个timer,其target是自己。即
timerWapper
中的timer
,一直监听自己,判断selector
,此时的selector已交给了传入的target(即vc对象),此时有一个方法fireHomeWapper
,在方法中,判断target是否存在
- 如果
target存在
,则需要让vc知道,即向传入的target发送selector消息,并将此时的timer参数也一并传入,所以vc就可以得知fireHome
方法,就这事这种方式定时器方法能够执行的原因 - 如果
target不存在
,已经释放了,则释放当前的timerWrapper,即打破了RunLoop对timeWrapper的强持有 (timeWrapper <-×- RunLoop
)
- 自定义
cjl_invalidate
方法中释放timer。这个方法在vc的dealloc方法中调用,即vc释放,从而导致timerWapper释放
,打破了vc
对timeWrapper
的的强持有(vc -×-> timeWrapper
)
//*********** .h文件 *********** @interface CJLTimerWapper : NSObject - (instancetype)cjl_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo; - (void)cjl_invalidate; @end //*********** .m文件 *********** #import "CJLTimerWapper.h" #import <objc/message.h> @interface CJLTimerWapper () @property(nonatomic, weak) id target; @property(nonatomic, assign) SEL aSelector; @property(nonatomic, strong) NSTimer *timer; @end @implementation CJLTimerWapper - (instancetype)cjl_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(CJLTimerWapper *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)cjl_invalidate{ [self.timer invalidate]; self.timer = nil; } - (void)dealloc { NSLog(@"%s",__func__); } @end
- timerWapper的使用
//定义 self.timerWapper = [[CJLTimerWapper alloc] cjl_initWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES]; //释放 - (void)dealloc{ [self.timerWapper cjl_invalidate]; }
运行结果如下
这种方式看起来比较繁琐,步骤很多,而且针对timerWapper
,需要不断的添加method,需要进行一系列的处理。
思路四:利用NSProxy虚基类的子类
下面来介绍一种timer
强引用最常用
的处理方式:NSProxy子类
可以通过NSProxy
虚基类,可以交给其子类实现,NSProxy的介绍在iOS-底层原理 30:Block底层原理已经介绍过了,这里不再重复
- 首先定义一个继承自
NSProxy
的子类
//************NSProxy子类************ @interface CJLProxy : NSProxy + (instancetype)proxyWithTransformObject:(id)object; @end @interface CJLProxy() @property (nonatomic, weak) id object; @end @implementation CJLProxy + (instancetype)proxyWithTransformObject:(id)object{ CJLProxy *proxy = [CJLProxy alloc]; proxy.object = object; return proxy; } -(id)forwardingTargetForSelector:(SEL)aSelector { return self.object; }
- 将
timer
中的target
传入NSProxy子类对象
,即timer持有NSProxy子类对象
//************解决timer强持有问题************ self.proxy = [CJLProxy proxyWithTransformObject:self]; self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) 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
,解释如下:
- vc释放,导致了
proxy
的释放 - dealloc方法中,timer进行了释放,所以runloop强引用也释放了