NSTimer 不释放问题
@interface ViewController ()
@property (nonatomic, weak) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(timerRun) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
如上代码所示,我们创建一个定时器,并添加到当前 runLoop
,也通过 __weak
修饰了 weakSelf
,但是当我们运行计时器离开页面的时候发现计时器并没有销毁,依然在执行。这里有个比较疑惑的点,我们用 block
的时候用 __weak
修饰的时候是可以解决循环引用的问题,但是为什么这里不可以呢?
通过官方文档我们可以看到,我们传入的 target
会被 timer
强持有,这里传入的 target
就是 weakSelf
,所以 weakSelf
指向的内存空间引用计数会被加 1。而 block
传入 weakSelf
是被弱引用捕获,不会对引用计数加 1。然后这里就会造成一种现象,[NSRunLoop currentRunLoop]
是常驻的,[NSRunLoop currentRunLoop]
持有 timer
,timer
强持有 weakSelf
,而 weakSelf
指向的内存空间就是 self
,引用计数会被加 1,所以会出现不释放的问题。
NSTimer 不释放问题解决方案
针对不释放问题我们这里有几种解决方案如下,以供参考。
使用 block 的形式
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer 执行");
}];
离开页面时对 timer 进行处理
- (void)viewWillDisappear:(BOOL)animated{
[super viewWillDisappear:animated];
[self.timer invalidate];
self.timer = nil;
}
类似这种,在离开页面的时候调用 invalidate
,并把 timer
设置为 nil
,但是这里会出现一个问题,push
到下一个页面的时候 timer
也会被释放。所以推荐下面这种方法,只在 pop
离开页面时进行释放。
- (void)didMoveToParentViewController:(UIViewController *)parent {
if (parent == nil) {
[self.timer invalidate];
self.timer = nil;
}
}
中介者模式
- (void)viewDidLoad {
[super viewDidLoad];
self.target = [[NSObject alloc] init];
class_addMethod([NSObject class], @selector(timerRun), (IMP) timerRunObjc, "v@:");
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.target selector:@selector(timerRun) userInfo:nil repeats:YES];
}
void timerRunObjc(id obj){
NSLog(@"%s -- %@",__func__,obj);
}
- (void)dealloc{
[self.timer invalidate];
self.timer = nil;
}
类似这种,我们可以使用一个中间者来作为 target
,来打破这种不释放问题,这种思维来自于 FBKVO
,但是这种虽然能解决问题,但是不够简洁,逻辑代码都写在了控制器中,我们可以对此进行优化。
@interface CXTimerViewController ()
@property (nonatomic, strong) CXTimerWapper *timerWapper;
@end
- (void)viewDidLoad {
[super viewDidLoad];
self.timerWapper = [[CXTimerWapper alloc] cx_initWithTimeInterval:1 target:self selector:@selector(timerRun) userInfo:nil repeats:YES];
}
@interface CXTimerWapper : NSObject
- (instancetype)cx_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
@end
#import "CXTimerWapper.h"
#import
@interface CXTimerWapper()
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL aSelector;
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation CXTimerWapper
- (instancetype)cx_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;
}
// 一直跑 runloop
void fireHomeWapper(CXTimerWapper *warpper){
if (warpper.target) { // vc - dealloc
void (*cx_msgSend)(void *,SEL, id) = (void *)objc_msgSend;
cx_msgSend((__bridge void *)(warpper.target), warpper.aSelector,warpper.timer);
}else{ // warpper.target
[warpper.timer invalidate];
warpper.timer = nil;
}
}
- (void)dealloc{
NSLog(@"%s",__func__);
}
@end
如上代码,这里采用分层思想,把 timer
跟控制器进行了隔离,timer
的所有逻辑处理都放到了 CXTimerWapper
类中,这里做的比较好的一点就是没有把 selector
进行写死,而是由外界传入的 target
调用 selector
,这样就会比较灵活,而且在 fireHomeWapper
函数中做了对 warpper.target
的判断,warpper.target
为空说明外界调用者已经被释放了,所以就会对 timer
进行释放。
虚基类的方式
@interface ViewController ()
@property (nonatomic, strong) CXProxy *proxy;
@end
- (void)viewDidLoad {
[super viewDidLoad];
self.proxy = [CXProxy proxyWithTransformObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(timerRun) userInfo:nil repeats:YES];
}
- (void)dealloc{
[self.timer invalidate];
self.timer = nil;
}
@interface CXProxy : NSProxy
+ (instancetype)proxyWithTransformObject:(id)object;
@end
@interface CXProxy()
@property (nonatomic, weak) id object;
@end
@implementation CXProxy
+ (instancetype)proxyWithTransformObject:(id)object{
CXProxy *proxy = [CXProxy alloc];
proxy.object = object;
return proxy;
}
// 消息快速转发
-(id)forwardingTargetForSelector:(SEL)aSelector {
return self.object;
}
// 消息转发 self.object
//- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
//
// if (self.object) {
// }else{
// NSLog(@"麻烦收集 stack111");
// }
// return [self.object methodSignatureForSelector:sel];
//
//}
//
//- (void)forwardInvocation:(NSInvocation *)invocation{
//
// if (self.object) {
// [invocation invokeWithTarget:self.object];
// }else{
// NSLog(@"麻烦收集 stack");
// }
//
//}
@end
这里我们定义了一个继承于 NSProxy
的类 CXProxy
,这里需要先了解一下 NSProxy
,NSProxy
是一个实现了 NSObject
协议的根类,苹果的官方文档是这样描述它的:NSProxy
是一个抽象基类,它为一些表现的像是其它对象替身或者并不存在的对象定义API
。一般的,发送给代理的消息被转发给一个真实的对象或者代理本身引起加载(或者将本身转换成)一个真实的对象。NSProxy
的基类可以被用来透明的转发消息或者耗费巨大的对象的 lazy
初始化。
在 CXProxy
类中我们实现了 forwardingTargetForSelector
方法,返回 self.object
,这里 self.object
就是外部的控制器,所有发送给 CXProxy
的方法都会被转发给 self.object
。这里跟上面讲的中介者模式类似,但是这里需要注意一点就是在控制器释放的时候需要在 dealloc
方法中对 timer
进行释放。
趁着中秋放假这几天更新了几篇博客,有不足的地方还请多多指正。