NSTimer 是iOS开发中常用的定时器,使用过程中也有一些弊端:
1. 受到RunLoopde影响出现计时准确性问题(很多大佬都转而使用GCD计时器)
2. 对target的强引用而导致页面无法释放的问题。
本文分别提供了Swift和Object-C两中开发语言的解决办法。
一、 先补充一下RunLoopMode
1.开发者常用的模式
NSDefaultRunLoopMode
: 默认模式,通常主线程是在这个Mode下运行的
UITrackingRunLoopMode
:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动的时候不受其他Mode的影响
kCFRunLoopCommonModes
:这是一个占位用的Mode,不是一种真正的Mode, 默认包含NSDefaultRunLoopMode、NSModalPanelRunLoopMode、NSEventTrackingRunLoopMode这三个模式。(计时器我们主要使用这个模式
)
2.开发者几乎不用的模式
UIInitalizationRunLoopMode
: 在刚启动App时进入的第一个Mode,启动完成以后就不再使用
GSEventReceiveRunLoopMode
:接受系统事件的内部Mode,通常用不到
通常情况下,我们会把Timer加入到Runloop中启用:
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
二、 分析原因
先看定时器创建的代码
@interface ViewController ()
@property (nonatomic, strong) NSTimer * timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(runTest) userInfo:nil repeats:YES];
}
- (void)runTest {
NSLog(@"%s", __func__ );
}
- (void)dealloc {
NSLog(@"%s", __func__ );
[self.timer invalidate];
}
@end
1、ViewController 有一个强引用引用着定时器
2、 定时器会对target产生一个强引用
3、产生了一个循环引用,两个都无法释放了
一说到循环引用,很多人脑子里的第一个想法就是用一个弱指针不就好了么?然后就一顿操作,以为解决了,立马运行一下验证结果
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target: weakSelf selector:@selector(runTest) userInfo:nil repeats:YES];
结果一脸懵逼。。。没解决啊
为什么不能呢?其实是我们自己的思维定式了,一看到循环引用就weakSelf
,好好回想一下,上一次解决循环引用时,使用weakSelf
用在什么地方的?答案是: block
原因:
1、 __weak typeof(self) weakSelf = self;
其实是用在block
里面的。block
有个特点:在 block内部使用弱指针,那么block会对外面的对象产生一个弱引用
。
2、 我们这里传的target
其实就是传一个参数而已,不管是传weakSelf
或者是self
最终传的是一个内存地址
而已。
3、对于传进去的target
对他产生强引用还是弱引用应该是NSTimer
内部定义接收这个传进来的target
的属性决定的,并非是外部决定的。
NSTimer内部可能可能可能是定义了类似下面这样的target:
@interface NSTimer ()
@property (nonatomic, strong) id target;
@end
所以,跟外面传进来的没啥关系,也就解决不了问题。
所以它能解决的是iOS10
提供的Block的形式的定时器:
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf runTest];
}];
三、 解决NSTimer强引用target无法释放
1.更换方法,使用block回调。
使用系统iOS10及以后
提供了一个使用block回调,可以避免target无法释放的计时器方法:
(1)、swift版的方法:
@available(iOS 10.0, *)
public /*not inherited*/ init(timeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void)
(2)、oc版的方法:
+ (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));
2.使用一个中间介来做消息转发。(推荐
)
(1)、swift版的方法:
import Foundation
class TimerTarget: NSObject {
weak var target :AnyObject?
convenience init(_ target:AnyObject) {
self.init()
self.target = target
}
override func forwardingTarget(for aSelector: Selector!) -> Any? {
return target
}
deinit {
print("---- deinit ---")
}
}
具体使用:
func initTimer() {
timer = Timer(timeInterval: 1.0, target: TimerTarget(self), selector: #selector(timerStarted), userInfo: nil, repeats: true)
RunLoop.current.add(timer!, forMode: .common)
}
(2)、oc版的方法:
oc版本使用借助NSProxy
这个抽象类,相比NSObject,NSProxy更轻量级, 做消息转发效率更高.必须继承实例化其子类才能使用。
TimerProxy.h
文件
#import
NS_ASSUME_NONNULL_BEGIN
@interface TimerProxy : NSProxy
- (instancetype)initWithTarget:(id)target;
+ (instancetype)proxyWithTarget:(id)target;
@end
NS_ASSUME_NONNULL_END
TimerProxy.m
文件
#import "TimerProxy.h"
@interface TimerProxy ()
@property (nonatomic, weak) id target;
@end
@implementation TimerProxy
- (instancetype)initWithTarget:(id)target {
_target = target;
return self;
}
+ (instancetype)proxyWithTarget:(id)target {
return [[TimerProxy alloc] initWithTarget:target];
}
- (id)forwardingTargetForSelector:(SEL)selector {
return _target;
}
@end
具体使用:
- (void)initTimer {
TimerProxy * proxy = [TimerProxy proxyWithTarget:self];
self.timer = [NSTimer timerWithTimeInterval:1.0 target:proxy selector:@selector(timerStart) userInfo:nil repeats:YES];//NSRunLoopCommonModes
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
亲测有效,已结束!!!