问题抛出
我们先来看一个案例,创建一个空工程,创建三个导航控制器,分别叫 A、B、C。添加如下逻辑:A 界面 push
到 B 界面,B 界面 push
到 C 界面,在 B 界面添加定时器,如下
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
- (void)fireHome {
num++;
NSLog(@"hello word - %d",num);
}
- (void)dealloc {
[self.timer invalidate];
self.timer = nil;
NSLog(@"%s",__func__);
}
当 B 控制器 push
到 C 时,定时器依然在执行,是我们希望看到的,但是当从 B 控制器 pop
到 A 时,定时器依然在执行,这个是我们不愿意看到的,我明明在 dealloc
方法中已经让它释放了,为什么依然在执行呢?
运行代码,我们可以看到,当从 B 控制器 pop
到 A 时,根本就没有走 B 的 dealloc
方法(即 B 控制器没有释放)。针对以上问题,我们先去官方文档查看下 timerWithTimeInterval:target:selector:userInfo:repeats:
的使用描述
从文档中可以看出,timer
对传入的 target
具有强持有(即 timer
持有 self
),由于 timer
定义在 B 控制器里,所以 self
也持有 timer
,这样就形成了 self -> timer -> self
循环引用。
按照以往的循环引用解决方法 strong-weak dance
通过弱引用来尝试解决下,代码修改如下
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:weakSelf.timer forMode:NSRunLoopCommonModes];
再次运行项目,重复上面的动作,发现问题依旧(定时器依然存在,dealloc 方法也没执行)。这是为什么呢?
问题分析
weakSelf
和 self
它们是同一个对象吗?weakself
会对引用计数加 1 吗?我们添加如下代码,打印 weakSelf
前后的引用计数
NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)self));
__weak typeof(self) weakSelf = self;
NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)self));
运行项目,打印结果如下
打印结果都是 8,说明 weakSelf
没有对内存引用计数加 1。现在在 weakSelf
下面添加一个断点,分别打印 self
和 weakSelf
对象以及它们的内存地址,如下
从打印结果可以看到,self
和 weakSelf
取地址的值是不一样的,但是它们指向的是同一片内存空间(即它们是两个指针,内存地址不一样,指向了同一片内存空间)。如下图所示
这里
timer
捕获的是weakSelf
对象(即<PushViewController: 0x7fcb3cc0cb60>
,那片内存地址),无法通过weakSelf
来解决强持有。
RunLoop 强引用
在上面的分析中,我们其实还忽略了一点,RunLoop
对 timer
的强持有,官方文档说明如下
RunLoop
对整个对象的空间有强持有,RunLoop
不停止,timer
和 self
是无法释放的。
- 所以起初的引用链应该是这样的
- 加上
weakSelf
后是这样子
block 与 timer 循环引用的区别
- timer
引用链为: self -> timer -> weakSelf -> self
,当前 timer
捕获的是 内存
(weakSelf
表示的是 vc对象
)
- block
在 block
底层原理的方法 _Block_object_assign
可知,block
捕获的是 对象的指针地址
(即 weakSelf
这个临时变量的指针地址)。
引用链为: self -> block -> weakSelf -> self
,当前 block
捕获的是 指针地址
(weakSelf
表示的是指向 self
的临时变量的指针地址,最后通过指针地址找到内存 self
)
解决思路
由于 Runloop
对 timer
的强持有,导致了 Runloop
间接的强持有了 self
(因为 timer
中捕获的是 vc对象
),所以导致 dealloc
方法无法执行。我们需要 打破这一层强持有
或者使用 中介者模式
来解决。
思路一
既然 dealloc
不能来,那我们看看有没有其他的方法在 pop
的时候就销毁 timer
,把核心 timer
销毁,那么强持有 - 循环引用就不存在了。有没有这个方法呢?didMoveToParentViewController
闪亮登场了
- 在 B 界面重写
didMoveToParentViewController
方法
- (void)didMoveToParentViewController:(UIViewController *)parent {
if (parent == nil) {
[self.timer invalidate];
self.timer = nil;
}
}
didMoveToParentViewController
是用于当一个视图控制器中添加或者移除viewController
后,必须调用的方法。目的是为了告诉iOS
,已经完成添加/删除子控制器的操作。
思路二
在计时器模式中,我们主要关心的是 fireHome
是否能够正常运行,并不关心 timer
捕获的 target
是谁,这里不方便使用 self
,那我们就换个其他对象(即中介者模式),如下
static int num = 0;
@interface PushViewController ()
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) id target;
@end
@implementation PushViewController
void fireHomeObjc() {
NSLog(@"%s", __func__);
}
- (void)viewDidLoad {
[super viewDidLoad];
self.target = [[NSObject alloc] init];
class_addMethod([NSObject class], @selector(fireHome), (IMP)fireHomeObjc, "V@:");
self.timer = [NSTimer timerWithTimeInterval:1 target:self.target selector:@selector(fireHome) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
- (void)fireHome {
num++;
NSLog(@"hello word - %d",num);
}
- (void)dealloc {
NSLog(@"%s",__func__);
}
@end
再次运行项目,执行上述操作,可以看到,当 pop
回来时,确实走了 dealloc
方法,但是此时计时器还是运行,也就是说 self
是释放了 ,但是 中介者
对象没有释放。在 dealloc
方法中添加 timer
的销毁,如下
- (void)dealloc {
[self.timer invalidate];
self.timer = nil;
NSLog(@"%s",__func__);
}
再次运行,发现 pop
之后,timer
释放,从而中介者也会呗回收了
思路三
根据思路二的原理,我们可以自定义封装一个 timer
- 创建一个类,继承自
NSObject
- 创建一个类,继承自
- 初始化方法中,接收
vc
传递来的参数,判断self.target
是否能够响应self.aSelector
(即vc
对象能否响应aSelector
),如果能响应,当前自定义类添加aSelector
方法实现
- 初始化方法中,接收
- 定义一个
timer
,设置target
为本身(即LCTimerWapper
中的timer
一直监听自己),走当前类的aSelector
方法
- 定义一个
- 定时器执行方法
- target 存在,需要让
vc
知道,即向传入的target
发送selector
消息,并将此时的timer
参数也一并传入,所以vc
就可以得知fireHome
方法,就这事这种方式定时器方法能够执行的原因 - target 不存在,说明已经释放了,则释放当前的
timerWrapper
,即打破了RunLoop
对timeWrapper
的强持有
- target 存在,需要让
- 定时器执行方法
-
lc_invalidate
方法释放timer
。需要在vc
的dealloc
方法中调用,即vc
释放,从而导致timerWapper
释放,打破了vc
对timeWrapper
的的强持有
-
自定义封装代码如下
/**------.h 文件------*/
#import <Foundation/Foundation.h>
@interface LCTimerWapper : NSObject
- (instancetype)lc_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;
- (void)lc_invalidate;
@end
/**------.m 文件------*/
#import "LCTimerWapper.h"
#import <objc/message.h>
@interface LCTimerWapper ()
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL aSelector;
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation LCTimerWapper
- (instancetype)lc_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(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);
// 给 LCTimerWapper 添加方法
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 (LCTimerWapper *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)lc_invalidate {
[self.timer invalidate];
self.timer = nil;
}
- (void)dealloc
{
NSLog(@"%s", __func__);
}
@end
使用方法
self.timerWapper = [[LCTimerWapper alloc] lc_initWithTimeInterval:1.0 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
- (void)fireHome {
// 需要法执行的代码
}
- (void)dealloc {
[self.timerWapper lc_invalidate];
NSLog(@"%s",__func__);
}
这种方式看起来比较繁琐,步骤很多,而且针对 timerWapper
,需要不断的添加 method
,需要进行一系列的处理
思路四
下面介绍一个终极大招,NSProxy
虚基类,通过虚基类,交给其子类实现,关于 NSProxy
可以参考 关于 NSProxy 的理解与运用
- 首先,自定义一个继承自
NSProxy
的子类
/**------.h 文件------*/
#import <Foundation/Foundation.h>
@interface LCProxy : NSProxy
+ (instancetype)proxyWithTransformObject:(id)object;
@end
/**------.m 文件------*/
#import "LCProxy.h"
@interface LCProxy ()
@property (nonatomic, weak) id object;
@end
@implementation LCProxy
+ (instancetype)proxyWithTransformObject:(id)object {
LCProxy *proxy = [LCProxy alloc];
proxy.object = object;
return proxy;
}
// 仅仅添加了 weak 类型的属性还不够,为了保证中间件能够响应外部 self 的事件,需要通过消息转发机制,让实际的响应 target 还是外部 self,这一步至关重要,主要涉及到 runtime 的消息机制。
// 转移
// 强引用 -> 消息转发
// 方法一
- (id)forwardingTargetForSelector:(SEL)aSelector {
return self.object;
}
// 方法二
//- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
// if (self.object && [self.object respondsToSelector:sel]) {
// return [self.object methodSignatureForSelector:sel];
// }
// return [super methodSignatureForSelector:sel];
//}
//
//- (void)forwardInvocation:(NSInvocation *)invocation {
// SEL sel = invocation.selector;
// if (self.object && [self.object respondsToSelector:sel]) {
// [invocation invokeWithTarget:self.object];
// }
// else {
// [super forwardInvocation:invocation];
// }
//}
@end
- 定义一个
timer
,将target
设置为NSProxy
子类对象,即timer
持有NSProxy
子类对象
@property (nonatomic, strong) LCProxy *proxy;
self.proxy = [LCProxy proxyWithTransformObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES];
- (void)fireHome {
// 执行代码
}
- (void)dealloc {
[self.timer invalidate];
self.timer = nil;
NSLog(@"%s",__func__);
}
从以上可以看出,将强引用的注意力转移成了消息转发。虚基类只负责消息转发,即使用 NSProxy
作为中间代理、中间者
在
dealloc
方法中vc
会正常释放,继而proxy
也会正常释放
timer
释放了也打破了runLoop对proxy的强持有
,不再有强持有了
思路五
定义 NSTimer
时可以采用 闭包
的形式,就不要指定 target
了
- (void)blockTimer {
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer fire - %@",timer);
}];
}
网友评论