美文网首页
iOS 中关于 NSTimer 的强引用分析

iOS 中关于 NSTimer 的强引用分析

作者: 远方竹叶 | 来源:发表于2020-12-04 16:06 被阅读0次

    问题抛出

    我们先来看一个案例,创建一个空工程,创建三个导航控制器,分别叫 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 方法也没执行)。这是为什么呢?

    问题分析

    weakSelfself 它们是同一个对象吗?weakself 会对引用计数加 1 吗?我们添加如下代码,打印 weakSelf 前后的引用计数

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

    运行项目,打印结果如下

    打印结果都是 8,说明 weakSelf 没有对内存引用计数加 1。现在在 weakSelf 下面添加一个断点,分别打印 selfweakSelf 对象以及它们的内存地址,如下

    从打印结果可以看到,selfweakSelf 取地址的值是不一样的,但是它们指向的是同一片内存空间(即它们是两个指针,内存地址不一样,指向了同一片内存空间)。如下图所示

    这里 timer 捕获的是 weakSelf 对象(即 <PushViewController: 0x7fcb3cc0cb60>,那片内存地址),无法通过 weakSelf 来解决强持有。

    RunLoop 强引用

    在上面的分析中,我们其实还忽略了一点,RunLooptimer 的强持有,官方文档说明如下

    RunLoop 对整个对象的空间有强持有,RunLoop 不停止,timerself 是无法释放的。

    • 所以起初的引用链应该是这样的
    • 加上 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

    解决思路

    由于 Runlooptimer 的强持有,导致了 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

      1. 创建一个类,继承自 NSObject
      1. 初始化方法中,接收 vc 传递来的参数,判断 self.target 是否能够响应 self.aSelector(即 vc 对象能否响应 aSelector),如果能响应,当前自定义类添加 aSelector 方法实现
      1. 定义一个 timer,设置 target 为本身(即 LCTimerWapper 中的 timer 一直监听自己),走当前类的 aSelector 方法
      1. 定时器执行方法
        • target 存在,需要让 vc 知道,即向传入的 target 发送 selector 消息,并将此时的 timer 参数也一并传入,所以 vc 就可以得知 fireHome 方法,就这事这种方式定时器方法能够执行的原因
        • target 不存在,说明已经释放了,则释放当前的 timerWrapper,即打破了 RunLooptimeWrapper 的强持有
      1. lc_invalidate 方法释放 timer。需要在 vcdealloc 方法中调用,即 vc 释放,从而导致 timerWapper 释放,打破了 vctimeWrapper 的的强持有

    自定义封装代码如下

    /**------.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);
        }];
    }
    

    相关文章

      网友评论

          本文标题:iOS 中关于 NSTimer 的强引用分析

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