【iOS】OC底层系列十五 - 内存管理-定时器

面试题:使用CADisplayLinkNSTimer有什么注意点(一般指内存管理方面)?

一、CADisplayLink、NSTimer

NSTimer是可以设置时间间隔的,自动添加到RunLoop中。而CADisplayLink默认情况下是保证调用频率和屏幕的刷帧频率一致(60FPS),并且CADisplayLink必须手动添加到RunLoop中,否则不会工作。

问题:CADisplayLinkNSTimer会对target产生强引用,如果target又对它们产生强引用,那么就会引发循环引用。

解决方案:使用block 或 使用代理对象。

使用block:(仅限NSTimer,因为CADisplayLink不支持block)

1
2
3
4
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[weakSelf timerTest];
}];

使用代理对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@interface DBProxy : NSObject

+ (instancetype)proxyWithTarget:(id)target;
// 注意:此处是弱引用
@property (weak, nonatomic) id target;

@end

@implementation DBProxy

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

// 由于方法是在target中定义的,所以需要把消息转发到target执行
- (id)forwardingTargetForSelector:(SEL)aSelector {
return self.target;
}

@end

@interface ViewController ()

@property (strong, nonatomic) CADisplayLink *link;
@property (strong, nonatomic) NSTimer *timer;

@end

@implementation ViewController

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

self.link = [CADisplayLink displayLinkWithTarget:[DBProxy proxyWithTarget:self] selector:@selector(linkTest)];
[self.link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}

@end

使用代理对象时,也可以使用NSProxy

NSProxy和NSObject一样,都是基类(没有继承父类)。

1
2
3
4
5
6
7
@interface NSProxy <NSObject> {
Class isa;
}

@interface NSObject <NSObject> {
Class isa;
}

使用NSProxy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@interface DBProxy : NSProxy

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

@end

@implementation MJProxy

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

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

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

@end

NSProxy没有forwardingTargetForSelector:方法,只有methodSignatureForSelector:forwardInvocation:,所以需要重写这两个方法让消息转发给传入的target。如果没有重写这两个方法,就会报错:-[NSProxy methodSignatureForSelector:] called!

假设NSProxy的target是控制器对象,[proxy isKindOfClass:self]的执行结果是1,因为NSProxy内部没有isKindOfClass方法(遵守了NSObject协议,所以可以调用该方法),最终会进入消息转发阶段,所以这句代码的本质就是[target isKindOfClass:self]

如果使用NSProxy,方法找不到时会直接进入到消息转发阶段。而使用自定义代理对象(继承NSObject),会通过isa指针查找类对象,再到类方法列表/缓存列表查找方法,如果找不到再进入动态解析阶段,最后才进入消息转发阶段。所以从这一层面也可以看出,**NSProxy的效率更高,它是专门用来做消息转发的**。

二、GCD定时器

NSTimer依赖于RunLoop,如果RunLoop的任务过于繁重,可能会导致NSTimer不准时。而GCD的定时器会更加准时,因为GCD不依赖RunLoop。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
dispatch_queue_t queue = dispatch_queue_create("timer", DISPATCH_QUEUE_SERIAL);
// 创建定时器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 设置时间
uint64_t start = 2.0; // 2秒后开始执行
uint64_t interval = 1.0; // 每隔1秒执行
dispatch_source_set_timer(timer,
dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
interval * NSEC_PER_SEC,
0);

// 设置回调
dispatch_source_set_event_handler(timer, ^{
NSLog(@"1111");
});
// 也可以使用函数回调(timerFuncHandle是dispatch_function_t类型)
// typedef void (*dispatch_function_t)(void *_Nullable);
// dispatch_source_set_event_handler_f(timer, timerEventHandle);

// 启动定时器
dispatch_resume(timer);

void timerEventHandle(void *param) {
NSLog(@"timerEventHandle - %@", [NSThread currentThread]);
}