iOS小知识之NSTimer的循环引用二

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

切断target强持有

除了常规的方法解决循环引用的问题,还可以通过切段target的强持有,解决循环引用的问题。

1.1 中介者模式

- (void)viewDidLoad {
    [super viewDidLoad];
    NSObject *objc = [[NSObject alloc] init]; 
    class_addMethod([NSObject class], @selector(fireHome), (IMP)fireHomeObjc, "v@:"); 
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:objc selector:@selector(fireHome) userInfo:nil repeats:YES]; 
}
void fireHomeObjc(id obj){ 
    num++; 
    NSLog(@"hello word - %d -%@",num, obj); 
}
- (void)dealloc{ 
    [self.timer invalidate]; 
    self.timer = nil;
}
复制代码
  • 创建的NSObject实例对象objc,通过Runtime对NSObject增加fireHome方法,IMP指向fireHomeObjc的函数地址
  • 创建NSTimer,将objc传入target参数,这样避免NSTimer对self的强持有
  • 当页面退出时,由于self没有被NSTimer持有,正常调用dealloc方法
    • 在dealloc中,对NSTimer进行释放。此时NSTimer对objc的强持有接触,objc也跟着释放

1.2 封装自定义的Timer

创建LGTimerWapper,实现自定义Timer的封装 打开LGTimerWapper.h文件,写入以下代码:

#import <Foundation/Foundation.h> 

@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
复制代码

打开LGTimerWapper.m文件,写入以下代码:

#import "LGTimerWapper.h" 
#import <objc/message.h> 
@interface LGTimerWapper() 

@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL aSelector;
@property (nonatomic, strong) NSTimer *timer; 

@end 

@implementation LGTimerWapper 

- (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; 
        self.aSelector = aSelector; 
        if ([self.target respondsToSelector:self.aSelector]) {
            Method method = class_getInstanceMethod([self.target class], aSelector); 
            const char *type = method_getTypeEncoding(method);
            class_addMethod([self class], aSelector, (IMP)fireHomeWapper, type); 
            
            self.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo]; 
        }
    }
    return self; 
}
void fireHomeWapper(LGTimerWapper *warpper){ 
    if (warpper.target) { 
        void (*lg_msgSend)(void *,SEL, id) = (void *)objc_msgSend;
        lg_msgSend((__bridge void *)(warpper.target), warpper.aSelector,warpper.timer); 
    } else { 
        [warpper lg_invalidate]; 
    }
}
- (void)lg_invalidate{ 
    [self.timer invalidate]; 
    self.timer = nil;
}

@end
复制代码

LGTimerWapper的调用代码:

- (void)viewDidLoad { 
    [super viewDidLoad]; 
    self.timerWapper = [[LGTimerWapper alloc] lg_initWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
}
- (void)fireHome{ 
    num++; 
}
复制代码
  • 在LGTimerWapper中,定义初始化lg_initWithtimInterval方法和lg_invalidate释放方法
  • 初始化方法
    • 控件内部的target使用weak修饰,对ViewController进行弱持有
    • 监测target中是否存在该selector。如果存在,对当前类使用Runtime添加同名方法编号,指向自身内部fireHomeWapper的函数地址
    • 创建真正的NSTimer定时器,将控件自身的实例对象传入target,避免NSTimer对ViewController强持有。
  • 当NSTimer回调时,会进入fireHomeWapper函数
    • 函数内部不负责业务处理,如果target存在,使用objc_msgSend,将消息发送给target自身下的selector方法。
  • 当页面退出时,ViewController可以正常释放。但LGTimerWapper和NSTimer相互持有,双方都无法释放。
  • 由于双方都无法释放,NSTimer的回调会继续调用
    • 当进入fireHomeWapper函数,发现target已经不存在了,调用LGTimerWapper的lg_invalidate方法,内部对NSTimer进行释放
    • 当NSTimer释放后,对LGTimerWapper的强持有解除,LGTimerWapper也跟着释放。

猜你喜欢

转载自juejin.im/post/7019090759739506719