共计 3942 个字符,预计需要花费 10 分钟才能阅读完成。
原文链接
开发过程中我们必不可少的需要接触定时器,在 iOS 中,常用的定时器有以下几种:
GCD Timer
CADisplayLink
NSTimer
这里我们主要来看下 NSTimer 的一个问题
#import “ViewController.h”
@interface ViewController ()
@property (nonatomic, strong) NSTimer *t;
@end
@implementation ViewController
– (void)viewDidLoad {
[super viewDidLoad];
}
– (void)startTImer {
_t = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(someBussiness) userInfo:nil repeats:true];
[[NSRunLoop currentRunLoop] addTimer:_t forMode:NSRunLoopCommonModes];
}
– (void)someBussiness {
NSLog(@”timer triggered”);
}
– (void)dealloc {
NSLog(@”Controller dealloc”);
if (self.t) {
[self.t invalidate];
}
}
– (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
if (self.presentingViewController) {
[self dismissViewControllerAnimated:true completion:nil];
}else {
ViewController *vc = ViewController.new;
vc.view.backgroundColor = UIColor.grayColor;
[self presentViewController:vc animated:true completion:nil];
[vc startTImer];
}
}
@end
这里在我们点击页面之后,会 present 出来一个新页面,并开始使用 NSTimer 计时,并在 dealloc 中打印信息。再次点击 present 出来的 viewController 把当前的 Controller 销毁掉。
再次点击我们会发现,计时器并没有停止,而且预期的 dealloc 中的信息也并没有打印,这是为什么呢?
这里我们可以使用 Xcode 的 Debug Memory Graph , 就在下方控制台上面的按键里面,可以看到如图所示
我们可以看到这里 Runloop 引用了 timer,而 timer 又引用了当前的 Controller,最终导致 Controller 无法释放
我们通常会想,那把 NSTimer 的 property 用 weak 来修饰,或者把 timer 的 target 使用 weak 修饰不就好了吗。那我们来修改一下代码
@property (nonatomic, weak) NSTimer *t;
– (void)startTImer {
__weak typeof(self) ws = self;
_t = [NSTimer timerWithTimeInterval:1.0f target:ws selector:@selector(someBussiness) userInfo:nil repeats:true];
[[NSRunLoop currentRunLoop] addTimer:_t forMode:NSRunLoopCommonModes];
}
这里我们修改 timer 的 property 为 weak,把 target 也修饰为 weak,再次运行。
哈,还是没有释放,timer 仍在打印。
这里其实是因为 Runloop 会对加入的 Timer 自动强引用,而 timer 会对 target 进行强引用,即使修饰为 weak 也没用,那么,有咩有什么办法来释放他呢?
– (void)startTImer {
__weak typeof(self) ws = self;
_t = [NSTimer timerWithTimeInterval:1.0f repeats:true block:^(NSTimer * _Nonnull timer) {
[ws someBussiness];
}];
[[NSRunLoop currentRunLoop] addTimer:_t forMode:NSRunLoopCommonModes];
}
???? ???? ???? 改为 Block 调用的方式就可以了,那么有没有别的方式也可以解决这个问题呢?(当然有了要不这篇我 tm 是在写啥)
NSProxy
An abstract superclass defining an API for objects that act as stand-ins for other objects or for objects that don’t exist yet.
一个抽象超类,用于定义对象的 API,这些对象充当其他对象或尚不存在的对象的替身。
官方文档
使用 NSProxy 我们可以把任意的对象隐藏在后面,由这个抽象类在前面为我们真实的对象代理,当然,我们需要实现两个方法
– (void)forwardInvocation:(NSInvocation *)invocation;
–
– (NSMethodSignature *)methodSignatureForSelector:(SEL)sel;
第一个是方法决议,我们可以在这里改变方法的指针,更换方法,第二个是方法签名,用来提供相应的函数返回类型和参数,
接下来我们新建 TimerProxy 类 继承 NSProxy
TimerProxy.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface TimerProxy : NSProxy
+ (instancetype)proxyWithObject:(id)obj;
@end
NS_ASSUME_NONNULL_END
TimerProxy.m
#import “TimerProxy.h”
@interface TimerProxy ()
@property (nonatomic, weak) id object;
@end
@implementation TimerProxy
– (instancetype)withProxy:(id)obj {
_object = obj;
return self;
}
+ (instancetype)proxyWithObject:(id)obj {
return [[self alloc] withProxy:obj];
}
– (void)forwardInvocation:(NSInvocation *)invocation {
SEL selector = invocation.selector;
if ([_object respondsToSelector:selector]) {
[invocation invokeWithTarget:_object];
}
}
– (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [_object methodSignatureForSelector:sel];
}
@end
再更新一下 viewController 的实现
#import “ViewController.h”
#import “TimerProxy.h”
@interface ViewController ()
@property (nonatomic, strong) NSTimer *t;
@end
@implementation ViewController
– (void)viewDidLoad {
[super viewDidLoad];
}
– (void)startTImer {
_t = [NSTimer timerWithTimeInterval:1.0f target:[TimerProxy proxyWithObject:self] selector:@selector(someBussiness) userInfo:nil repeats:true];
[[NSRunLoop currentRunLoop] addTimer:_t forMode:NSRunLoopCommonModes];
}
– (void)someBussiness {
NSLog(@”timer triggered”);
}
– (void)dealloc {
NSLog(@”Controller dealloc”);
if (self.t) {
[self.t invalidate];
}
}
– (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
if (self.presentingViewController) {
[self dismissViewControllerAnimated:true completion:nil];
}else {
ViewController *vc = ViewController.new;
vc.view.backgroundColor = UIColor.grayColor;
[self presentViewController:vc animated:true completion:nil];
[vc startTImer];
}
}
@end
应该可以看到正常的 dealloc 的输出,并且 timer 也停止了,NSProxy 是一个非常有用的抽象类,当然还有其他用途,比如能够模拟多继承,待后续补充。