1. 循环引证

问题代码

日常开发中,经常会用到NSTimer定时器,一些不正确的写法,会导致NSTimer形成循环引证,如下:

@interface TargetViewController ()
@property (nonatomic, strong) NSTimer *timer; 
@end
@implementation TargetViewController
- (void)viewDidLoad {
  [super viewDidLoad];
  self.timer = [NSTimer timerWithTimeInterval:1 target:self selector: @selector(timerAction) userInfo:nil repeats:YES];
  [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
- (void**)timerAction {
  static uint64_t num = 0;
  NSLog(@"%s, num = %@", __func__ , @(num));
  num++;
}
- (void)dealloc {
  [self.timer invalidate];
  self.title = nil;
  NSLog(@"%s", __func__);
}
@end

这种代码必然会形成循环引证:

  • 创立timer时,将self传入target,导致timer持有self,而self又持有timer

  • 运用timerinvalidate办法能够免除timerself的持有,可是 timer 持有 self,导致 self 不可能调用 dealloc, 从而不能调用 invalidate 故此两边彼此等候,形成循环引证

运用__weak 可行否

在解决 block 循环引证时,我们运用 __weak typeof(self) weakSelf = self;,那么 针对tiemer 是不是也能够这样呢?修改代码如下:

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

运转代码,依然不行,为啥呢? 查看官文 :

NSTimer 循环引用问题

  • target:定时器触发时指定的音讯发送到的目标。计时器维护对该目标的强引证,直到它(计时器)失效

  • timerWithTimeInterval内部,运用强引证目标接纳target参数,所以这里在外部界说为弱引证目标没有任何含义, 类似于这种代码:

    __weak typeof(self) weakSelf = self; // 外部
    __strong typeof(weakSelf)strongSelf = weakSelf; // 内部
    

Block的区别,Block将捕获到的弱引证目标,内部赋值给一个弱引证的临时变量,当Block履行完毕,临时变量会主动销毁,免除对外部变量的持有

2. 惯例解决办法

运用 带blockAPI

运用带着Block的办法创立NSTimer,防止target的强持有

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                    repeats:(BOOL)repeats 
                                      block:(void (^)(NSTimer *timer))block;

在恰当机遇调用invalidate

如:在didMoveToParentViewController办法中

- (void)didMoveToParentViewController:(UIViewController *)parent{
    if (parent == nil) {
        [self.timer invalidate];
        self.timer = nil;
    }
}

3. 打断强持有

除了惯例解决方案,还能够经过打断target的强持有,解决循环引证的问题

中介者形式

- (void)viewDidLoad {
    [super viewDidLoad];
    NSObject *objc = [[NSObject alloc] init];
    class_addMethod([NSObject class], @selector(timerAction), (IMP)timerActionObjc, "v@:");
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:objc selector:@selector(timerAction) userInfo:nil repeats:YES];
}
void timerActionObjc(id obj){
    static uint64_t num = 0;
  NSLog(@"%s - %@ - %@", __func__ , @(num), obj);
  num++;
}
- (void)dealloc{
    [self.timer invalidate];
    self.timer = nil;
    NSLog(@"%s", **__func__** );
}
  • 创立NSObject实例目标objc,经过RuntimeNSObject增加timerAction办法,IMP指向timerActionObjc的函数地址

  • 创立NSTimer,将objc传入target参数,这样防止NSTimerself的强持有

  • 当页面退出时,由于self没有被NSTimer持有,正常调用dealloc办法

    • dealloc中,对NSTimer进行开释。此刻NSTimerobjc的强持有免除,objc也跟着开释

封装自界说Timer

创立YJWeakTimer,完成自界说Timer的封装

翻开YJWeakTimer.h文件,写入以下代码:

#import <Foundation/Foundation.h>
@interface YJWeakTimer : NSObject
- (instancetype)yj_initWithTimeInterval:(NSTimeInterval)interval target:(id)target selector:(SEL)selector userInfo:(id)userInfo repeats:(BOOL)repeats;
- (void)yj_invalidate;
@end

翻开YJWeakTimer.m文件,写入以下代码:

#import "YJWeakTimer.h"
#import <objc/message.h>
@interface YJWeakTimer()
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL aSelector;
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation YJWeakTimer
- (instancetype)yj_initWithTimeInterval:(NSTimeInterval)interval target:(id)target selector:(SEL)selector userInfo:(id)userInfo repeats:(BOOL)repeats {
    if (self == [super init]) {
        self.target     = target;
        self.aSelector  = selector;
        if ([self.target respondsToSelector:self.aSelector])
            Method method = class_getInstanceMethod([self.target class], selector);
            const char *type = method_getTypeEncoding(method);
            class_addMethod([self class], selector, (IMP)fireHomeWapper, type);
            self.timer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:selector userInfo:userInfo repeats:repeats];
        }
    }
    return self;
}
void fireHomeWapper(YJWeakTimer *wtimer){
    if (wtimer.target) {
        void (*yj_msgSend)(void *, SEL, id) = (void *)objc_msgSend;
        yj_msgSend((__bridge void *)(wtimer.target), wtimer.aSelector, wtimer.timer);
    } else {
        [wtimer yj_invalidate];
    }
}
- (void)yj_invalidate {
    [self.timer invalidate];
    self.timer = nil;
}
@end

