iOS中Timer循环引用原因及解决方案

一、准备

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方法中,timerself对象进行了强持有,因此造成了循环引用。
  • 但是当我们按照惯例用weakSelf去打破强引用的时候,发现weakSelf没有打破循环引用,timer仍然在运行。
  • self -> timer -> weakSelf -> self

2. __weak typeof(self) weakSelf = self分析

从上面我们会疑惑为什么blockself -> block -> wealSelf -> self可以打破循环引用,而 self -> timer -> weakSelf -> self无法打破呢?
带着这个疑问,我们要了解__weak typeof(self) weakSelf = self;做了什么。

从上图可知,weakSelfself两个指针地址不同但内存空间地址相同,也就是两个对象同时持有同一个内存空间。
并且正常情况下经过__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的指针,我们知道weakselfself虽然内存地址相同,但指针是不一样的,也就是block中并没有直接持有self,而是通过weakSelf指针操作,所以就打破了self -> block -> weakSelf -> selfself这一层的循环引用,变成了self -> block -> weakSelf (临时变量的指针地址)来打破循环引用。

4. 总结

  1. self -> block -> weakSelf -> self:block使用weakSelf之所以能够打破循环引用是因为block内部操作的是weakSelf的指针地址,它和self是两个不同的指针地址,即 没有直接持有self,所以可以weakSelf可以打破self的循环引用关系self -> block -> weakSelf
  2. 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 NSProxycan be used to implement transparent distributed messaging (for example, NSDistantObject) or for lazy instantiation of objects that are expensive to create.
NSProxy implements the basic methods required of a root class, including those defined in the NSObject 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 the forwardInvocation: and methodSignatureForSelector: methods to handle messages that it doesn’t implement itself. A subclass’s implementation of forwardInvocation: 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. methodSignatureForSelector: 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 an NSMethodSignatureobject accordingly. See the NSDistantObject, NSInvocation, and NSMethodSignature class specifications for more information.

我们不用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 这一块的循环。

你可能感兴趣的:(iOS中Timer循环引用原因及解决方案)