引言

在iOS开发中,多线程技术是提升应用性能与响应能力的关键。它能让应用在同一时间处理多个任务,避免主线程阻塞,为用户带来流畅的体验。本文将深入探讨Objective - C底层的多线程技术,涵盖原子锁atomic、GCD Timer、NSTimerCADisplayLink等知识点,并结合示例代码进行详细说明。同时,会着重分析NSTimerCADisplayLink可能造成的循环引用问题以及相应的解决办法。

一、原子锁 atomic

1.1 原理

在Objective - C中,atomic 是属性声明时的一个修饰符。当一个属性被声明为 atomic 时,编译器会自动生成一些额外的代码来保证该属性的 settergetter 方法是原子操作。也就是说,在多线程环境下,同一时间只会有一个线程对该属性进行读写操作,从而避免数据竞争。

1.2 使用示例

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
#import <Foundation/Foundation.h>

@interface AtomicExample : NSObject
@property (atomic, assign) NSInteger counter;
@end

@implementation AtomicExample

@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
AtomicExample *example = [[AtomicExample alloc] init];

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_async(queue, ^{
for (int i = 0; i < 10000; i++) {
example.counter++;
}
});

dispatch_async(queue, ^{
for (int i = 0; i < 10000; i++) {
example.counter++;
}
});

sleep(2);
NSLog(@"Counter value: %ld", (long)example.counter);
}
return 0;
}

1.3 优缺点

  • 优点:使用方便,只需要在属性声明时添加 atomic 修饰符,编译器会自动处理线程安全问题。
  • 缺点atomic 只能保证属性的 settergetter 方法是原子操作,但不能保证整个操作的原子性。例如,对于 self.counter++ 操作,它包含了读取、加一和写入三个步骤,atomic 无法保证这三个步骤的原子性。而且,atomic 会带来一定的性能开销,因为它需要进行额外的锁操作。

二、GCD Timer

2.1 原理

GCD Timer 是基于 Grand Central Dispatch(GCD)实现的定时器。GCD 是苹果提供的一套多线程编程解决方案,它将任务封装成块(block)并放入队列中执行。GCD Timer 利用 GCD 的特性,在指定的时间间隔内重复执行任务。

2.2 使用示例

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
42
43
44
45
46
47
48
49
50
#import <Foundation/Foundation.h>

@interface GCDTimerExample : NSObject
@property (nonatomic, strong) dispatch_source_t timer;
@end

@implementation GCDTimerExample

- (void)startTimer {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);

// 设置定时器的开始时间、间隔和精度
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
dispatch_source_set_timer(self.timer, start, interval, 0);

// 设置定时器的回调函数
__weak typeof(self) weakSelf = self;
dispatch_source_set_event_handler(self.timer, ^{
[weakSelf timerFired];
});

// 启动定时器
dispatch_resume(self.timer);
}

- (void)timerFired {
NSLog(@"GCD Timer fired");
}

- (void)stopTimer {
if (self.timer) {
dispatch_source_cancel(self.timer);
self.timer = nil;
}
}

@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
GCDTimerExample *example = [[GCDTimerExample alloc] init];
[example startTimer];

sleep(5);
[example stopTimer];
}
return 0;
}

2.3 优缺点

  • 优点:精度高,不会受到 RunLoop 的影响;可以在后台线程执行任务,不影响主线程的响应;可以通过 dispatch_source_set_timer 方法灵活设置定时器的开始时间、间隔和精度。
  • 缺点:使用相对复杂,需要手动管理定时器的创建、启动和停止。

三、NSTimer

3.1 原理

NSTimer 是基于 RunLoop 实现的定时器。RunLoop 是一个事件循环机制,负责处理各种事件,如触摸事件、定时器事件等。当创建一个 NSTimer 并将其添加到 RunLoop 中时,RunLoop 会在指定的时间间隔内触发定时器事件。

3.2 使用示例

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
#import <Foundation/Foundation.h>

@interface NSTimerExample : NSObject
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation NSTimerExample

- (void)startTimer {
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(timerFired)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}

- (void)timerFired {
NSLog(@"NSTimer fired");
}

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

@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
NSTimerExample *example = [[NSTimerExample alloc] init];
[example startTimer];

sleep(5);
[example stopTimer];
}
return 0;
}

3.3 循环引用问题

在上述示例中,NSTimertargetself,这会导致循环引用问题。NSTimer 会强引用 target,而 target 又持有 NSTimer 的引用,当 target 所在的对象需要被释放时,由于 NSTimer 对其的强引用,对象无法被释放,从而造成内存泄漏。

3.4 解决办法

