🔥🔥iOS中解决NSTimer循环引用问题

Posted 1-434

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了🔥🔥iOS中解决NSTimer循环引用问题相关的知识,希望对你有一定的参考价值。

 
ios中解决NSTimer循环引用问题
  NSTimer使用不当就会造成内存泄漏,比如常见的使用方法:
//定义
@property (nonatomic, strong) NSTimer *timer;
 
//实现
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(showMsg) userInfo:nil repeats:YES];
 
//销毁
-(void)dealloc
{
[self.timer invalidate];
self.timer = nil;
}
由于NSTimer会引用住self,而 self又持有NSTimer对象,所以形成循环引用,dealloc 永远不会被执行,timer 也永远不会被释放,造成内存泄漏。
尝试解决办法:
1、把timer改成弱引用
@property (nonatomic, weak) NSTimer *timer;
虽然self对timer是弱引用,但是控制的delloc方法的执行依赖于timer的invalidate,timer的invalidate又依赖于控制器的delloc方法,这是一个鸡生蛋还是蛋生鸡的问题,依旧是循环引用;
2、使用__weak
  那换个思路能不能让NSTimer弱引用target:
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:weakSelf selector:@selector(showMsg) userInfo:nil repeats:YES];
weak关键字适用于block,当block引用了块外的变量时,会根据修饰变量的关键字来决定是强引用还是弱引用,如果变量使用weak关键字修饰,那block会对变量进行弱引用,如果没有__weak关键字,那就是强引用。
  但是NSTimer的 scheduledTimerWithTimeInterval:target方法内部不会判断修饰target的关键字,所以这里传self 和 weakSelf是没区别的,其内部会对target进行强引用,还是会产生循环引用。
3、 选择合适的时机手动释放timer
采用下面的方法解决循环引用:
- (void) viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
 
if (self.timer) {
[self.timer invalidate];
self.timer = nil;
}
}
在某些情况下,这种做法是可以解决问题的,但是有时却会引起其他问题,比如控制器push到下一个控制器,viewDidDisappear执行后,timer被释放,此时再pop回来,timer已经不复存在了。
所以,这种"方案"并不是合理的。
优化上面的方法这个时候可以采用配对使用在 viewWillAppear 开timer启,在 viewWillDisappear 关闭timer:
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
if (!self.timer) {
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(showMsg) userInfo:nil repeats:YES];
}
}
 
-(void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
if (self.timer) {
[self.timer invalidate];
self.timer = nil;
}
}
上面的方法只是维护起来比较麻烦
最终解决办法
1、自定义categoryblock解决
  网上有一些封装的比较好的block的解决方案,思路无外乎是封装一个NSTimer的category,提供block形式的接口:
#import <Foundation/Foundation.h>
 
@interface NSTimer (TimerBlock)
 
/**
分类解决NSTimer在使用时造成的循环引用的问题
 
@param interval 间隔时间
@param block 回调
@param repeats 用于设置定时器是否重复触发
 
@return 返回NSTimer实体
*/
+ (NSTimer *)block_TimerWithTimeInterval:(NSTimeInterval)interval block:(void(^)())block repeats:(BOOL)repeats;
 
@end
 
#import "NSTimer+TimerBlock.h"
 
@implementation NSTimer (TimerBlock)
+ (NSTimer *)block_TimerWithTimeInterval:(NSTimeInterval)interval block:(void (^)())block repeats:(BOOL)reqeats{
return [self timerWithTimeInterval:interval target:self selector:@selector(blockSelector:) userInfo:[block copy] repeats:reqeats];
}
 
