美文网首页iOS技术类学编程循环引用
NSTimer 循环引用的原因和解决方案

NSTimer 循环引用的原因和解决方案

作者: just东东 | 来源:发表于2021-04-09 14:54 被阅读0次

    NSTimer 循环引用的原因和解决方案

    造成循环引用的原因就是两个对象之间因为强引用无法释放。本文将通过NSTimer来剖析强引用,以及解决方法。

    1. 强引用

    举个例子,比如我们有两个ViewController,分别为AB,从A可以pushB,从B可以popAB中代码如下:

    
    static int num = 0;
    
    @property (nonatomic, strong) NSTimer       *timer;
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
         self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
         // 加runloop
         [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
    }
    
    - (void)fireHome{
        num++;
        NSLog(@"hello word - %d",num);
    }
    
    - (void)dealloc{
        [self.timer invalidate];
        self.timer = nil;
        NSLog(@"%s",__func__);
    }
    

    当我们从B界面popA时,timer并不会停,那是为什么呢?显然是没有执行B界面的dealloc方法,导致B界面没有被释放。

    既然没释放肯定是有循环引用,那么这个循环引用产生的在哪里呢?乍一看,我们的BViewController强引用了timer,那么如果说造成循环引用就是timer强引用了self,但是这里面没有block怎么产生的循环引用呢?这里面在初始化timer的时候有个target,我们查看一下这个初始化方法shift+command+0,搜索一下timerWithTimeInterval:target:selector:userInfo:repeats:关于target的描述如下:

    image

    可以看到timertarget保持强引用,直到timer失效。

    所以说循环引用就产生了,B强引用着timertimer强引用着target也就是self,在这里self就是B的实例对象。此时就是:
    self -> timer -> self构成的循环引用。

    我们在iOS Objective-C Block简介这篇文章中介绍了使用weakSelf来解决循环引用,既然是这样,那么我们用weakSelf是否可以解决这层循环引用呢?

    将代码修改为如下:

    - (void)viewDidLoad {
        [super viewDidLoad];
    
        __weak typeof(self) weakSelf = self;
         self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
         // 加runloop
         [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
    }
    

    运行,依旧没有打破循环引用,timerpop后依旧运行。那么这是为什么呢?,在block中我们可以使用weakSelf来打破循环引用,那么在这里为什么不行呢?

    此时我们使用__weak虽然打破了self -> timer -> self这个循环引用,使其变成了self -> timer -> weakSelf -> self

    但是这里我们分析的并不全面,因为我们的timer需要加入到RunloopRunlooptimer是一个强持有,Runloop的生命周期比B界面更长,所以这才是导致timer无法释放的真正原因,timer无法释放,自然self也就无法释放。所以这个引用链最初应该是这样的:

    self -> timer -> self
    runloop -> timer -> self

    画个图:

    image

    加上weakSelf之后,变成了这样:

    self -> timer -> weakSelf -> self
    runloop -> timer -> weakSelf -> self

    image

    那么虽然是这样weakSelf也是弱引用啊,为什么不能打破循环引用呢?在block中我们可以通过self -> block -> weakSelf -> self打破循环引用?为什么这里就不可以了呢?

    这里我们就要稍微研究一下这行代码了:
    __weak typeof(self) weakSelf = self;

    我们想知道weakSelfself有什么区别,其实主要是这三点:

    1. weakSelf会对self的引用计数+1吗?
    2. weakSelfself的指针地址相同吗?
    3. weakSelfself是指向同一片内存空间吗?

    下面我们验证一下,添加这样一段代码:

        NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
        __weak typeof(self) weakSelf = self;
        NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
    

    运行并通过lldb调试得到如下结果:

    image

    我们可以看到:

    • weakSelf并没有增加self的引用计数
    • weakSelfself指向同一内存区域
    • weakSelfself的指针地址是不同的

    其实分析完这里我们也看不出什么,这里的引用关系还是这幅图:

    image

    下面我们在看看block中的weakSelf,添加如下代码:

    @property (nonatomic, copy)  void(^myBlock)(void);
    @property (nonatomic, copy) NSString *name;
    
    - (void)test1 {
        __weak typeof(self) weakSelf = self;
        self.name = @"test1";
        self.myBlock = ^{
            NSLog(@"%@",weakSelf.name);
        };
        
        self.myBlock();
    }
    

    调用test1,通过lldb调试:

    image

    此时就很清晰了,block中的weakSelf与外面的weakSelf根本不是同一个对象,虽然他们指向的都是同一片内存区域,在这里就是<LGTimerViewController: 0x7fc275604b10>,下面我们在看看libclosure中的_Block_object_assign函数。

    image

    在这里我们看到都是取的对象的地址**,或者是通过_Block_copy拷贝一份,也就是说在block中都是临时变量,一份新的变量,所以说在block中其引用链并不存在对weakSelf持有,而是持有的weakSelf的指针地址,也就是*weakSelf,跟self没有任何关系。

    然而在timer这里,timerweakSelf也就是target是强持有,所以不能打破循环引用。

    所以对于blocktimer两个模型之间循环引用的区别如下:

    timerself -> timer -> weakSelf -> self
    blockself -> block -> *weakSelf

    2. 解决Timer强引用

    2.1 不使用带target的Timer

    因为timer通过target强持有了self,那么我们不使用含有target的API不就就可以了,修改代码为如下:

    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"hello word - %d",num);
    }];
    

    2.2 提前销毁timer

    因为timer通过target强持有了self,当我们需要pop的时候,提前销毁timer就可以打破这层循环引用,所以我们可以通过didMoveToParentViewController,但是无论是pop还是push都会调用该方法,所以我们加一层判断,代码如下:

    - (void)didMoveToParentViewController:(UIViewController *)parent{
        // 无论push 进来 还是 pop 出去 正常跑
        // 就算继续push 到下一层 pop 回去还是继续
        if (parent == nil) {
           [self.timer invalidate];
            self.timer = nil;
            NSLog(@"timer 走了");
        }
    }
    

    此时当我们pop的时候就可以正常销毁timer了。

    2.3 中介者模式

    在这里我们关系的是fireHome能执行,并不关心timer捕获的target是谁,所以为了避免循环引用,我们可以把target换成其他对象,将fireHome交给target执行。所以修改代码为如下:

    #import <objc/runtime.h>// 导入runtime
    
    //* 定义一个id类型的对象属性 */
    @property (nonatomic, strong) id            target;
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        // 初始化target
        self.target = [[NSObject alloc] init];
        // 给NSObject添加方法
        class_addMethod([NSObject class], @selector(fireHome), (IMP)fireHomeObjc, "v@:");
        // 初始化timer
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.target selector:@selector(fireHome) userInfo:nil repeats:YES];
    }
    
    void fireHomeObjc(id obj){
        num++;
        NSLog(@"hello word - %d",num);
    }
    
    - (void)dealloc{
        [self.timer invalidate];
        self.timer = nil;
        NSLog(@"%s",__func__);
    }
    

    这里因为不在强引用selfself就可以正常dealloc,也就可以停掉timer。从而解除对target的强引用。

    2.4 自定义封装timer

    上面的解决方式其实需要考虑的方面比较多,需要定义target对象,添加方法,停掉和置空timer,步骤还是蛮多的,稍不注意就可能出错,所以我们自己封装一个timer,作为中间层,来解决调用者这些复杂的操作,来使调用显得简单、方便、安全。

    首先我们提供两个方法,分别是初始化方法和销毁timer的方法,代码如下:

    @interface LGTimerWapper : NSObject
    
    - (instancetype)lg_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
    - (void)lg_invalidate;
    
    @end
    

    然后我们提供了三个属性,分别用于存储targetselector以及自定义timer中的timer属性,代码如下:

    #import <objc/message.h>
    
    @interface LGTimerWapper()
    // 定义一个target 用于存储传入的target 注意这里使用的是weak
    @property (nonatomic, weak) id target;
    // 存储 sel
    @property (nonatomic, assign) SEL aSelector;
    // timer
    @property (nonatomic, strong) NSTimer *timer;
    
    @end
    

    下面是初始化方法的实现:

    1. 首先我们存储了targetaSelector
    2. 然后判断target能响应aSelector的时候
      1. 为中介添加方法,这里面的中介就是当前类
      2. 并把imp指向当前类的fireHomeWapper方法
      3. 初始化timer
    3. return self
    - (instancetype)lg_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{
        if (self == [super init]) {
            self.target     = aTarget; // vc
            self.aSelector  = aSelector; // 方法 -- vc 释放
            
            if ([self.target respondsToSelector:self.aSelector]) {
                // 将中介的处理添加到这里,不去外面再次添加,这里面的中介就是当前类型
                // 通过Runtime 获取到方法
                Method method    = class_getInstanceMethod([self.target class], aSelector);
                // 获取方法的type
                const char *type = method_getTypeEncoding(method);
                // 为当前类添加这个方法
                class_addMethod([self class], aSelector, (IMP)fireHomeWapper, type);
    
                // runloop&self -> timer -> lgtimerwarpper
                self.timer      = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo];
            }
        }
        return self;
    }
    

    下面我们看看fireHomeWapper方法的实现,这里是重点也是难点:

    1. 首先判断target属性是否有值,因为这个属性是weak的,如果有值说明能响应
      1. 这里通过objc_msgSend来调用存储的aSelector
    2. 如果不存在,说明不能响应了,停掉timer并置空就好了

    关于lg_invalidate方法的实现就更简单了,在本示例中没有用到该方法,但是如果想要主动销毁可以调用,代码如下:

    - (void)lg_invalidate{
        [self.timer invalidate];
        self.timer = nil;
    }
    

    这样编写后调用的时候就非常简单了,减少了很多需要处理的地方:

    #import "LGTimerWapper.h"
    
    @property (nonatomic, strong) LGTimerWapper *timerWapper;
    //* 定义一个id类型的对象属性 */
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.timerWapper = [[LGTimerWapper alloc] lg_initWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
    }
    

    2.5 使用NSProxy虚基类的子类

    上面的代码虽然使用起来比较简单,但是代码写起来少多了些,有时候也存在维护问题,对于调用者没有真正的去调用invalidate和置空timer,总是有些别扭的,其实解决timer循环引用的最好的方式还是使用NSProxy。下面我们来看看怎么实现:

    首先我们定义一个NSProxy的子类,这个类里面通过一个weak属性,持有着target中需要强引用的实例对象。代码如下:

    #import "LGProxy.h"
    
    @interface LGProxy()
    @property (nonatomic, weak) id object;
    @end
    
    @implementation LGProxy
    + (instancetype)proxyWithTransformObject:(id)object{
        LGProxy *proxy = [LGProxy alloc];
        proxy.object = object;
        return proxy;
    }
    

    但是仅仅是这样还是不行的,还需要让实际的target响应消息,毕竟LGProxy不能真正响应timer中的消息。

    /*
        仅仅添加了weak类型的属性还不够,为了保证中间件能够响应外部self的事件
        需要通过消息转发机制,让实际的响应target还是外部self,
        这一步至关重要,主要涉及到runtime的消息机制。
    */
    -(id)forwardingTargetForSelector:(SEL)aSelector {
        return self.object;
    }
    

    下面我们看看怎么使用:

    #import "LGProxy.h"
    
    @property (nonatomic, strong) LGProxy       *proxy;
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.proxy = [LGProxy proxyWithTransformObject:self];
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES];
    }
    
    - (void)fireHome{
        num++;
        NSLog(@"hello word - %d",num);
    }
    
    - (void)dealloc{
        [self.timer invalidate];
        self.timer = nil;
        NSLog(@"%s",__func__);
    }
    

    此时使用起来还是直接使用NSTimer,只是对target的强引用的修改成了Proxy

    相关文章

      网友评论

        本文标题:NSTimer 循环引用的原因和解决方案

        本文链接:https://www.haomeiwen.com/subject/kogikltx.html