(001)NSTimer浅析

NSTimer

timer是一个能从某时刻或者周期性的给target对象发送一条指定的消息。

定时器是线程通知自己做某件事的方法,定时器和你的RunLoop的特定的模式相关。如果定时器所在的模式当前未被RunLoop监视,那么定时器将不会开始 直到RunLoop运行在相应的模式下。如果RunLoop不在运行,那定时器也将永远不启动。

生命周期

NSTimer 会对外界传递的target进行retain。如果是一次性调用(repeats:NO),会在本次调用之后自身invalidate,并且NSTimer retain的那个target会做一次release。

但是,如果是多次重复调用,就需要我们自己手动进行invalidate,不然NSTimer一直存在。

准确性

不是实时机制

重复工作的定时器会基于安排好的时间而非实际时间调度它自己运行。举个例子,如果定时器被设定在某一特定时间开始 并 5 秒重复一次,那么定时器会在那个特定时间后 5 秒启动,即使在那个特定的触发 时间延迟了。如果定时器被延迟以至于它错过了一个或多个触发时间,那么定时器会 在下一个最近的触发事件启动,而后面会按照触发间隔正常执行 

对于UIScrollView的Timer

当使用NSTimer的scheduledTimerWithTimeInterval方法时。事实上此时Timer会被加入到当前线程的Run Loop中,且模式是默认的NSDefaultRunLoopMode。而如果当前线程就是主线程,也就是UI线程时,某些UI事件,比如UIScrollView的拖动操作,会将Run Loop切换成NSEventTrackingRunLoopMode模式,在这个过程中,默认的NSDefaultRunLoopMode模式中注册的事件是不会被执行的。也就是说,此时使用scheduledTimerWithTimeInterval添加到RunLoop中的Timer就不会执行。

 

NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(timer_callback) userInfo:nil repeats:YES];     
//使用NSRunLoopCommonModes模式,把timer加入到当前Run Loop中。  
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

 

 

在学习NSTimer的时候,遇到的一些问题整理。

通过 A页面  push  到 B页面,在B页面开启NSTimer,然后pop回到A页面的时候,发现没有执行dealloc。这个时候定时器还在运行,造成内存泄露。

代码如下:

#import "LJLNSTimerViewController.h"
#import "DeviceHeader.h"
#import "LJLWeakProxy.h"

static int timerNum = 0;
@interface LJLNSTimerViewController ()

@property(nonatomic, strong) NSTimer * timer;
@property(nonatomic, strong) NSThread *thread;
@property(nonatomic, assign) BOOL stopTimer;

@end

@implementation LJLNSTimerViewController

- (void)viewDidLoad {

    [super viewDidLoad];

//一、这样写pop的时候不会调用dealloc,造成内存泄露
//    因为LJLNSTimerViewController 与 timer 互相强引用
    self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerRun) userInfo:nil repeats:YES];

//    NSDefaultRunLoopMode 默认运行循环模式
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}

-(void)timerRun{
    timerNum++;
    NSLog(@"%s 触发计时器%d",__func__,timerNum);
}

- (void)dealloc{
    NSLog(@"%s",__func__);
    [self.timer invalidate];
}

B页面就是  LJLNSTimerViewController

  1. 首先要知道,NSTimer是运行在NSRunLoop中。
  2. 没有调用dealloc,说明B页面没有释放。
  3. 如下图,实线代表强引用。由于timer 和 TimerVC 互相强引用,所以造成了循环引用,无法正常释放。

尝试一:

首先想的肯定是把NSTimer的属性改成弱引用,把strong改成weak。

@property(nonatomic, weak) NSTimer * timer;

但是发现并没有什么作用,因这个时候虽然没有循环引用,但是RunLoop引用这timer,而timer又强引用这TimerViewController,pop 的时候指向 TimerViewcontroller 的指针销毁了,但是timer指向 TimerViewcontroller 的指针没有被销毁,所以仍然存在内存泄露,无法执行dealloc。

尝试二

既然不能直接传 self,那传 weakSelf 试试

