解决NSTimer,强引用target引起的无法释放

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];
}

亲测有效,已结束!!!

你可能感兴趣的:(解决NSTimer,强引用target引起的无法释放)