Skip to content
This repository was archived by the owner on Feb 2, 2023. It is now read-only.

Commit 9ddf68f

Browse files
author
Scott Goodson
committed
[ASTextNode] Optimize handling of constrained size to almost never recreate NSLayoutManager
This also fixes two fairly subtle but serious bugs, #1076 and #1046.
1 parent 82f7956 commit 9ddf68f

9 files changed

Lines changed: 81 additions & 55 deletions

AsyncDisplayKit/ASCollectionNode+Beta.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* of patent rights can be found in the PATENTS file in the same directory.
77
*/
88

9+
#import "ASCollectionNode.h"
910
@protocol ASCollectionViewLayoutFacilitatorProtocol;
1011

1112
NS_ASSUME_NONNULL_BEGIN

AsyncDisplayKit/ASTextNode.mm

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -35,34 +35,32 @@ @interface ASTextNodeDrawParameters : NSObject
3535

3636
- (instancetype)initWithRenderer:(ASTextKitRenderer *)renderer
3737
textOrigin:(CGPoint)textOrigin
38-
backgroundColor:(CGColorRef)backgroundColor;
38+
backgroundColor:(UIColor *)backgroundColor;
3939

4040
@property (nonatomic, strong, readonly) ASTextKitRenderer *renderer;
4141

4242
@property (nonatomic, assign, readonly) CGPoint textOrigin;
4343

44-
@property (nonatomic, assign, readonly) CGColorRef backgroundColor;
44+
@property (nonatomic, strong, readonly) UIColor *backgroundColor;
4545

4646
@end
4747

4848
@implementation ASTextNodeDrawParameters
4949

5050
- (instancetype)initWithRenderer:(ASTextKitRenderer *)renderer
5151
textOrigin:(CGPoint)textOrigin
52-
backgroundColor:(CGColorRef)backgroundColor
52+
backgroundColor:(UIColor *)backgroundColor
5353
{
5454
if (self = [super init]) {
5555
_renderer = renderer;
5656
_textOrigin = textOrigin;
57-
_backgroundColor = CGColorRetain(backgroundColor);
57+
_backgroundColor = backgroundColor;
5858
}
5959
return self;
6060
}
6161

6262
- (void)dealloc
6363
{
64-
CGColorRelease(_backgroundColor);
65-
6664
// Destruction of the layout managers/containers/text storage is quite
6765
// expensive, and can take some time, so we dispatch onto a bg queue to
6866
// actually dealloc.
@@ -182,7 +180,7 @@ - (NSString *)description
182180
NSString *truncationString = [_composedTruncationString string];
183181
if (plainString.length > 50)
184182
plainString = [[plainString substringToIndex:50] stringByAppendingString:@"\u2026"];
185-
return [NSString stringWithFormat:@"<%@: %p; text = \"%@\"; truncation string = \"%@\"; frame = %@>", self.class, self, plainString, truncationString, self.nodeLoaded ? NSStringFromCGRect(self.layer.frame) : nil];
183+
return [NSString stringWithFormat:@"<%@: %p; text = \"%@\"; truncation string = \"%@\"; frame = %@; renderer = %p>", self.class, self, plainString, truncationString, self.nodeLoaded ? NSStringFromCGRect(self.layer.frame) : nil, _renderer];
186184
}
187185

188186
#pragma mark - ASDisplayNode
@@ -240,13 +238,13 @@ - (void)didLoad
240238
- (void)setFrame:(CGRect)frame
241239
{
242240
[super setFrame:frame];
243-
[self _invalidateRendererIfNeeded:frame.size];
241+
[self _invalidateRendererIfNeededForBoundsSize:frame.size];
244242
}
245243

246244
- (void)setBounds:(CGRect)bounds
247245
{
248246
[super setBounds:bounds];
249-
[self _invalidateRendererIfNeeded:bounds.size];
247+
[self _invalidateRendererIfNeededForBoundsSize:bounds.size];
250248
}
251249

252250
#pragma mark - Renderer Management
@@ -291,12 +289,12 @@ - (void)_invalidateRenderer
291289