__weak typeof(self) weakSelf = self;  
self.timer = [NSTimer scheduledTimerWithTimeInterval:1  target:weakSelf selector:@selector(timerAction:) userInfo:nil repeats:true]; 

测试结果还是发生了循环引用,B 没有释放,timer 对 weakSelf 这个变量是强引用的,timer -> weakSelf -> TimerViewcontroller -> timer,三者之间形成循环引用。

尝试三

分析可以通过添加一个中间代理对象来处理LJProxy。让TimerViewcontroller持有LJProxy实例,让LJProxy实例来弱引用TimerViewcontroller。timer强引用LJProxy实例。

@interface LJProxy : NSObject
+ (instancetype) proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
@implementation LJProxy
+ (instancetype) proxyWithTarget:(id)target
{
    LJProxy *proxy = [[LJProxy alloc] init];
    proxy.target = target;
    return proxy;
}

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return self.target;
}
@end

- (void)viewDidLoad {
    [super viewDidLoad];
    // 这里的target发生了变化
    self.timer = [NSTimer timerWithTimeInterval:1.0 target:[LJProxy proxyWithTarget:self] selector:@selector(timerRun) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
  1. 首先当指向pop的时候,1号指针被销毁,现在就没有强引用指向TimerViewcontroller,TimerViewcontroller就可以被正常销毁。
  2. TimerViewcontroller销毁会走dealloc方法,在dealloc里调用[self.timer invalidate];将timer从RunLoop移除,3号指针销毁
  3. 当TimerViewcontroller,销毁对应它强引用的指针也会销毁,那么2号指针也会销毁。
  4. 上面的执行完,timer已经没有被别的对象强引用,timer会销毁,LJProxy也会销毁

 

尝试四

经查资料知道有一个NSProxy类,这是专门用来做消息转发的类。

@interface LJLWeakProxy : NSProxy
@property(nonatomic, weak)id target;
+(instancetype)proxyWithTarget:(id)target;
@end


@implementation LJLWeakProxy

+ (instancetype)proxyWithTarget:(id)target{

    LJLWeakProxy * proxy = [LJLWeakProxy alloc];
    proxy.target = target;
    return proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{

    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
    return [invocation invokeWithTarget:self.target];
}
@end

下面是NSProxy的Apple文档说明,简单来说提供了几个信息

  • NSProxy是一个专门用来做消息转发的类
  • NSProxy是个抽象类,使用需自己写一个子类继承自NSProxy
  • NSProxy的子类需要实现两个方法,就是下面那两个
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
     return [self.target methodSignatureForSelector:sel];
} 

- (void)forwardInvocation:(NSInvocation *)invocation {
     [invocation invokeWithTarget:self.target];
}

 

引申:OC的消息查找机制     OC的消息转发机制

  1. 消息发送:
    1. 先从方法缓存中找方法,找不到就去当前类的方法列表中找,找到了加入方法缓存(最开始的缓存大小是4-1,超过3/4就清空扩容,扩容成原来的2倍4*2-1,然后缓存当前的方法,之前的清空了再用的时候在缓存,一直类推都是2倍扩容)。
    2. 如果找不到实例方法去父类里面重复前面的步骤进行查找。类方法按isa的查找链进行查找。如果找到底(基类NSObject或根元类)还是没有找到那个方法的话那么就进入下面步骤2
  2. 动态方法解析:
    1. 看该类中是否实现了resolveInstanceMethod:和resolveClassMethod:,如果实现了就解析动态添加的方法,调用该方法,如果没有实现进入步骤3
  3. 消息转发   分二步
    1. 调用forwardingTargetForSelector:,看返回的对象是否是nil,如果不是nil调用objc_msgSend传入对象和SEL;
    2. 如果返回的是nil,那么就调用调用methodSignatureForSelector:,返回方法签名,如果方法签名不为nil,调用forwardInvocation:来执行该方法。
  4. 如果消息转发还无法处理,就抛异常。

尝试二中,继承自NSObject的对象LJProxy。方法没有找到实现的时候回经过上面的1、2、3步骤操作才能抛出错误,如果在这个过程中我们做了补救措施,那么就不会抛出doesNotRecognizeSelector:,程序就可以正常执行,但是如果是继承自NSProxy的LJLWeakProxy,就会跳过前面的所有步骤,直接到第3步的第2小步,直接找到对象,执行方法,提高了性能。(NSProxy只有methodSignatureForSelector:和forwardInvocation:这两个方法,所以只能到第3步的第2小步)

 

方法五

借鉴:

NSTimer 已知是会强引用参数 target:self 的了,如果忘记关 timer 的话,传什么进去都会被强引用。timer 的功能就是定时调某个方法,而且NSTimer 的调用时间是不精确的!它挂在 runloop 上受线程切换,上一个事件执行时间的影响。

利用 dispatch_asyn() 定时执行函数。看下面代码。

- (void)loop {
    [self doSomething];
    ......
    // 休息 time 秒,再调 loop,实现定时调用
    [NSThread sleepForTimeInterval:time];
    dispatch_async(self.runQueue, ^{
        [weakSelf loop];
    });    
}

dispatch_async 中调 loop 不会产生递归调用
dispatch_async 是在队列中添加一个任务,由 GCD 去回调 [weakSelf loop]

这办法解决了timer 不能释放,挂在 runloop 不能移除的问题。

这样就可以自己写一个timer。controller 释放,timer 也自动停止释放  GitHub下载地址
或者是在 viewWillAppear 开启,在 viewWillDisappear 关闭。来解决NSTimer强引用VC不释放的问题。方法很多可以自行查找。

子线程使用NSTimer

一、

//四、子线程添加NSTimer 如果直接pop会内存泄露,需要再pop之前处理停止NSTimer
    __weak typeof(self) weakSelf = self;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        __strong typeof(weakSelf) strongWeakSelf = weakSelf;
        if (strongWeakSelf) {
            strongWeakSelf.thread = [NSThread currentThread];
            [strongWeakSelf.thread setName:@"线程1"];
            strongWeakSelf.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:strongWeakSelf selector:@selector(timerRun) userInfo:nil repeats:YES];
            NSRunLoop * runloop = [NSRunLoop currentRunLoop];
            [runloop addTimer:strongWeakSelf.timer forMode:NSDefaultRunLoopMode];
            [runloop run];
        }
    });

