一、准备
timer的创建
第一种:
- 如果在主线程里创建,需要修改下Mode为NSRunLoopCommonModes,不然,当滚动事件发生时,会导致NSTimer不执行,主线程的RunLoop是默认开启的,所以不需要[[NSRunLoop currentRunLoop] run]。
- 如果在子线程里创建,且当前线程里无滚动事件,则不需要修改Mode,子线程的RunLoop默认不开启的,需要手动加入
Runloop
:
__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:NSDefaultRunLoopMode];
- (void)fireHome {
num++;
NSLog(@"hello word - %d",num);
}
第二种:
另一种创建timer
方法,自动加入Runloop
无需手动添加,等同于上述方法:
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1
target:weakSelf
selector:@selector(fireHome)
userInfo:nil
repeats:YES];
二、timer循环引用分析:
1. timer循环引用分析
self -> timer -> self
循环引用分析:
-
self
强持有timer
我们都能直接看出来,那么timer
是什么时候强持有self
的呢?看苹果官方文档可知:target
方法中,timer
对self
对象进行了强持有,因此造成了循环引用。 - 但是当我们按照惯例用
weakSelf
去打破强引用的时候,发现weakSelf
没有打破循环引用,timer
仍然在运行。 - 即
self -> timer -> weakSelf -> self
。
2. __weak typeof(self) weakSelf = self分析
从上面我们会疑惑为什么block
中 self -> block -> wealSelf -> self
可以打破循环引用,而 self -> timer -> weakSelf -> self
无法打破呢?
带着这个疑问,我们要了解__weak typeof(self) weakSelf = self;
做了什么。
从上图可知,
weakSelf
和self
两个指针地址不同但内存空间地址相同,也就是两个对象同时持有同一个内存空间。
并且正常情况下经过__weak typeof(self) weakSelf = self
操作我们需要进行引用计数处理,但是实际情况是经过弱引用表并没有处理引用计数。
3. 分析block使用weakSelf为什么可以打破循环引用呢?
a). 通过命令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
生成 .cpp代码:
void _Block_object_assign(void *destArg, const void *object, const int flags) {
const void **dest = (const void **)destArg;
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
case BLOCK_FIELD_IS_OBJECT:
_Block_retain_object(object);
*dest = object; // 实际上是指针赋值
b). 在.cpp文件中我们可以看到如上代码段,虽然weakself
对象传入进来,但是内部实际操作的是对象的指针,也就是weakself
的指针,我们知道weakself
和self
虽然内存地址相同,但指针是不一样的,也就是block
中并没有直接持有self
,而是通过weakSelf
指针操作,所以就打破了self -> block -> weakSelf -> self
中self
这一层的循环引用,变成了self -> block -> weakSelf
(临时变量的指针地址)来打破循环引用。
4. 总结
self -> block -> weakSelf -> self
:block使用weakSelf之所以能够打破循环引用是因为block内部操作的是weakSelf的指针地址,它和self是两个不同的指针地址,即 没有直接持有self,所以可以weakSelf可以打破self的循环引用关系self -> block -> weakSelf
。self -> timer -> weakSelf -> self
:那timer之所以无法打破循环关系是因为timer创建时target是对weakSelf的对象强持有操作,而weakSelf和self虽然是不同的指针但是指向的对象是相同的,也就相当于间接的强持有了self,所以weakSelf并没有打破循环引用关系。
二、解决timer循环引用的四种方法
1. 使用invalidate结束timer运行
我们第一时间肯定想到的是[self.timer invalidate]
不就可以了吗,当然这是正确的思路,那么我们调用时机是什么呢?viewWillDisAppear
还是viewDidDisAppear
?实际上在我们实际操作中,如果当前页面有push操作的话,当前页面还在栈里面,这时候我们释放timer肯定是错误的,所以这时候我们可以用到下面的方法:
- (void)didMoveToParentViewController:(UIViewController *)parent {
// 无论push 进来 还是 pop 出去 正常运行
// 就算继续push 到下一层 pop 回去还是继续
if (parent == nil) {
[self.timer invalidate];
self.timer = nil;
NSLog(@"timer 走了");
}
}
2. 中介者模式
换个思路,timer会造成循环引用是因为target强持有了self,造成的循环引用,那我们是否可以包装一下target,使得timer绑定另外一个不是self的target对象来打破这层强持有关系。
@property (nonatomic, strong) id target;
self.target = [[NSObject alloc] init]; // 自己创建的target
class_addMethod([NSObject class],
@selector(fireHome),
(IMP)fireHomeObjc,
"v@:");
self.timer = [NSTimer
scheduledTimerWithTimeInterval:1
target:self.target
selector:@selector(fireHome)
userInfo:nil
repeats:YES];
根据打印结果我们发现在dealloc的时候也可以实现timer的释放,打破了循环引用。
class_addMethod的作用:看着是给target增加了一个方法,但是实际上timer的执行是在fireHomeObjc里面执行的,而不是应该执行的fireHome函数。
分析一下:在没有使用自定义的target之前,fireHome函数的IMP是指向fireHome的这是毋庸置疑的,而使用class_addMethod之后,相当于重新指定了fireHome的IMP指针,让他指向了fireHomeObjc。
- 代码优化:既然class_addMethod中需要一个函数的IMP,那么我们直接获取fireHome的IMP就可以了。
self.target = [[NSObject alloc] init];
Method method = class_getInstanceMethod([self class], @selector(fireHome));
class_addMethod([self.target class], @selector(fireHome), method_getImplementation(method), "v@:");
self.timer = [NSTimer
scheduledTimerWithTimeInterval:1
target:self.target
selector:@selector(fireHome)
userInfo:nil
repeats:YES];
3. NSProxy虚基类的方式
NSProxy是一个虚基类,它的地位等同于NSObject。
command+shift+0
打开Xcode参考文档搜索NSProxy,说明如下:
NSProxy
An abstract superclass defining an API for objects that act as stand-ins for other objects or for objects that don’t exist yet.Declaration
@interface NSProxy
Overview
Typically, a message to a proxy is forwarded to the real object or causes the proxy to load (or transform itself into) the real object. Subclasses of
NSProxy
can be used to implement transparent distributed messaging (for example,NSDistant
) or for lazy instantiation of objects that are expensive to create.Object
NSProxy
implements the basic methods required of a root class, including those defined in theNSObject
protocol. However, as an abstract class it doesn’t provide an initialization method, and it raises an exception upon receiving any message it doesn’t respond to. A concrete subclass must therefore provide an initialization or creation method and override theforward
andInvocation: method
methods to handle messages that it doesn’t implement itself. A subclass’s implementation ofSignature For Selector: forward
should do whatever is needed to process the invocation, such as forwarding the invocation over the network or loading the real object and passing it the invocation.Invocation: method
is required to provide argument type information for a given message; a subclass’s implementation should be able to determine the argument types for the messages it needs to forward and should construct anSignature For Selector: NSMethod
object accordingly. See theSignature NSDistant
,Object NSInvocation
, andNSMethod
class specifications for more information.Signature
我们不用self来响应timer方法的target,而是用NSProxy来响应。
- DZProxy.h
#import
NS_ASSUME_NONNULL_BEGIN
@interface DZProxy : NSProxy
+ (instancetype)proxyWithTransformObject:(id)object;
@end
NS_ASSUME_NONNULL_END
- DZProxy.m
#import "DZProxy.h"
@interface DZProxy()
@property (nonatomic, weak) id object;
@end
@implementation DZProxy
+ (instancetype)proxyWithTransformObject:(id)object {
DZProxy *proxy = [DZProxy alloc];
proxy.object = object; // 我们拿到外边的self,weak弱引用持有
return proxy;
}
// 仅仅添加了weak类型的属性还不够,为了保证中间件能够响应外部self的事件,需要通过消息转发机制,让实际的响应target还是外部self,这一步至关重要,主要涉及到runtime的消息机制。
// proxy虚基类并没有持有vc,而是消息的转发,又给了vc
- (id)forwardingTargetForSelector:(SEL)aSelector {
return self.object;
}
- VC
- (void)viewDidLoad {
[super viewDidLoad];
self.proxy = [DZProxy proxyWithTransformObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES];
}
- (void)dealloc{
[self.timer invalidate];
self.timer = nil;
NSLog(@"%s",__func__);
}
虚基类方法是用proxy打破self 这一块的循环。