+ (void) blockSelector:(NSTimer *)timer{
void (^block)() = timer.userInfo;
if (block) {
block();
}
}
@end
上述创建方式调用者是NSTImer自己,只是NSTimer捕获了参数block。这样我们在使用timer时,由于target的改变,就不再有循环引用了。
__weak typeof(self) weakSelf = self; //避免 block 强引用 self
self.timer = [NSTimer block_TimerWithTimeInterval:3 block:^{
// [weakSelf dosomething];
} repeats:YES];
  iOS10中,定时器的API新增了block方法,实现原理与此类似,这里采用分类为NSTimer添加了带有block参数的方法,而系统是在原始类中直接添加方法,最终的行为是一致的。
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
2、给self添加中间件proxy
引入一个对象proxy,proxy弱引用 self,然后 proxy 传入NSTimer。即self 强引用NSTimer,NSTimer强引用 proxy,proxy 弱引用 self,这样通过弱引用来解决了相互引用,此时不会形成环。
技术图片

 

 

定义一个继承自NSObject的中间代理对象FFProxy,ViewController不持有timer,而是持有FFProxy实例,让FFProxy实例来弱引用ViewController,timer强引用FFProxy实例,直接看代码:
//FFProxy.h
@interface FFProxy : NSObject
+(instancetype)proxyWithTarget:(id)target;
@end
 
//FFProxy.m
#import "FFProxy.h"
 
@interface FFProxy()
@property (nonatomic ,weak) id target;
@end
 
@implementation FFProxy
 
+(instancetype)proxyWithTarget:(id)target
{
FFProxy *proxy = [[FFProxy alloc] init];
proxy.target = target;
return proxy;
}
 
//仅仅添加了weak类型的属性还不够,为了保证中间件能够响应外部self的事件,需要通过消息转发机制,让实际的响应target还是外部self,这一步至关重要,主要涉及到runtime的消息机制。
-(id)forwardingTargetForSelector:(SEL)aSelector
{
return self.target;
}
@end
 
//ViewController.m
 
FFProxy *proxy = [FFProxy proxyWithTarget:self];
//将timer的target设置为proxy,proxy又弱引用了控制器,其实最终还是调用了控制器的showMsg方法。
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:proxy selector:@selector(showMsg) userInfo:nil repeats:YES];
 
//销毁
-(void)dealloc
{
[self.timer invalidate];
self.timer = nil;
}
- (id)forwardingTargetForSelector:(SEL)aSelector是什么?
  消息转发,简单来说就是如果当前对象没有实现这个方法,系统会到这个方法里来找实现对象。
  本文中由于当前targetFFProxy,但是FFProxy没有实现showMsg方法(当然也不需要它实现),让系统去找target实例的方法实现,也就是去找ViewController中的方法实现。
 
3、使用NSProxy
使用iOS的NSProxy类,NSProxy就是专门用来做消息转发的。
//FFWeakProxy.h
@interface FFWeakProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@end
 
//FFWeakProxy.m
@interface FFWeakProxy()
@property (nonatomic ,weak)id target;
@end
@implementation FFWeakProxy
+ (instancetype)proxyWithTarget:(id)target {
//NSProxy实例方法为alloc
FFWeakProxy *proxy = [FFWeakProxy alloc];
proxy.target = target;
return proxy;
}
 
/**
这个函数让重载方有机会抛出一个函数的签名,再由后面的forwardInvocation:去执行
为给定消息提供参数类型信息
*/
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}
 
/**
* NSInvocation封装了NSMethodSignature,通过invokeWithTarget方法将消息转发给其他对象。这里转发给控制器执行。
*/
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}
@end
Controller里代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
// 这里的target又发生了变化
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[FFWeakProxy proxyWithTarget:self] selector:@selector(showMsg) userInfo:nil repeats:YES];
}
 
//销毁
-(void)dealloc
{
[self.timer invalidate];
self.timer = nil;
}


以上是关于🔥🔥iOS中解决NSTimer循环引用问题的主要内容,如果未能解决你的问题,请参考以下文章

🔥iOS中野指针问题叙述

🔥🔥造成循环引用和内存泄漏的几种情况

🔥🔥如何令自己所写的对象具有拷贝功能?

🔥为什么苹果提倡尽量多的使用PNG格式的图片?

Git入门图文教程(1.5W字40图)🔥🔥--深入浅出图文并茂

🔥isKindOfClass 和 isMemberOfClass