天天看点

内存管理——定时器问题

内存管理系列文章

内存管理—MRC时代的手动内存管理

内存管理—weak的实现原理

内存管理——autorelease原理分析

内存管理——定时器问题

iOS程序的内存布局

CADisplayLink、NSTimer的循环引用问题

CADisplayLink

QuartzCore

框架下的的一种定时器,用在跟画图相关的处理当中。

NSTimer

大家应该很熟悉,是我们最常用的定时器。这两种定时器分别提供如下两个API

这两个API里面都有

target

参数,该

target

会被

CADisplayLink/NSTimer

强引用。如果

CADisplayLink

或者

NSTimer

作为属性被一个视图控制器VC强引用,当我们在调用上述两个API的时候,

target

参数传VC,这样VC和

CADisplayLink/NSTimer

之间便会形成引用循环,无法释放,造成内存泄漏。图示如下

内存管理——定时器问题

NSTimer的解决方案1

通过使用别的API来添加

NSTimer

,如

并且将

self

通过

__weak typeof(self) weakSelf == self;

包装成弱指针,传入其中即可。

NSTimer的解决方案2

通过增加一个中间代理对象来打破引用循环。请看下图

内存管理——定时器问题

如上图所示,在

timer

VC

之间增加一个代理对象

otherObject

timer

的强指针

target

指向

otherObject

otherObject

的弱指针

target

指向

VC

,这样就成功打破了引用循环。我们之所以需要借助第三者来破环,是因为

NSTimer

并非开源,我们无法修改其内部

target

的强弱性。因此只能通过一个自定义的代理对象来做一层引用中转,最终打破引用循环。

现在还有一个细节需要处理,增加代理对象

otherObject

之前,是由

timer

通过

target

直接调用

VC

里面的定时器方法的。现在中间多了一层

otherObject

,该如何实现定时器方法的调用呢?其实方法蛮多的,相信大家都能想出一些解决方案。这里就直接推荐一种比较巧妙的方法——通过消息转发。如下图

内存管理——定时器问题

因为代理对象的本质目的,就是打破引用循环,并且传递方法,了解OC消息机制的原理前提下,你应该很好理解消息转发的作用,正好可以巧妙的用在这个场景下。请好好体会一下。

下面是一份代码案例

#import "ViewController.h"
#import "CLProxy.h"

@interface ViewController ()
//@property (nonatomic, strong) CADisplayLink *link;
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    //CADisplayLink用来保证调用频率和屏幕的刷帧频率一致,60FPS
//    self.link = [CADisplayLink displayLinkWithTarget:[CLProxy proxyWithTarget:self] selector:@selector(linkTest)];
//    [self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
    
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[CLProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}

//- (void)linkTest {
//    NSLog(@"%s",__func__);
//}

- (void)timerTest {
    NSLog(@"%s",__func__);
}

-(void)dealloc {
    NSLog(@"%s",__func__);
}

@end

****************????代理类CLProxy???
**************** CLProxy.h  ****************
#import <Foundation/Foundation.h>

@interface CLProxy : NSObject
+(instancetype)proxyWithTarget: (id)target;
@property (weak, nonatomic) id target;

@end

**************** CLProxy.m  ****************
#import "CLProxy.h"
@implementation CLProxy

+(instancetype)proxyWithTarget: (id)target {
    CLProxy *proxy = [[CLProxy alloc] init];
    proxy.target = target;
    return proxy;
}


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

该方案同样适用于

CADisplayLink

,不再赘述。

认识NSProxy

大家可能看到过一个类叫

NSProxy

,但应该很少能用到,这是一个非常特殊的类。我们来对比一下它和

NSObject

的定义的对比

@interface NSProxy <NSObject> {
    Class	isa;
}

@interface NSObject <NSObject> {
    Class isa  ;
}
           

你可以看到,

NSProxy

NSObject

是同一层级的,因此也可以吧

NSProxy

理解成一个基类。他们都遵守

<NSObject>

协议,他们都没有父类。

那么

NSProxy

是干嘛用的呢?其实它就是专门用来解决通过中间对象转发消息的问题的。

这里先贴出案例代码

#import "ViewController.h"
#import "CLProxy2.h"

@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
   
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:[CLProxy2 proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}

- (void)timerTest {
    NSLog(@"%s",__func__);
}

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

****************????代理类CLProxy???
**************** CLProxy2.h  ****************
#import <Foundation/Foundation.h>

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

**************** CLProxy2.m  ****************

#import "CLProxy2.h"

@implementation CLProxy2

+(instancetype)proxyWithTarget: (id)target {
//NSProxy对象不需要调用init,因为它本来就没有init方法,直接alloc之后就可以使用
    CLProxy2 *proxy = [CLProxy2 alloc];
    proxy.target = target;
    return proxy;
    
}