292290
- (void)_invalidateRendererIfNeeded
293291
{
294-
[self _invalidateRendererIfNeeded:self.bounds.size];
292+
[self _invalidateRendererIfNeededForBoundsSize:self.bounds.size];
295293
}
296294

297-
- (void)_invalidateRendererIfNeeded:(CGSize)newSize
295+
- (void)_invalidateRendererIfNeededForBoundsSize:(CGSize)boundsSize
298296
{
299-
if ([self _needInvalidateRenderer:newSize]) {
297+
if ([self _needInvalidateRendererForBoundsSize:boundsSize]) {
300298
// Our bounds of frame have changed to a size that is not identical to our constraining size,
301299
// so our previous layout information is invalid, and TextKit may draw at the
302300
// incorrect origin.
@@ -305,17 +303,17 @@ - (void)_invalidateRendererIfNeeded:(CGSize)newSize
305303
}
306304
}
307305

308-
- (BOOL)_needInvalidateRenderer:(CGSize)newSize
306+
- (BOOL)_needInvalidateRendererForBoundsSize:(CGSize)boundsSize
309307
{
310308
if (!_renderer) {
311309
return YES;
312310
}
313311

314312
// If the size is not the same as the constraint we provided to the renderer, start out assuming we need
315313
// a new one. However, there are common cases where the constrained size doesn't need to be the same as calculated.
316-
CGSize oldSize = _renderer.constrainedSize;
314+
CGSize rendererConstrainedSize = _renderer.constrainedSize;
317315

318-
if (CGSizeEqualToSize(newSize, oldSize)) {
316+
if (CGSizeEqualToSize(boundsSize, rendererConstrainedSize)) {
319317
return NO;
320318
} else {
321319
// It is very common to have a constrainedSize with a concrete, specific width but +Inf height.
@@ -324,7 +322,12 @@ - (BOOL)_needInvalidateRenderer:(CGSize)newSize
324322
// experience truncation and don't need to recreate the renderer with the size it already calculated,
325323
// as this would essentially serve to set its constrainedSize to be its calculatedSize (unnecessary).
326324
ASLayout *layout = self.calculatedLayout;
327-
if (layout != nil && CGSizeEqualToSize(newSize, layout.size)) {
325+
if (layout != nil && CGSizeEqualToSize(boundsSize, layout.size)) {
326+
if (!CGSizeEqualToSize(boundsSize, rendererConstrainedSize)) {
327+
// Don't bother changing _constrainedSize, as ASDisplayNode's -measure: method would have a cache miss
328+
// and ask us to recalculate layout if it were called with the same calculatedSize that got us to this point!
329+
_renderer.constrainedSize = boundsSize;
330+
}
328331
return NO;
329332
} else {
330333
return YES;
@@ -409,12 +412,10 @@ + (void)drawRect:(CGRect)bounds withParameters:(ASTextNodeDrawParameters *)param
409412

410413
// Fill background
411414
if (!isRasterizing) {
412-
CGColorRef backgroundColor = parameters.backgroundColor;
415+
UIColor *backgroundColor = parameters.backgroundColor;
413416
if (backgroundColor) {
414-
CGContextSetFillColorWithColor(context, backgroundColor);
415-
CGContextSetBlendMode(context, kCGBlendModeCopy);
416-
CGContextFillRect(context, CGContextGetClipBoundingBox(context));
417-
CGContextSetBlendMode(context, kCGBlendModeNormal);
417+
[backgroundColor setFill];
418+
UIRectFillUsingBlendMode(CGContextGetClipBoundingBox(context), kCGBlendModeCopy);
418419
}
419420
}
420421

@@ -430,14 +431,15 @@ + (void)drawRect:(CGRect)bounds withParameters:(ASTextNodeDrawParameters *)param
430431

431432
- (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer
432433
{
433-
[self _invalidateRendererIfNeeded];
434+
CGRect bounds = self.bounds;
435+
[self _invalidateRendererIfNeededForBoundsSize:bounds.size];
434436

435437
// Offset the text origin by any shadow padding
436438
UIEdgeInsets shadowPadding = [self shadowPadding];
437-
CGPoint textOrigin = CGPointMake(self.bounds.origin.x - shadowPadding.left, self.bounds.origin.y - shadowPadding.top);
439+
CGPoint textOrigin = CGPointMake(bounds.origin.x - shadowPadding.left, bounds.origin.y - shadowPadding.top);
438440
return [[ASTextNodeDrawParameters alloc] initWithRenderer:[self _renderer]
439441
textOrigin:textOrigin
440-
backgroundColor:self.backgroundColor.CGColor];
442+
backgroundColor:self.backgroundColor];
441443
}
442444

443445
#pragma mark - Attributes

AsyncDisplayKit/TextKit/ASTextKitContext.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
constrainedSize:(CGSize)constrainedSize
3131
layoutManagerFactory:(NSLayoutManager*(*)(void))layoutManagerFactory;
3232

33+
@property (nonatomic, assign, readwrite) CGSize constrainedSize;
34+
3335
/**
3436
All operations on TextKit values MUST occur within this locked context. Simultaneous access (even non-mutative) to
3537
TextKit components may cause crashes.

AsyncDisplayKit/TextKit/ASTextKitContext.mm

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@ - (instancetype)initWithAttributedString:(NSAttributedString *)attributedString
4949
return self;
5050
}
5151

52+
- (CGSize)constrainedSize
53+
{
54+
return _textContainer.size;
55+
}
56+
57+
- (void)setConstrainedSize:(CGSize)constrainedSize
58+
{
59+
_textContainer.size = constrainedSize;
60+
}
61+
5262
- (void)performBlockWithLockedTextKitComponents:(void (^)(NSLayoutManager *,
5363
NSTextStorage *,
5464
NSTextContainer *))block

AsyncDisplayKit/TextKit/ASTextKitRenderer.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737

3838
/**
3939
Designated Initializer
40-
dvlkferufedgjnhjjfhldjedlunvtdtv
4140
@discussion Sizing will occur as a result of initialization, so be careful when/where you use this.
4241
*/
4342
- (instancetype)initWithTextKitAttributes:(const ASTextKitAttributes &)textComponentAttributes
@@ -51,7 +50,7 @@ dvlkferufedgjnhjjfhldjedlunvtdtv
5150

5251
@property (nonatomic, assign, readonly) ASTextKitAttributes attributes;
5352

54-
@property (nonatomic, assign, readonly) CGSize constrainedSize;
53+
@property (nonatomic, assign, readwrite) CGSize constrainedSize;
5554

5655
#pragma mark - Drawing
5756
/*

AsyncDisplayKit/TextKit/ASTextKitRenderer.mm

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
#import "ASTextKitTailTruncater.h"
1818
#import "ASTextKitTruncating.h"
1919

20+
//#define LOG(...) NSLog(__VA_ARGS__)
21+
#define LOG(...)
22+
2023
static NSCharacterSet *_defaultAvoidTruncationCharacterSet()
2124
{
2225
static NSCharacterSet *truncationCharacterSet;
@@ -65,12 +68,10 @@ - (ASTextKitTailTruncater *)truncater
6568
{
6669
if (!_truncater) {
6770
ASTextKitAttributes attributes = _attributes;
68-
// We must inset the constrained size by the size of the shadower.
69-
CGSize shadowConstrainedSize = [[self shadower] insetSizeWithConstrainedSize:_constrainedSize];
71+
NSCharacterSet *avoidTailTruncationSet = attributes.avoidTailTruncationSet ? : _defaultAvoidTruncationCharacterSet();
7072
_truncater = [[ASTextKitTailTruncater alloc] initWithContext:[self context]
7173
truncationAttributedString:attributes.truncationAttributedString
72-
avoidTailTruncationSet:attributes.avoidTailTruncationSet ?: _defaultAvoidTruncationCharacterSet()
73-
constrainedSize:shadowConstrainedSize];
74+
avoidTailTruncationSet:avoidTailTruncationSet];
7475
}
7576
return _truncater;
7677
}
@@ -79,6 +80,7 @@ - (ASTextKitContext *)context
7980
{
8081
if (!_context) {
8182
ASTextKitAttributes attributes = _attributes;
83+
// We must inset the constrained size by the size of the shadower.
8284
CGSize shadowConstrainedSize = [[self shadower] insetSizeWithConstrainedSize:_constrainedSize];
8385
_context = [[ASTextKitContext alloc] initWithAttributedString:attributes.attributedString
8486
lineBreakMode:attributes.lineBreakMode
@@ -92,6 +94,30 @@ - (ASTextKitContext *)context
9294

9395
#pragma mark - Sizing
9496

97+
- (CGSize)size
98+
{
99+
if (!_sizeIsCalculated) {
100+
[self _calculateSize];
101+
_sizeIsCalculated = YES;
102+
}
103+
return _calculatedSize;
104+
}
105+
106+
- (void)setConstrainedSize:(CGSize)constrainedSize
107+
{
108+
if (!CGSizeEqualToSize(constrainedSize, _constrainedSize)) {
109+
_sizeIsCalculated = NO;
110+
_constrainedSize = constrainedSize;
111+
// If the context isn't created yet, it will be initialized with the appropriate size when next accessed.
112+
if (_context) {
113+
// If we're updating an existing context, make sure to use the same inset logic used during initialization.
114+
// This codepath allows us to reuse the
115+
CGSize shadowConstrainedSize = [[self shadower] insetSizeWithConstrainedSize:constrainedSize];
116+
_context.constrainedSize = shadowConstrainedSize;
117+
}
118+
}
119+
}
120+
95121
- (void)_calculateSize
96122
{
97123
// Force glyph generation and layout, which may not have happened yet (and isn't triggered by
@@ -111,16 +137,7 @@ - (void)_calculateSize
111137
// to make sure our width calculations aren't being offset by glyphs going beyond the constrained rect.
112138
boundingRect = CGRectIntersection(boundingRect, {.size = constrainedRect.size});
113139

114-
_calculatedSize = [_shadower outsetSizeWithInsetSize:CGSizeMake(boundingRect.size.width + boundingRect.origin.x, boundingRect.size.height + boundingRect.origin.y)];
115-
}
116-
117-
- (CGSize)size
118-
{
119-
if (!_sizeIsCalculated) {
120-
[self _calculateSize];
121-
_sizeIsCalculated = YES;
122-
}
123-
return _calculatedSize;
140+
_calculatedSize = [_shadower outsetSizeWithInsetSize:boundingRect.size];
124141
}
125142

126143
#pragma mark - Drawing
@@ -136,8 +153,12 @@ - (void)drawInContext:(CGContextRef)context bounds:(CGRect)bounds;
136153
[[self shadower] setShadowInContext:context];
137154
UIGraphicsPushContext(context);
138155

156+
LOG(@"%@, shadowInsetBounds = %@",self, NSStringFromCGRect(shadowInsetBounds));
157+
139158
[[self context] performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) {
159+
LOG(@"usedRect: %@", NSStringFromCGRect([layoutManager usedRectForTextContainer:textContainer]));
140160
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
161+
LOG(@"boundingRect: %@", NSStringFromCGRect([layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer]));
141162
[layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:shadowInsetBounds.origin];
142163
[layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:shadowInsetBounds.origin];
143164
}];

AsyncDisplayKit/TextKit/ASTextKitTailTruncater.mm

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,18 @@ @implementation ASTextKitTailTruncater
1818
__weak ASTextKitContext *_context;
1919
NSAttributedString *_truncationAttributedString;
2020
NSCharacterSet *_avoidTailTruncationSet;
21-
CGSize _constrainedSize;
2221
}
2322
@synthesize visibleRanges = _visibleRanges;
2423
@synthesize truncationStringRect = _truncationStringRect;
2524

2625
- (instancetype)initWithContext:(ASTextKitContext *)context
2726
truncationAttributedString:(NSAttributedString *)truncationAttributedString
2827
avoidTailTruncationSet:(NSCharacterSet *)avoidTailTruncationSet
29-
constrainedSize:(CGSize)constrainedSize
3028
{
3129
if (self = [super init]) {
3230
_context = context;
3331
_truncationAttributedString = truncationAttributedString;
3432
_avoidTailTruncationSet = avoidTailTruncationSet;
35-
_constrainedSize = constrainedSize;
3633

3734
[self _truncate];
3835
}

AsyncDisplayKit/TextKit/ASTextKitTruncating.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
*/
3232
- (instancetype)initWithContext:(ASTextKitContext *)context
3333
truncationAttributedString:(NSAttributedString *)truncationAttributedString
34-
avoidTailTruncationSet:(NSCharacterSet *)avoidTailTruncationSet
35-
constrainedSize:(CGSize)constrainedSize;
34+
avoidTailTruncationSet:(NSCharacterSet *)avoidTailTruncationSet;
3635

3736
@end

AsyncDisplayKitTests/ASTextKitTruncationTests.mm

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,7 @@ - (void)testEmptyTruncationStringSameAsStraightTextKitTailTruncation
5050
}];
5151
ASTextKitTailTruncater *tailTruncater = [[ASTextKitTailTruncater alloc] initWithContext:context
5252
truncationAttributedString:nil
53-
avoidTailTruncationSet:nil
54-
constrainedSize:constrainedSize];
53+
avoidTailTruncationSet:nil];
5554
XCTAssert(NSEqualRanges(textKitVisibleRange, tailTruncater.visibleRanges[0]));
5655
}
5756

@@ -67,8 +66,7 @@ - (void)testSimpleTailTruncation
6766
layoutManagerFactory:nil];
6867
ASTextKitTailTruncater *tailTruncater = [[ASTextKitTailTruncater alloc] initWithContext:context
6968
truncationAttributedString:[self _simpleTruncationAttributedString]
70-
avoidTailTruncationSet:[NSCharacterSet characterSetWithCharactersInString:@""]
71-
constrainedSize:constrainedSize];
69+
avoidTailTruncationSet:[NSCharacterSet characterSetWithCharactersInString:@""]];
7270
__block NSString *drawnString;
7371
[context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) {
7472
drawnString = textStorage.string;
@@ -90,8 +88,7 @@ - (void)testAvoidedCharTailWordBoundaryTruncation
9088
layoutManagerFactory:nil];
9189
ASTextKitTailTruncater *tailTruncater = [[ASTextKitTailTruncater alloc] initWithContext:context
9290
truncationAttributedString:[self _simpleTruncationAttributedString]
93-
avoidTailTruncationSet:[NSCharacterSet characterSetWithCharactersInString:@"."]
94-
constrainedSize:constrainedSize];
91+
avoidTailTruncationSet:[NSCharacterSet characterSetWithCharactersInString:@"."]];
9592
(void)tailTruncater;
9693
__block NSString *drawnString;
9794
[context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) {
@@ -114,8 +111,7 @@ - (void)testAvoidedCharTailCharBoundaryTruncation
114111
layoutManagerFactory:nil];
115112
ASTextKitTailTruncater *tailTruncater = [[ASTextKitTailTruncater alloc] initWithContext:context
116113
truncationAttributedString:[self _simpleTruncationAttributedString]
117-
avoidTailTruncationSet:[NSCharacterSet characterSetWithCharactersInString:@"."]
118-
constrainedSize:constrainedSize];
114+
avoidTailTruncationSet:[NSCharacterSet characterSetWithCharactersInString:@"."]];
119115
// So Xcode doesn't yell at me for an unused var...
120116
(void)tailTruncater;
121117
__block NSString *drawnString;
@@ -139,8 +135,7 @@ - (void)testHandleZeroHeightConstrainedSize
139135
layoutManagerFactory:nil];
140136
XCTAssertNoThrow([[ASTextKitTailTruncater alloc] initWithContext:context
141137
truncationAttributedString:[self _simpleTruncationAttributedString]
142-
avoidTailTruncationSet:[NSCharacterSet characterSetWithCharactersInString:@"."]
143-
constrainedSize:constrainedSize]);
138+
avoidTailTruncationSet:[NSCharacterSet characterSetWithCharactersInString:@"."]]);
144139
}
145140

146141
@end

0 commit comments

Comments
 (0)