0%

TextKit 的线程安全

苹果文档

Instances of the NSTextContainer, NSLayoutManager, and NSTextStorage classes can be accessed from threads other than the main thread as long as the app guarantees access from only one thread at a time.

这句话并不是说可以这些属性是线程安全的——必须保证每次访问都在同一个线程。

但是如果在主线程修改 NSTextStorage 中的 attributes,同时在另外一个线程尝试调用 NSLayoutManager 进行绘制其实是会有线程安全问题的。

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
@interface ViewController () <UITextViewDelegate>

@property (nonatomic, strong) NSTextStorage *storage;
@property (nonatomic, strong) NSLayoutManager *layoutManager;
@property (nonatomic, strong) NSTextContainer *textContainer;

@property (nonatomic, strong) UITextView *textView;
@property (nonatomic, strong) dispatch_queue_t queue;
@property (nonatomic, strong) NSLock *lock;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
m = i = 0;
_storage = [NSTextStorage new];

_layoutManager = ({
NSLayoutManager *layoutManager = [NSLayoutManager new];
[self.storage addLayoutManager:layoutManager];
layoutManager;
});
self.lock = [NSLock new];
_textContainer = ({
NSTextContainer *container = [NSTextContainer new];
[_layoutManager addTextContainer:container];
container;
});

_textView = ({
UITextView *textView = [[UITextView alloc] initWithFrame:CGRectMake(20, 100, 200, 200)
textContainer:_textContainer];
textView.textContainerInset = UIEdgeInsetsZero;
textView.textContainer.lineFragmentPadding = 0;
textView.backgroundColor = [UIColor clearColor];
textView.scrollEnabled = NO;
textView.showsVerticalScrollIndicator = NO;
textView.showsHorizontalScrollIndicator = NO;
textView.spellCheckingType = UITextSpellCheckingTypeNo;
textView;
});
[self.view addSubview:_textView];
_textView.text = @"啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊";

NSTimer *timer = [NSTimer timerWithTimeInterval:0 repeats:YES block:^(NSTimer * _Nonnull timer) {
[self updateAttributesInMainThread];
}];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
_queue = dispatch_queue_create("com.jft0m.draw", 0);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), self.queue, ^{
while (1) {
[self drawInBackgroundThread];
}
});
}

- (void)updateAttributesInMainThread {
// [self.lock lock];
if (i == 0) {
self.textView.frame = CGRectMake(20, 100, 200, 200);
i = 1;
} else {
self.textView.frame = CGRectMake(20, 100, 300, 200);
i = 0;
}
[self.textView sizeToFit];
// [self.lock unlock];
}

- (void)drawInBackgroundThread {
// [self.lock lock];
[self.storage beginEditing];
if (m == 0) {
NSDictionary *dic = @{NSFontAttributeName : [UIFont systemFontOfSize:14.f]};
NSRange range = NSMakeRange(0, self.layoutManager.numberOfGlyphs);
[self.storage removeAttribute:NSFontAttributeName
range:range];
[self.storage setAttributes:dic
range:range];
m = 1;
} else {
NSDictionary *dic = @{NSFontAttributeName : [UIFont systemFontOfSize:29.f]};
NSRange range = NSMakeRange(0, self.layoutManager.numberOfGlyphs);
[self.storage setAttributes:dic range:range];
m = 0;
}
[self.storage endEditing];

UIGraphicsBeginImageContext(CGSizeMake(10, 10));
CGContextRef ctx = UIGraphicsGetCurrentContext();
UIGraphicsPushContext(ctx);
NSRange range = NSMakeRange(0, self.layoutManager.numberOfGlyphs);
[self.layoutManager drawGlyphsForGlyphRange:range
atPoint:CGPointZero];
UIGraphicsPopContext();
UIGraphicsEndImageContext();
// [self.lock unlock];
}


@end

综上所述

有理由认为这句文档是建立在不使用 UITextView 的前提下。如果你使用了 UITextView 就必须保证 NSTextContainer, NSLayoutManager, and NSTextStorage 都在主线程访问。

因为 UITextView 在渲染的时候也会需要访问这三个属性。哪怕自己加锁也无法保证线程安全,因为 UITextView 内部对这三个属性的访问我们是无法控制的。