3.4.1 使用中间对象(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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#import <Foundation/Foundation.h>

@interface WeakProxy : NSProxy
@property (nonatomic, weak, readonly) id target;
- (instancetype)initWithTarget:(id)target;
+ (instancetype)proxyWithTarget:(id)target;
@end

@implementation WeakProxy
- (instancetype)initWithTarget:(id)target {
_target = target;
return self;
}

+ (instancetype)proxyWithTarget:(id)target {
return [[WeakProxy alloc] initWithTarget:target];
}

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

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


@interface NSTimerProxyExample : NSObject
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation NSTimerProxyExample

- (void)startTimer {
WeakProxy *proxy = [WeakProxy proxyWithTarget:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:proxy
selector:@selector(timerFired)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}

- (void)timerFired {
NSLog(@"NSTimer fired");
}

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

@end

使用 NSProxy 作为中间对象,NSTimer 强引用 NSProxy,而 NSProxy 弱引用 target,避免了循环引用。

3.4.2 在合适的时机手动停止定时器

target 即将被释放时,手动调用 invalidate 方法停止定时器,打破循环引用。

1
2
3
4
- (void)dealloc {
[self.timer invalidate];
self.timer = nil;
}

3.5 优缺点

  • 优点:使用简单,只需要创建一个 NSTimer 并将其添加到 RunLoop 中即可;可以方便地设置定时器的时间间隔、是否重复等属性。
  • 缺点:精度相对较低,因为它依赖于 RunLoop 的运行状态。如果 RunLoop 被阻塞,定时器可能会延迟触发;只能在主线程或指定的 RunLoop 中运行;容易造成循环引用问题。

4.1 原理

CADisplayLink 是一个与屏幕刷新率同步的定时器,它会在每次屏幕刷新时触发一次回调。CADisplayLink 通常用于实现动画效果,因为它可以保证动画的流畅性。

4.2 使用示例

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
#import <Foundation/Foundation.h>
#import <QuartzCore/QuartzCore.h>

@interface CADisplayLinkExample : NSObject
@property (nonatomic, strong) CADisplayLink *displayLink;
@end

@implementation CADisplayLinkExample

- (void)startDisplayLink {
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkFired)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}

- (void)displayLinkFired {
NSLog(@"CADisplayLink fired");
}

- (void)stopDisplayLink {
if (self.displayLink) {
[self.displayLink invalidate];
self.displayLink = nil;
}
}

@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
CADisplayLinkExample *example = [[CADisplayLinkExample alloc] init];
[example startDisplayLink];

sleep(5);
[example stopDisplayLink];
}
return 0;
}

4.3 循环引用问题

NSTimer 类似,CADisplayLinktarget 强引用 self,而 self 又持有 CADisplayLink 的引用,会造成循环引用,导致 self 无法被释放,造成内存泄漏。

4.4 解决办法

4.4.1 使用中间对象(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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#import <Foundation/Foundation.h>
#import <QuartzCore/QuartzCore.h>

@interface WeakProxy : NSProxy
@property (nonatomic, weak, readonly) id target;
- (instancetype)initWithTarget:(id)target;
+ (instancetype)proxyWithTarget:(id)target;
@end

@implementation WeakProxy
- (instancetype)initWithTarget:(id)target {
_target = target;
return self;
}

+ (instancetype)proxyWithTarget:(id)target {
return [[WeakProxy alloc] initWithTarget:target];
}

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

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


@interface CADisplayLinkProxyExample : NSObject
@property (nonatomic, strong) CADisplayLink *displayLink;
@end

@implementation CADisplayLinkProxyExample

- (void)startDisplayLink {
WeakProxy *proxy = [WeakProxy proxyWithTarget:self];
self.displayLink = [CADisplayLink displayLinkWithTarget:proxy selector:@selector(displayLinkFired)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}

- (void)displayLinkFired {
NSLog(@"CADisplayLink fired");
}

- (void)stopDisplayLink {
if (self.displayLink) {
[self.displayLink invalidate];
self.displayLink = nil;
}
}

@end

利用 NSProxy 作为中间层,避免了 CADisplayLinktarget 的循环引用。

4.4.2 在合适的时机手动停止定时器

target 即将被释放时,手动调用 invalidate 方法停止 CADisplayLink,打破循环引用。

1
2
3
4
- (void)dealloc {
[self.displayLink invalidate];
self.displayLink = nil;
}

4.5 优缺点

  • 优点:与屏幕刷新率同步,能够实现流畅的动画效果;可以精确控制动画的帧率。
  • 缺点:只能用于实现与屏幕刷新相关的任务,如动画;如果处理任务的时间过长,可能会导致屏幕卡顿;容易造成循环引用问题。

五、不同多线程技术的比较与选择

5.1 性能比较

  • GCD Timer:性能较高,因为它基于 GCD 实现,不依赖于 RunLoop,能够在后台线程高效执行任务。
  • NSTimer:性能相对较低,因为它依赖于 RunLoop,当 RunLoop 被阻塞时,定时器的精度会受到影响。
  • CADisplayLink:性能与屏幕刷新率相关,如果处理任务的时间过长,会影响屏幕的流畅性。

5.2 适用场景

  • **原子锁 atomic**:适用于简单的属性读写操作,需要保证属性的线程安全。
  • GCD Timer:适用于需要高精度定时任务的场景,如后台定时数据更新、定时任务执行等。
  • NSTimer:适用于对精度要求不高的定时任务,如简单的倒计时、定时提醒等。
  • CADisplayLink:适用于实现动画效果,如 UI 动画、游戏动画等。

六、总结

在Objective - C开发中,掌握多线程技术是提升应用性能和用户体验的关键。原子锁 atomic、GCD Timer、NSTimerCADisplayLink 各有优缺点,适用于不同的场景。同时,NSTimerCADisplayLink 容易造成循环引用问题,开发者需要采取相应的解决办法。希望本文能帮助你深入理解Objective - C底层的多线程技术,在实际开发中灵活运用。

参考资料