手动目录
- NSTimer 打破强持有的方法
方法一: 在 viewWillDisappear 中释放NSTimer
方法二:在didMoveToParentViewController中释放
方法三:消息转发
方法四:中介者模式
___普通方式
___runtimer方式- 为什么weakSelf不能打破循环引用
iOS开发中,遇到定时器的时候很多,大多数人会选择用NSTimer。
NSTimer 有一个新的API不用考虑循环引用
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
但是它支持的版本是10.12之后。
我们用的多的还是这个API:
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
这个API就会 有一个问题:循环引用造成VC释放不掉。
为什么会释放不掉? 我们在API中看说明:
target
The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to target until it (the timer) is invalidated.
target 是一个强持有。
NSTimer 打破强持有的方法
方法一: 在 viewWillDisappear 中释放NSTimer
@property (nonatomic, strong) NSTimer *timer;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
- (void)fireHome {
}
- (void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
[self.timer invalidate];
self.timer = nil;
}
这种方法很显然,局限性很大,大多数情况下,这种当时都是不友好的,因为大多数情况下,都会进行push或者present。
方法二:在didMoveToParentViewController中释放
- (void)didMoveToParentViewController:(UIViewController *)parent{
// 无论push 进来 还是 pop 出去 正常跑
if (parent == nil) {
[self.timer invalidate];
self.timer = nil;
NSLog(@"timer 走了");
}
}
方法三:消息转发
这种方式 需要借助虚基类来实现。
思路: 创建一个虚基类,指定Timer的target为这个虚基类。然后让虚基类把消息在发送给原来的target
// 虚基类 LGProxy
@interface LGProxy : NSProxy
+ (instancetype)proxyWithTransformObject:(id)object;
@end
@interface LGProxy()
@property (nonatomic, weak) id object;
@end
@implementation LGProxy
+ (instancetype)proxyWithTransformObject:(id)object{
LGProxy *proxy = [LGProxy alloc];
proxy.object = object;
return proxy;
}
-(id)forwardingTargetForSelector:(SEL)aSelector {
return self.object;
}
// 用这种方式创建NSTimer --- 指定target 为 LGProxy.object
self.proxy = [LGProxy proxyWithTransformObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES];
- (void)fireHome {
NSLog(@"hello word - %d",num);
}
方法四:中介者模式
普通方式
用一个类保存Timer的target、SEL等信息
// .h
@interface JEWeakTimer : NSObject
+ (NSTimer *) scheduledTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats;
@end
// .m
@interface JEWeakTimerTarget : NSObject
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer* timer;
@end
@implementation JEWeakTimerTarget
- (void) fire:(NSTimer *)timer {
if(self.target) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.target performSelector:self.selector withObject:timer.userInfo afterDelay:0.0f];
#pragma clang diagnostic pop
} else {
[self.timer invalidate];
}
}
@end
@implementation JEWeakTimer
+ (NSTimer *) scheduledTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats {
JEWeakTimerTarget* timerTarget = [[JEWeakTimerTarget alloc] init];
timerTarget.target = aTarget;
timerTarget.selector = aSelector;
timerTarget.timer = [NSTimer scheduledTimerWithTimeInterval:interval
target:timerTarget
selector:@selector(fire:)
userInfo:userInfo
repeats:repeats];
return timerTarget.timer;
}
@end
runtime 方式
思路: 通过runtime 动态添加 动态向中介者中添加一个方法实现(SEL(timer的SEL) - > IMP(自定义的IMP ),在自定义的IMP中,通过runtime 向原来的类中发送消息
// .h
@interface LGTimerWapper : NSObject
- (instancetype)lg_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
@end
// .m
#import
@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; // vc
self.aSelector = aSelector; // 方法 -- vc 释放
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(JETimerWapper *warpper){
if (warpper.target) {
void (*lg_msgSend)(void *,SEL, id) = (void *)objc_msgSend;
lg_msgSend((__bridge void *)(warpper.target), warpper.aSelector,warpper.timer);
}
}
@end
为什么weakSelf不能打破循环引用
上面说的各种解决NSTimer不释放的问题,都没有提到weakSelf。为什么block可以解决循环引用,而NSTimer不可以?
其实把Block本质了解之后,再来看这个问题就很好处理了。
在iOS-- block中提到一个地方,block_assign (第二层拷贝) 中是对对象的指针进行持有。
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; // ⬅️ 持有的是指针地址,而不是对象本身
break;
self和weakSelf虽然都是指向同一个对象,但他们是两个不同的地址,weakSelf不强持有对象,也就是不操作引用计数。
block在copy的时候,会强持有临时变量的指针地址,而不是指针指向的对象,所以weakSelf可以解决block循环引用问题,而NSTimer强持有的是对象。