- (void)cancelTimer{
    if (self.timer && self.thread) {
        [self performSelector:@selector(cancel) onThread:self.thread withObject:nil waitUntilDone:YES];
    }
}

- (void)cancel{
    if (self.timer) {
        [self.timer invalidate];
        self.timer = nil;
    }
}

不能直接交给dealloc处理。需要手动添加pop事件,然后在执行pop的时候先去销毁NSTimer。

二、

//五、子线程添加 NSTimer 解决内存泄露问题
    self.timer = [NSTimer timerWithTimeInterval:1.0 target:[LJLWeakProxy proxyWithTarget:self] selector:@selector(timerRun) userInfo:nil repeats:YES] ;

    __weak typeof(self) weakSelf = self;
    self.thread = [[NSThread alloc] initWithBlock:^{
        [[NSRunLoop currentRunLoop] addTimer:weakSelf.timer forMode:NSDefaultRunLoopMode];
        // 这里需要注意不要使用[[NSRunLoop currentRunLoop] run]
        while (weakSelf && !weakSelf.stopTimer) {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
    }];
    [self.thread start];

- (void)dealloc
{
    NSLog(@"%s",__func__);
    [self stop];    
}

-(void)stop
{
    if (self.timer && self.thread) {
        [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
    }
}

-(void)stopThread
{
//    设置标记为YES
    self.stopTimer = YES;
//    停止RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());
//    清空线程
    self.thread = nil;
}

这里不使用[[NSRunLoop currenRunLoop] run]来开启当前线程RunLoop,而是使用runMode: beforeDate:。

因为通过run方法开启的RunLoop没有相应的停止方法,所以使用while循环和runMode: beforeDate:来运行RunLoop。

 

 

 

参考:

https://www.jianshu.com/p/d4589134358a

https://www.jianshu.com/p/bb691938fb2f

发布了83 篇原创文章 · 获赞 12 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/shengdaVolleyball/article/details/104656411
001