diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index 81c94627dd..e92ebf8c55 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -45,85 +45,6 @@ static const CGFloat ASTextNodeHighlightLightOpacity = 0.11; static const CGFloat ASTextNodeHighlightDarkOpacity = 0.22; static NSString *ASTextNodeTruncationTokenAttributeName = @"ASTextNodeTruncationAttribute"; -#pragma mark - ASTextKitRenderer - -// Not used at the moment but handy to have -/*ASDISPLAYNODE_INLINE NSUInteger ASHashFromCGRect(CGRect rect) -{ - return ((*(NSUInteger *)&rect.origin.x << 10 ^ *(NSUInteger *)&rect.origin.y) + (*(NSUInteger *)&rect.size.width << 10 ^ *(NSUInteger *)&rect.size.height)); -}*/ - -ASDISPLAYNODE_INLINE NSUInteger ASHashFromCGSize(CGSize size) -{ - return ((*(NSUInteger *)&size.width << 10 ^ *(NSUInteger *)&size.height)); -} - -@interface ASTextNodeRendererKey : NSObject -@property (assign, nonatomic) ASTextKitAttributes attributes; -@property (assign, nonatomic) CGSize constrainedSize; -@end - -@implementation ASTextNodeRendererKey - -- (NSUInteger)hash -{ - return _attributes.hash() ^ ASHashFromCGSize(_constrainedSize); -} - -- (BOOL)isEqual:(ASTextNodeRendererKey *)object -{ - if (self == object) { - return YES; - } - - return _attributes == object.attributes - && CGSizeEqualToSize(_constrainedSize, object.constrainedSize); -} - -@end - -//static NSCache *sharedRendererCache() -//{ -// static dispatch_once_t onceToken; -// static NSCache *__rendererCache = nil; -// dispatch_once(&onceToken, ^{ -// __rendererCache = [[NSCache alloc] init]; -// __rendererCache.totalCostLimit = 500; // 500 renders cache -// }); -// return __rendererCache; -//} - -/** - The concept here is that neither the node nor layout should ever have a strong reference to the renderer object. - This is to reduce memory load when loading thousands and thousands of text nodes into memory at once. Instead - we maintain a LRU renderer cache that is queried via stack-allocated keys. - */ - -// Disabled renderer cache for now -//static ASTextKitRenderer *rendererForAttributes(ASTextKitAttributes attributes, CGSize constrainedSize) -//{ -// NSCache *cache = sharedRendererCache(); -// -// ASTextNodeRendererKey *key = [[ASTextNodeRendererKey alloc] init]; -// key.attributes = attributes; -// key.constrainedSize = constrainedSize; -// -// ASTextKitRenderer *renderer = [cache objectForKey:key]; -// if (renderer == nil) { -// renderer = [[ASTextKitRenderer alloc] initWithTextKitAttributes:attributes constrainedSize:constrainedSize]; -// [cache setObject:renderer forKey:key cost:1]; -// } -// -// return renderer; -//} - -static ASTextKitRenderer *rendererForAttributes(ASTextKitAttributes attributes, CGSize constrainedSize) -{ - return [[ASTextKitRenderer alloc] initWithTextKitAttributes:attributes constrainedSize:constrainedSize]; -} - -#pragma mark - ASTextNode - struct ASTextNodeDrawParameter { CGRect bounds; UIColor *backgroundColor; @@ -154,6 +75,8 @@ struct ASTextNodeDrawParameter { CGSize _constrainedSize; + ASTextKitRenderer *_renderer; + ASTextNodeDrawParameter _drawParameter; UILongPressGestureRecognizer *_longPressGestureRecognizer; @@ -220,6 +143,8 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; if (_shadowColor != NULL) { CGColorRelease(_shadowColor); } + + [self _invalidateRenderer]; if (_longPressGestureRecognizer) { _longPressGestureRecognizer.delegate = nil; @@ -261,6 +186,29 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; #pragma mark - ASDisplayNode +// FIXME: Re-evaluate if it is still the right decision to clear the renderer at this stage. +// This code was written before TextKit and when 512MB devices were still the overwhelming majority. +- (void)displayDidFinish +{ + [super displayDidFinish]; + + // We invalidate our renderer here to clear the very high memory cost of + // keeping this around. _invalidateRenderer will dealloc this onto a bg + // thread resulting in less stutters on the main thread than if it were + // to be deallocated in dealloc. This is also helpful in opportunistically + // reducing memory consumption and reducing the overall footprint of the app. + [self _invalidateRenderer]; +} + +- (void)clearContents +{ + // We discard the backing store and renderer to prevent the very large + // memory overhead of maintaining these for all text nodes. They can be + // regenerated when layout is necessary. + [super clearContents]; // ASDisplayNode will set layer.contents = nil + [self _invalidateRenderer]; +} + - (void)didLoad { [super didLoad]; @@ -275,6 +223,18 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; } } +- (void)setFrame:(CGRect)frame +{ + [super setFrame:frame]; + [self _invalidateRendererIfNeededForBoundsSize:frame.size]; +} + +- (void)setBounds:(CGRect)bounds +{ + [super setBounds:bounds]; + [self _invalidateRendererIfNeededForBoundsSize:bounds.size]; +} + #pragma mark - Renderer Management - (ASTextKitRenderer *)_renderer @@ -286,16 +246,20 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; { ASDN::MutexLocker l(__instanceLock__); - CGSize constrainedSize; - if (_constrainedSize.width != -INFINITY) { - constrainedSize = _constrainedSize; - } else { - constrainedSize = bounds.size; - constrainedSize.width -= (_textContainerInset.left + _textContainerInset.right); - constrainedSize.height -= (_textContainerInset.top + _textContainerInset.bottom); + if (_renderer == nil) { + CGSize constrainedSize; + if (_constrainedSize.width != -INFINITY) { + constrainedSize = _constrainedSize; + } else { + constrainedSize = bounds.size; + constrainedSize.width -= (_textContainerInset.left + _textContainerInset.right); + constrainedSize.height -= (_textContainerInset.top + _textContainerInset.bottom); + } + + _renderer = [[ASTextKitRenderer alloc] initWithTextKitAttributes:[self _rendererAttributes] + constrainedSize:constrainedSize]; } - - return rendererForAttributes([self _rendererAttributes], constrainedSize); + return _renderer; } - (ASTextKitAttributes)_rendererAttributes @@ -308,15 +272,51 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; .lineBreakMode = _truncationMode, .maximumNumberOfLines = _maximumNumberOfLines, .exclusionPaths = _exclusionPaths, + // use the property getter so a subclass can provide these scale factors on demand if desired + .pointSizeScaleFactors = self.pointSizeScaleFactors, .shadowOffset = _shadowOffset, .shadowColor = _cachedShadowUIColor, .shadowOpacity = _shadowOpacity, - .shadowRadius = _shadowRadius, - // use the property getter so a subclass can provide these scale factors on demand if desired - .pointSizeScaleFactors = self.pointSizeScaleFactors + .shadowRadius = _shadowRadius }; } +- (void)_invalidateRendererIfNeeded +{ + [self _invalidateRendererIfNeededForBoundsSize:self.threadSafeBounds.size]; +} + +- (void)_invalidateRendererIfNeededForBoundsSize:(CGSize)boundsSize +{ + if ([self _needInvalidateRendererForBoundsSize:boundsSize]) { + // Our bounds have changed to a size that is not identical to our constraining size, + // so our previous layout information is invalid, and TextKit may draw at the + // incorrect origin. + { + ASDN::MutexLocker l(__instanceLock__); + _constrainedSize = CGSizeMake(-INFINITY, -INFINITY); + } + [self _invalidateRenderer]; + } +} + +- (void)_invalidateRenderer +{ + ASDN::MutexLocker l(__instanceLock__); + + if (_renderer) { + // Destruction of the layout managers/containers/text storage is quite + // expensive, and can take some time, so we dispatch onto a bg queue to + // actually dealloc. + __block ASTextKitRenderer *renderer = _renderer; + + ASPerformBlockOnDeallocationQueue(^{ + renderer = nil; + }); + _renderer = nil; + } +} + #pragma mark - Layout and Sizing - (void)setTextContainerInset:(UIEdgeInsets)textContainerInset @@ -337,6 +337,44 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; return _textContainerInset; } +- (BOOL)_needInvalidateRendererForBoundsSize:(CGSize)boundsSize +{ + ASDN::MutexLocker l(__instanceLock__); + + if (_renderer == nil) { + return YES; + } + + // If the size is not the same as the constraint we provided to the renderer, start out assuming we need + // a new one. However, there are common cases where the constrained size doesn't need to be the same as calculated. + CGSize rendererConstrainedSize = _renderer.constrainedSize; + + //inset bounds + boundsSize.width -= _textContainerInset.left + _textContainerInset.right; + boundsSize.height -= _textContainerInset.top + _textContainerInset.bottom; + + if (CGSizeEqualToSize(boundsSize, rendererConstrainedSize)) { + return NO; + } else { + // It is very common to have a constrainedSize with a concrete, specific width but +Inf height. + // In this case, as long as the text node has bounds as large as the full calculatedLayout suggests, + // it means that the text has all the room it needs (as it was not vertically bounded). So, we will not + // experience truncation and don't need to recreate the renderer with the size it already calculated, + // as this would essentially serve to set its constrainedSize to be its calculatedSize (unnecessary). + ASLayout *layout = self.calculatedLayout; + if (layout != nil && CGSizeEqualToSize(boundsSize, layout.size)) { + if (boundsSize.width != rendererConstrainedSize.width) { + // Don't bother changing _constrainedSize, as ASDisplayNode's -measure: method would have a cache miss + // and ask us to recalculate layout if it were called with the same calculatedSize that got us to this point! + _renderer.constrainedSize = boundsSize; + } + return NO; + } else { + return YES; + } + } +} + - (void)calculatedLayoutDidChange { [super calculatedLayoutDidChange]; @@ -353,6 +391,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; if (CGSizeEqualToSize(_constrainedSize, layoutSize) == NO) { _constrainedSize = layoutSize; + _renderer.constrainedSize = layoutSize; } } } @@ -373,20 +412,24 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; _constrainedSize = constrainedSize; - ASTextKitRenderer *renderer = [self _renderer]; + // Instead of invalidating the renderer, in case this is a new call with a different constrained size, + // just update the size of the NSTextContainer that is owned by the renderer's internal context object. + [self _renderer].constrainedSize = _constrainedSize; + + [self setNeedsDisplay]; - CGSize size = renderer.size; + CGSize size = [self _renderer].size; if (_attributedText.length > 0) { self.style.ascender = [[self class] ascenderWithAttributedString:_attributedText]; self.style.descender = [[_attributedText attribute:NSFontAttributeName atIndex:_attributedText.length - 1 effectiveRange:NULL] descender]; - if (renderer.currentScaleFactor > 0 && renderer.currentScaleFactor < 1.0) { + if (_renderer.currentScaleFactor > 0 && _renderer.currentScaleFactor < 1.0) { // while not perfect, this is a good estimate of what the ascender of the scaled font will be. - self.style.ascender *= renderer.currentScaleFactor; - self.style.descender *= renderer.currentScaleFactor; + self.style.ascender *= _renderer.currentScaleFactor; + self.style.descender *= _renderer.currentScaleFactor; } } - // Add textContainerInset to size + // Add the constrained size back textContainerInset size.width += (_textContainerInset.left + _textContainerInset.right); size.height += (_textContainerInset.top + _textContainerInset.bottom); @@ -413,6 +456,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; - (void)setAttributedText:(NSAttributedString *)attributedText { + if (attributedText == nil) { attributedText = [[NSAttributedString alloc] initWithString:@"" attributes:nil]; } @@ -432,6 +476,9 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; // Without this, the size calculation of the text with truncation applied will // not take into account the attributes of attributedText in the last line [self _updateComposedTruncationText]; + + // We need an entirely new renderer + [self _invalidateRenderer]; } NSUInteger length = attributedText.length; @@ -446,6 +493,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; // Force display to create renderer with new size and redisplay with new string [self setNeedsDisplay]; + // Accessiblity self.accessibilityLabel = attributedText.string; self.isAccessibilityElement = (length != 0); // We're an accessibility element by default if there is a string. @@ -462,6 +510,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; } _exclusionPaths = [exclusionPaths copy]; + [self _invalidateRenderer]; [self invalidateCalculatedLayout]; [self setNeedsDisplay]; } @@ -764,11 +813,10 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; if (highlightTargetLayer != nil) { ASDN::MutexLocker l(__instanceLock__); - ASTextKitRenderer *renderer = [self _renderer]; - NSArray *highlightRects = [renderer rectsForTextRange:highlightRange measureOption:ASTextKitRendererMeasureOptionBlock]; + NSArray *highlightRects = [[self _renderer] rectsForTextRange:highlightRange measureOption:ASTextKitRendererMeasureOptionBlock]; NSMutableArray *converted = [NSMutableArray arrayWithCapacity:highlightRects.count]; for (NSValue *rectValue in highlightRects) { - UIEdgeInsets shadowPadding = renderer.shadower.shadowPadding; + UIEdgeInsets shadowPadding = _renderer.shadower.shadowPadding; CGRect rendererRect = ASTextNodeAdjustRenderRectForShadowPadding(rectValue.CGRectValue, shadowPadding); // The rects returned from renderer don't have `textContainerInset`, @@ -1098,6 +1146,7 @@ static CGRect ASTextNodeAdjustRenderRectForShadowPadding(CGRect rendererRect, UI } _shadowColor = shadowColor; _cachedShadowUIColor = [UIColor colorWithCGColor:shadowColor]; + [self _invalidateRenderer]; [self setNeedsDisplay]; } } @@ -1115,6 +1164,7 @@ static CGRect ASTextNodeAdjustRenderRectForShadowPadding(CGRect rendererRect, UI if (!CGSizeEqualToSize(_shadowOffset, shadowOffset)) { _shadowOffset = shadowOffset; + [self _invalidateRenderer]; [self setNeedsDisplay]; } } @@ -1132,6 +1182,7 @@ static CGRect ASTextNodeAdjustRenderRectForShadowPadding(CGRect rendererRect, UI if (_shadowOpacity != shadowOpacity) { _shadowOpacity = shadowOpacity; + [self _invalidateRenderer]; [self setNeedsDisplay]; } } @@ -1149,6 +1200,7 @@ static CGRect ASTextNodeAdjustRenderRectForShadowPadding(CGRect rendererRect, UI if (_shadowRadius != shadowRadius) { _shadowRadius = shadowRadius; + [self _invalidateRenderer]; [self setNeedsDisplay]; } } @@ -1207,6 +1259,7 @@ static NSAttributedString *DefaultTruncationAttributedString() if (_truncationMode != truncationMode) { _truncationMode = truncationMode; + [self _invalidateRenderer]; [self setNeedsDisplay]; } } @@ -1225,6 +1278,7 @@ static NSAttributedString *DefaultTruncationAttributedString() if ([_pointSizeScaleFactors isEqualToArray:pointSizeScaleFactors] == NO) { _pointSizeScaleFactors = pointSizeScaleFactors; + [self _invalidateRenderer]; [self setNeedsDisplay]; }} @@ -1234,6 +1288,7 @@ static NSAttributedString *DefaultTruncationAttributedString() if (_maximumNumberOfLines != maximumNumberOfLines) { _maximumNumberOfLines = maximumNumberOfLines; + [self _invalidateRenderer]; [self setNeedsDisplay]; } } @@ -1257,6 +1312,7 @@ static NSAttributedString *DefaultTruncationAttributedString() - (void)_invalidateTruncationText { [self _updateComposedTruncationText]; + [self _invalidateRenderer]; [self setNeedsDisplay]; } diff --git a/AsyncDisplayKit/TextKit/ASTextKitAttributes.h b/AsyncDisplayKit/TextKit/ASTextKitAttributes.h index e524a5b6bd..dae549e514 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitAttributes.h +++ b/AsyncDisplayKit/TextKit/ASTextKitAttributes.h @@ -110,8 +110,7 @@ struct ASTextKitAttributes { && maximumNumberOfLines == other.maximumNumberOfLines && shadowOpacity == other.shadowOpacity && shadowRadius == other.shadowRadius - && (pointSizeScaleFactors == other.pointSizeScaleFactors - || [pointSizeScaleFactors isEqualToArray:other.pointSizeScaleFactors]) + && [pointSizeScaleFactors isEqualToArray:other.pointSizeScaleFactors] && CGSizeEqualToSize(shadowOffset, other.shadowOffset) && ASObjectIsEqual(exclusionPaths, other.exclusionPaths) && ASObjectIsEqual(avoidTailTruncationSet, other.avoidTailTruncationSet) diff --git a/AsyncDisplayKit/TextKit/ASTextKitContext.h b/AsyncDisplayKit/TextKit/ASTextKitContext.h index 9e90fac710..58257efbab 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitContext.h +++ b/AsyncDisplayKit/TextKit/ASTextKitContext.h @@ -29,6 +29,8 @@ exclusionPaths:(NSArray *)exclusionPaths constrainedSize:(CGSize)constrainedSize; +@property (nonatomic, assign, readwrite) CGSize constrainedSize; + /** All operations on TextKit values MUST occur within this locked context. Simultaneous access (even non-mutative) to TextKit components may cause crashes. diff --git a/AsyncDisplayKit/TextKit/ASTextKitContext.mm b/AsyncDisplayKit/TextKit/ASTextKitContext.mm index 7eca01d531..ba7477c500 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitContext.mm +++ b/AsyncDisplayKit/TextKit/ASTextKitContext.mm @@ -54,6 +54,18 @@ return self; } +- (CGSize)constrainedSize +{ + ASDN::MutexSharedLocker l(__instanceLock__); + return _textContainer.size; +} + +- (void)setConstrainedSize:(CGSize)constrainedSize +{ + ASDN::MutexSharedLocker l(__instanceLock__); + _textContainer.size = constrainedSize; +} + - (void)performBlockWithLockedTextKitComponents:(void (^)(NSLayoutManager *, NSTextStorage *, NSTextContainer *))block diff --git a/AsyncDisplayKit/TextKit/ASTextKitRenderer.h b/AsyncDisplayKit/TextKit/ASTextKitRenderer.h index d34ce0e1c5..17c9439e0a 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitRenderer.h +++ b/AsyncDisplayKit/TextKit/ASTextKitRenderer.h @@ -53,7 +53,7 @@ @property (nonatomic, assign, readonly) ASTextKitAttributes attributes; -@property (nonatomic, assign, readonly) CGSize constrainedSize; +@property (nonatomic, assign, readwrite) CGSize constrainedSize; @property (nonatomic, assign, readonly) CGFloat currentScaleFactor; diff --git a/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm b/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm index 8ee899c211..9faec1786f 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm +++ b/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm @@ -116,6 +116,31 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() return _calculatedSize; } +- (void)setConstrainedSize:(CGSize)constrainedSize +{ + if (!CGSizeEqualToSize(constrainedSize, _constrainedSize)) { + _sizeIsCalculated = NO; + _constrainedSize = constrainedSize; + _calculatedSize = CGSizeZero; + + // Throw away the all subcomponents to create them with the new constrained size new as well as let the + // truncater do it's job again for the new constrained size. This is necessary as after a truncation did happen + // the context would use the truncated string and not the original string to truncate based on the new + // constrained size + __block ASTextKitContext *ctx = _context; + __block ASTextKitTailTruncater *tru = _truncater; + __block ASTextKitFontSizeAdjuster *adj = _fontSizeAdjuster; + _context = nil; + _truncater = nil; + _fontSizeAdjuster = nil; + ASPerformBlockOnDeallocationQueue(^{ + ctx = nil; + tru = nil; + adj = nil; + }); + } +} + - (void)_calculateSize { // if we have no scale factors or an unconstrained width, there is no reason to try to adjust the font size