YJWeakTimer 的运用:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.weakTimer = [[YJWeakTimer alloc] yj_initWithTimeInterval:1 target:self selector: @selector(timerAction) userInfo:nil repeats:YES];
}
- (void)timerAction {
  static uint64_t num = 0;
  NSLog(@"%s, num = %@", __func__ , @(num));
  num++;
}
  • YJWeakTimer 中界说 yj_initWithTimeIntervalyj_invalidate 办法

    • YJWeakTimer 经过 weak 润饰的 target,对ViewController`进行弱持有

    • 检测target中是否能呼应selector。能呼应,对当前类经过Runtime API添加同名办法编号,指向本身内部fireHomeWapper的函数地址

    • 创立真正的NSTimer定时器,将控件本身的实例目标传入target,防止NSTimerViewController强持有

  • NSTimer回调时,会进入fireHomeWapper函数

    • 函数内部不负责事务处理,假如target存在,运用objc_msgSend,将音讯发送给target本身下的selector办法
  • 当页面退出时,ViewController能够正常开释。但YJWeakTimerNSTimer彼此持有,两边都无法开释

  • 由于两边都无法开释,NSTimer的回调会持续调用

    • 当进入fireHomeWapper函数,发现target已经不存在了,调用YJWeakTimeryj_invalidate办法,内部对NSTimer进行开释

    • NSTimer开释后,对YJWeakTimer的强持有免除,YJWeakTimer也跟着开释

NSProxy虚基类

NSProxy的效果:

  • OC不支持多承继,可是它依据运转机遇制,能够经过NSProxy来完成伪多承继

  • NSProxyNSObject属于同一级其他类,也能够说是一个虚拟类,只完成了NSObject的协议部分

  • NSProxy实质是一个音讯转发封装的抽象类,类似一个代理人

能够经过承继NSProxy,并重写以下两个办法完成音讯转发

- (void)forwardInvocation:(NSInvocation *)invocation;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel;

NSProxy除了能够用于多承继,也能够作为堵截强持有的中间人

翻开YJProxy.h文件,写入以下代码:

NSTimer 循环引用问题

翻开YJProxy.m文件,写入以下代码:

NSTimer 循环引用问题

YJProxy的调用运用:

- (void)viewDidLoad {
  [uper viewDidLoad];
  self.proxy = [YJProxy proxyWithTarget:self];
  self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(timerAction) userInfo:nil repeats:YES];
}
- (void)timerAction {
  static uint64_t num = 0;
  NSLog(@"%s, num = %@", __func__ , @(num));
  num++;
}
- (void)dealloc {
  [self.timer invalidate];
  self.timer = nil;
  NSLog(@"%s", **__func__** );
}
  • YJProxy初始化办法,将传入的object赋值给弱引证目标

  • UIViewController中,创立YJProxy目标proxy。创立NSTimer目标,将proxy传入target,防止NSTimerViewController强持有

  • NSTimer回调时,触发YJProxy的音讯转发办法

    • methodSignatureForSelector:设置办法签名
    • forwardInvocation:本身不做事务处理,将音讯转发给object
  • 当页面退出时,ViewController能够正常开释

    • dealloc中,对NSTimer进行开释。此刻NSTimerproxy的强持有免除,proxy也跟着开释

总结:

循环引证:

  • 创立NSTimer时,运用带有target参数的办法,会对传入的目标进行强引证。假如传入的是持有timer的目标,双发会彼此持有,形成循环引证

  • 不能在UIViewControllerdealloc办法中开释timer。只需timer有用,UIViewControllerdealloc办法就不会履行。故此两边彼此等候,谁都无法开释

  • NSTimertarget参数传入一个弱引证的self没有任何含义,由于在创立NSTimer的办法内部,运用强引证目标接纳target参数

惯例解决方案:

  • 运用带着Block的办法创立NSTimer,防止target的强持有

  • 依据事务需求,在恰当机遇调用invalidate。例如:viewWillDisappeardidMoveToParentViewController

堵截target的强持有:

  • 中介者形式

  • 封装自界说Timer

  • 运用NSProxy虚基类