@end
           

CLProxy2

继承自

NSProxy

,首先还是按照跟之前的案例的套路一样,将

VC

timer

CLProxy2

链接起来,我们先不在

CLProxy2

对消息做任何处理,看一下会有什么情况,结果是报错信息

可以看出,向

CLProxy2

对象发送一个它没有实现的方法(消息),最后会调用

methodSignatureForSelector

方法。如果你很熟悉**【OC消息机制】**的话,对继承自

NSObject

的类的实例对象发送消息,如果该对象没有实现对应的方法的话,出现的报错将是

也就是经典的

unrecognized selector sent to instance

这是怎么回事呢?其实

NSProxy

接受到消息之后的处理流程如下

  • [proxyObj message]

  • (1)到

    proxyObj

    的类对象里面寻找对应的方法,找到就调用
  • (2)尝试进入父类对象递归查找方法(省略该步骤)
  • (3)找不到方法,尝试进行方法动态解析(省略该步骤)
  • (4)尝试调用

    forwardingTargetForSelector

    进行消息转发`(省略该步骤)
  • (5)尝试调用

    methodSignatureForSelector

    +

    forwardInvocation

    进行消息转发。

因此可以发现,相比较完整的消息机制流程,

NSProxy

的处理过程中,省略了(2)、(3)、(4)步骤。所以它相比于

NSObject

,效率更高,我们的今天所讨论的代理对象传递消息问题,正好可以通过

NSProxy

来解决,提升效率。根绝第(5)步骤,我们只需要在子类里面实现

methodSignatureForSelector

+

forwardInvocation

这两个方法即可,上面的

CLProxy2.m

代码修改如下即可

#import "CLProxy2.h"

@implementation CLProxy2

+(instancetype)proxyWithTarget: (id)target {
//NSProxy对象不需要调用init,因为它本来就没有init方法,直接alloc之后就可以使用
    CLProxy2 *proxy = [CLProxy2 alloc];
    proxy.target = target;
    return proxy;
    
}


-(NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}

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

@end
           

以后碰到类似的通过中间对象传递消息的场景,最为推荐的就是利用

NSProxy

来实现。

标题如果别人问你CADisplayLink、NSTimer是否准时?

相信答案大家都会说:不准时。但是不准时的原因未必每个人都清楚。那这里就来简单梳理一下。

CADisplayLink

NSTimer

底层都是靠RunLoop来实现的,也就是可以把它们理解成RunLoop所需要处理的事件。我们知道RunLoop可以拿来刷新UI,处理定时器(

CADisplayLink

NSTimer

),处理点击滑动事件等非常多的事情。这里,就需要来了解一下RunLoop是如何触发

NSTimer

任务的。RunLoop每循环一圈,都会处理一定的事件,会消耗一定的时间,但是具体耗时多少这个是无法确定的。

假如你开启一个

timer

,隔1秒触发定时器事件,RunLoop会开始累计每一圈循环的用时,当时间累计够1秒,就会触发定时器事件。你有兴趣的话,是可以在RunLoop的源码里面找到时间累加相关代码的。可以借助下图来加深理解

内存管理——定时器问题

如果RunLoop在某一圈任务过于繁重,就可能出现如下情况

内存管理——定时器问题

所以

CADisplayLink

NSTimer

是无法保证准时性的。

GCD定时器

GCD的定时器是直接跟系统内核挂钩,不依赖于RunLoop机制,所以时间是相当精准的。GCD定时器的使用非常简单,如下所示

@interface ViewController ()
@property (nonatomic, strong) dispatch_source_t timer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //初始化定时器
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
    //开始时间
    dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, 3.0*NSEC_PER_SEC);
    //间隔时间
    uint64_t intervalTime = 1.0;
    //误差时间
    uint64_t leewayTime = 0;
    //设置定时器时间
    dispatch_source_set_timer(self.timer, startTime, 1.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    //设置定时器回调事件
    dispatch_source_set_event_handler(self.timer, ^{
        //定时器事件代码
        NSLog(@"GCD定时器事件");
        //如果定时器不需要重复,可以在这里取消定时器
        dispatch_source_cancel(self.timer);
    });
    //运行定时器
    dispatch_resume(self.timer);
    
}
           
GCD计时器细节:我们之前在RunLoop一章中讨论过使用

NSTimer

被界面滑动事件阻塞的问题,置于相同的场景下(GCD定时器放主线程),GCD定时器是不会受到UI界面滑动的印象的,其根本原因就是在于GCD定时器跟RunLoop是没有关系的,它们是两套独立的机制,因此GCD的定时器不会受到

RunLoopMode

的约束。大家可以自己通过代码体会一下。

另外需要注意一下,ARC环境下,GCD里面的创建的一些对象都是不需要销毁的。GCD已经帮我们做好了内存管理相关的事情。