From b7afb9b8f5c81232bcd1cd0095c3ebbe49f2263f Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Sun, 14 May 2017 18:59:50 -0700 Subject: [PATCH] Go further --- AsyncDisplayKit.xcodeproj/project.pbxproj | 8 + Source/Private/ASTextNode2.h | 2 +- Source/Private/ASTextNode2.mm | 231 +++++------------- Source/Private/TextExperiment/ASTextCaching.h | 28 +++ Source/Private/TextExperiment/ASTextCaching.m | 106 ++++++++ 5 files changed, 203 insertions(+), 172 deletions(-) create mode 100644 Source/Private/TextExperiment/ASTextCaching.h create mode 100644 Source/Private/TextExperiment/ASTextCaching.m diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 3257c2e3..7c903a2c 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -339,6 +339,8 @@ CC57EAF71E3939350034C595 /* ASCollectionView+Undeprecated.h in Headers */ = {isa = PBXBuildFile; fileRef = CC2E317F1DAC353700EEE891 /* ASCollectionView+Undeprecated.h */; settings = {ATTRIBUTES = (Private, ); }; }; CC57EAF81E3939450034C595 /* ASTableView+Undeprecated.h in Headers */ = {isa = PBXBuildFile; fileRef = CC512B841DAC45C60054848E /* ASTableView+Undeprecated.h */; settings = {ATTRIBUTES = (Private, ); }; }; CC58AA4B1E398E1D002C8CB4 /* ASBlockTypes.h in Headers */ = {isa = PBXBuildFile; fileRef = CC58AA4A1E398E1D002C8CB4 /* ASBlockTypes.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CC5BE2891EC9174B0077A1A8 /* ASTextCaching.h in Headers */ = {isa = PBXBuildFile; fileRef = CC5BE2871EC9174B0077A1A8 /* ASTextCaching.h */; }; + CC5BE28A1EC9174B0077A1A8 /* ASTextCaching.m in Sources */ = {isa = PBXBuildFile; fileRef = CC5BE2881EC9174B0077A1A8 /* ASTextCaching.m */; }; CC6AA2DA1E9F03B900978E87 /* ASDisplayNode+Ancestry.h in Headers */ = {isa = PBXBuildFile; fileRef = CC6AA2D81E9F03B900978E87 /* ASDisplayNode+Ancestry.h */; settings = {ATTRIBUTES = (Public, ); }; }; CC6AA2DB1E9F03B900978E87 /* ASDisplayNode+Ancestry.m in Sources */ = {isa = PBXBuildFile; fileRef = CC6AA2D91E9F03B900978E87 /* ASDisplayNode+Ancestry.m */; }; CC7FD9E11BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC7FD9E01BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.m */; }; @@ -790,6 +792,8 @@ CC55A7101E52A0F200594372 /* ASResponderChainEnumerator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASResponderChainEnumerator.m; sourceTree = ""; }; CC57EAF91E394EA40034C595 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; CC58AA4A1E398E1D002C8CB4 /* ASBlockTypes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASBlockTypes.h; sourceTree = ""; }; + CC5BE2871EC9174B0077A1A8 /* ASTextCaching.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTextCaching.h; sourceTree = ""; }; + CC5BE2881EC9174B0077A1A8 /* ASTextCaching.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTextCaching.m; sourceTree = ""; }; CC6AA2D81E9F03B900978E87 /* ASDisplayNode+Ancestry.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "ASDisplayNode+Ancestry.h"; path = "Base/ASDisplayNode+Ancestry.h"; sourceTree = ""; }; CC6AA2D91E9F03B900978E87 /* ASDisplayNode+Ancestry.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "ASDisplayNode+Ancestry.m"; path = "Base/ASDisplayNode+Ancestry.m"; sourceTree = ""; }; CC7FD9DC1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPhotosFrameworkImageRequest.h; sourceTree = ""; }; @@ -1487,6 +1491,8 @@ CCCCCCC11EC3EF060087FE10 /* TextExperiment */ = { isa = PBXGroup; children = ( + CC5BE2871EC9174B0077A1A8 /* ASTextCaching.h */, + CC5BE2881EC9174B0077A1A8 /* ASTextCaching.m */, CCCCCCC21EC3EF060087FE10 /* Component */, CCCCCCCB1EC3EF060087FE10 /* String */, CCCCCCD01EC3EF060087FE10 /* Utility */, @@ -1642,6 +1648,7 @@ B35062581B010F070018CF92 /* ASAvailability.h in Headers */, DE84918D1C8FFF2B003D89E9 /* ASRunLoopQueue.h in Headers */, CC0F88621E4281E200576FED /* ASSectionController.h in Headers */, + CC5BE2891EC9174B0077A1A8 /* ASTextCaching.h in Headers */, A2763D7A1CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.h in Headers */, 34EFC7611B701C9C00AD841F /* ASBackgroundLayoutSpec.h in Headers */, B35062591B010F070018CF92 /* ASBaseDefines.h in Headers */, @@ -2099,6 +2106,7 @@ DBDB83971C6E879900D0098C /* ASPagerFlowLayout.m in Sources */, E5B078001E69F4EB00C24B5B /* ASElementMap.m in Sources */, 9C8898BC1C738BA800D6B02E /* ASTextKitFontSizeAdjuster.mm in Sources */, + CC5BE28A1EC9174B0077A1A8 /* ASTextCaching.m in Sources */, 690ED59B1E36D118000627C0 /* ASImageNode+tvOS.m in Sources */, CCCCCCD81EC3EF060087FE10 /* ASTextInput.m in Sources */, 34EFC7621B701CA400AD841F /* ASBackgroundLayoutSpec.mm in Sources */, diff --git a/Source/Private/ASTextNode2.h b/Source/Private/ASTextNode2.h index 81cb0837..50e42501 100644 --- a/Source/Private/ASTextNode2.h +++ b/Source/Private/ASTextNode2.h @@ -177,7 +177,7 @@ NS_ASSUME_NONNULL_BEGIN /** @abstract The style to use when highlighting text. */ -@property (nonatomic, assign) ASTextNodeHighlightStyle highlightStyle; +@property (atomic, assign) ASTextNodeHighlightStyle highlightStyle; /** @abstract The range of text highlighted by the receiver. Changes to this property are not animated by default. diff --git a/Source/Private/ASTextNode2.mm b/Source/Private/ASTextNode2.mm index a3dddfdb..9a0414f7 100644 --- a/Source/Private/ASTextNode2.mm +++ b/Source/Private/ASTextNode2.mm @@ -29,6 +29,7 @@ #import #import +#import #import // TODO: This is more like ASTextNode2State @@ -48,60 +49,36 @@ @implementation ASTextNode2DrawParameters -@end - -@interface ASTextNode2CacheKey : NSObject -- (instancetype)initForResultWithLayout:(ASTextLayout *)layout; -- (instancetype)initForQueryWithParameters:(ASTextNode2DrawParameters *)parameters; -@property (nonatomic, strong, readonly) NSAttributedString *attributedString; -@property (nonatomic, strong, readonly) ASTextContainer *textContainer; -@property (nonatomic, readonly) ASSizeRange resultSizeRange; -@property (nonatomic, readonly) CGSize querySize; -@end -@implementation ASTextNode2CacheKey - -- (instancetype)initForResultWithLayout:(ASTextLayout *)layout +- (NSAttributedString *)processedAttributedText { - if (self = [super init]) { - _textContainer = layout.container; - _attributedString = layout.text; - _resultSizeRange = ASSizeRangeMakeLoosely(_textContainer.size, layout.textBoundingSize); - } - return self; -} - -- (instancetype)initForQueryWithParameters:(ASTextNode2DrawParameters *)parameters -{ - if (self = [super init]) { - _textContainer = parameters.textContainer; - _attributedString = parameters.processedAttributedText; - _querySize = _textContainer.size; - } - return self; -} - -- (NSUInteger)hash -{ - // Just use attributed string for hash. Previously we included container attributes - // in the hash but it actually deteriorated cache performance (more collisions). - return _attributedString.hash; -} - -- (BOOL)isEqual:(id)object -{ - ASDisplayNodeAssert([object isKindOfClass:[ASTextNode2CacheKey class]], @"Unexpected object: %@", object); - ASTextNode2CacheKey *otherKey = object; - if (![_attributedString isEqualToAttributedString:otherKey->_attributedString] || ![_textContainer isEqualToContainerExcludingSize:otherKey->_textContainer]) { - return NO; - } + @synchronized (self) { + if (_processedAttributedText) { + return _processedAttributedText; + } - ASDisplayNodeAssert(CGSizeEqualToSize(_querySize, CGSizeZero), @"Query-key should not be on the left-hand side of isEqual:."); - - BOOL otherIsResult = CGSizeEqualToSize(otherKey->_querySize, CGSizeZero); - if (otherIsResult) { - return ASSizeRangeEqualToSizeRange(_resultSizeRange, otherKey->_resultSizeRange); - } else { - return ASSizeRangeContainsSize(_resultSizeRange, otherKey->_querySize); + NSMutableAttributedString *attributedString = [_attributedText mutableCopy]; + // Apply paragraph style if needed + [attributedString enumerateAttribute:NSParagraphStyleAttributeName inRange:NSMakeRange(0, attributedString.length) options:kNilOptions usingBlock:^(NSParagraphStyle *style, NSRange range, BOOL * _Nonnull stop) { + if (style == nil || style.lineBreakMode == _lineBreakMode) { + return; + } + + NSMutableParagraphStyle *paragraphStyle = [style mutableCopy] ?: [[NSMutableParagraphStyle alloc] init]; + paragraphStyle.lineBreakMode = _lineBreakMode; + [attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range]; + }]; + + // Apply background color if needed + UIColor *backgroundColor = self.backgroundColor; + if (CGColorGetAlpha(backgroundColor.CGColor) > 0) { + [attributedString addAttribute:NSBackgroundColorAttributeName value:backgroundColor range:NSMakeRange(0, attributedString.length)]; + } + + // Apply shadow if needed + if (_shadow != nil) { + [attributedString addAttribute:NSShadowAttributeName value:_shadow range:NSMakeRange(0, attributedString.length)]; + } + _processedAttributedText = [attributedString copy]; } } @@ -128,7 +105,7 @@ static const CGFloat ASTextNodeHighlightDarkOpacity = 0.22; static NSString *ASTextNodeTruncationTokenAttributeName = @"ASTextNodeTruncationAttribute"; @interface ASTextNode2 () -@property (atomic, strong) ASTextNode2DrawParameters *displayedParameters; +@property (atomic, strong) ASTextLayout *displayedTextLayout; @end @implementation ASTextNode2 { @@ -145,6 +122,7 @@ static NSString *ASTextNodeTruncationTokenAttributeName = @"ASTextNodeTruncation NSAttributedString *_attributedText; NSAttributedString *_composedTruncationText; NSArray *_pointSizeScaleFactors; + std::atomic _additionalTruncationMessage; NSString *_highlightedLinkAttributeName; id _highlightedLinkAttributeValue; @@ -274,23 +252,31 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; return YES; } +#define ASDeclareGetter(source, type, getterName) \ +- (type)getterName \ +{\ + return source;\ +} + +#define ASDeclareSetter(expr, type, setterName, equal, sideEffect)\ +- (void)setterName(type)value \ +{ \ + BOOL sameValue = equal(expr, value); \ + if (sameValue) { \ + return; \ + }\ + expr = value; \ + sideEffect;\ +} \ + +#define ASDeclareReadwrite(expr, type, setterName, getterName, equal, sideEffect) \ + ASDeclareGetter(expr, type, getterName) \ + ASDeclareSetter(expr, type, setterName, equal, sideEffect) + +ASDeclareReadwrite(_textContainer.insets, UIEdgeInsets, setTextContainerInset:, textContainerInset, UIEdgeInsetsEqualToEdgeInsets, [self setNeedsLayout]); + #pragma mark - Layout and Sizing -- (void)setTextContainerInset:(UIEdgeInsets)textContainerInset -{ - BOOL needsUpdate = !UIEdgeInsetsEqualToEdgeInsets(_textContainer.insets, textContainerInset); - _textContainer.insets = textContainerInset; - - if (needsUpdate) { - [self setNeedsLayout]; - } -} - -- (UIEdgeInsets)textContainerInset -{ - return _textContainer.insets; -} - - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize { ASDisplayNodeAssert(constrainedSize.width >= 0, @"Constrained width for text (%f) is too narrow", constrainedSize.width); @@ -298,7 +284,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; ASTextNode2DrawParameters *params = [self textNodeDrawParameters]; params.textContainer.size = constrainedSize; - ASTextLayout *layout = [ASTextNode2 compatibleLayoutWithParameters:params]; + ASTextLayout *layout = [ASTextLayoutCache layoutForText:params.processedAttributedText container:params.textContainer]; return layout.textBoundingSize; } @@ -382,41 +368,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; return _textContainer.exclusionPaths; } -- (void)prepareAttributedStringForDrawing:(NSMutableAttributedString *)attributedString -{ - ASDN::MutexLocker lock(__instanceLock__); - - // Apply paragraph style if needed - [attributedString enumerateAttribute:NSParagraphStyleAttributeName inRange:NSMakeRange(0, attributedString.length) options:kNilOptions usingBlock:^(NSParagraphStyle *style, NSRange range, BOOL * _Nonnull stop) { - if (style == nil || style.lineBreakMode == _truncationMode) { - return; - } - - NSMutableParagraphStyle *paragraphStyle = [style mutableCopy] ?: [[NSMutableParagraphStyle alloc] init]; - paragraphStyle.lineBreakMode = _truncationMode; - [attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range]; - }]; - - // Apply background color if needed - UIColor *backgroundColor = self.backgroundColor; - if (CGColorGetAlpha(backgroundColor.CGColor) > 0) { - [attributedString addAttribute:NSBackgroundColorAttributeName value:backgroundColor range:NSMakeRange(0, attributedString.length)]; - } - - // Apply shadow if needed - if (_shadowOpacity > 0 && (_shadowRadius != 0 || !CGSizeEqualToSize(_shadowOffset, CGSizeZero)) && CGColorGetAlpha(_shadowColor) > 0) { - NSShadow *shadow = [[NSShadow alloc] init]; - if (_shadowOpacity != 1) { - shadow.shadowColor = [UIColor colorWithCGColor:CGColorCreateCopyWithAlpha(_shadowColor, _shadowOpacity * CGColorGetAlpha(_shadowColor))]; - } else { - shadow.shadowColor = [UIColor colorWithCGColor:_shadowColor]; - } - shadow.shadowOffset = _shadowOffset; - shadow.shadowBlurRadius = _shadowRadius; - [attributedString addAttribute:NSShadowAttributeName value:shadow range:NSMakeRange(0, attributedString.length)]; - } -} - #pragma mark - Drawing // NOTE: This should be called something like `-drawParameters` but that's actually a (dumb) private ASDisplayNode method. @@ -447,7 +398,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; { [super displayDidFinish]; ASDisplayNodeAssertNotNil(_parametersForCurrentDisplay, @"Expected to have parameters for current display when display finishes."); - _displayedParameters = _parametersForCurrentDisplay; + _displayedTextLayout = [ASTextLayoutCache layoutForText:_parametersForCurrentDisplay.processedAttributedText container:_parametersForCurrentDisplay.textContainer]; _parametersForCurrentDisplay = nil; } @@ -468,50 +419,10 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; return shadow; } -#define ENABLE_ASTEXT_CACHE_LOG 0 -/** - * If it can't find a compatible layout, this method creates one. - */ -+ (ASTextLayout *)compatibleLayoutWithParameters:(ASTextNode2DrawParameters *)drawParameters -{ - static NSCache *textLayoutCache2; -#if ENABLE_ASTEXT_CACHE_LOG - static std::atomic cacheHits; - static std::atomic cacheQueries; -#endif - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - textLayoutCache2 = [[NSCache alloc] init]; - }); - -#if ENABLE_ASTEXT_CACHE_LOG - if (cacheQueries.load() % 5 == 0) { - NSLog(@"%g", cacheHits.load() / (float)cacheQueries.load()); - } - cacheQueries += 1; -#endif - auto cacheKey = [[ASTextNode2CacheKey alloc] initForQueryWithParameters:drawParameters]; - ASTextLayout *cachedLayout = [textLayoutCache2 objectForKey:cacheKey]; - if (cachedLayout != nil) { -#if ENABLE_ASTEXT_CACHE_LOG - cacheHits += 1; -#endif - return cachedLayout; - } - - // Cache Miss. - - // Compute the text layout. - ASTextLayout *layout = [ASTextLayout layoutWithContainer:drawParameters.textContainer text:drawParameters.processedAttributedText]; - ASTextNode2CacheKey *key = [[ASTextNode2CacheKey alloc] initForResultWithLayout:layout]; - [textLayoutCache2 setObject:layout forKey:key]; - return layout; -} - + (void)drawRect:(CGRect)bounds withParameters:(ASTextNode2DrawParameters *)drawParams isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing; { drawParams.textContainer.size = bounds.size; - ASTextLayout *layout = [self compatibleLayoutWithParameters:drawParams]; + ASTextLayout *layout = [ASTextLayoutCache layoutForText:drawParams.processedAttributedText container:drawParams.textContainer]; if (isCancelledBlock()) { return; @@ -541,7 +452,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; inAdditionalTruncationMessage:(out BOOL *)inAdditionalTruncationMessageOut forHighlighting:(BOOL)highlighting { - AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); + auto range = [self.displayedTextLayout textRangeAtPoint:point]; return nil; } @@ -580,20 +491,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; #pragma mark - Highlighting -- (ASTextNodeHighlightStyle)highlightStyle -{ - ASDN::MutexLocker l(__instanceLock__); - - return _highlightStyle; -} - -- (void)setHighlightStyle:(ASTextNodeHighlightStyle)highlightStyle -{ - ASDN::MutexLocker l(__instanceLock__); - - _highlightStyle = highlightStyle; -} - - (NSRange)highlightRange { ASDisplayNodeAssertMainThread(); @@ -928,14 +825,6 @@ static NSAttributedString *DefaultTruncationAttributedString() return defaultTruncationAttributedString; } -- (void)_ensureTruncationText -{ - if (_textContainer.truncationToken == nil) { - ASDN::MutexLocker l(__instanceLock__); - _textContainer.truncationToken = [self _locked_composedTruncationText]; - } -} - - (void)setTruncationAttributedText:(NSAttributedString *)truncationAttributedText { { @@ -956,7 +845,7 @@ static NSAttributedString *DefaultTruncationAttributedString() { ASDN::MutexLocker l(__instanceLock__); - if (ASObjectIsEqual(_additionalTruncationMessage, additionalTruncationMessage)) { + if (ASObjectIsEqual(_additionalTruncationMessage.load(), additionalTruncationMessage)) { return; } @@ -996,7 +885,7 @@ static NSAttributedString *DefaultTruncationAttributedString() - (BOOL)isTruncated { - return ([ASTextNode2 compatibleLayoutWithParameters:self.displayedParameters].truncatedLine != nil); + return (_displayedTextLayout.truncatedLine != nil); } - (NSUInteger)maximumNumberOfLines diff --git a/Source/Private/TextExperiment/ASTextCaching.h b/Source/Private/TextExperiment/ASTextCaching.h new file mode 100644 index 00000000..6ee528a7 --- /dev/null +++ b/Source/Private/TextExperiment/ASTextCaching.h @@ -0,0 +1,28 @@ +// +// ASTextCaching.h +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +@class ASTextContainer, ASTextLayout; + +NS_ASSUME_NONNULL_BEGIN + +AS_SUBCLASSING_RESTRICTED +@interface ASTextLayoutCache : NSObject + ++ (ASTextLayout *)layoutForText:(NSAttributedString *)attributedText + container:(ASTextContainer *)container; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Private/TextExperiment/ASTextCaching.m b/Source/Private/TextExperiment/ASTextCaching.m new file mode 100644 index 00000000..a8b78d93 --- /dev/null +++ b/Source/Private/TextExperiment/ASTextCaching.m @@ -0,0 +1,106 @@ +// +// ASTextCaching.m +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASTextCaching.h" +#import +#import +#import + +@interface ASTextLayoutKey : NSObject +- (instancetype)initForResultWithLayout:(ASTextLayout *)layout; + +- (instancetype)initForQueryWithText:(NSAttributedString *)attributedString + container:(ASTextContainer *)container; +@property (nonatomic, strong, readonly) NSAttributedString *attributedString; +@property (nonatomic, strong, readonly) ASTextContainer *textContainer; +@property (nonatomic, readonly) ASSizeRange resultSizeRange; +@property (nonatomic, readonly) CGSize querySize; +@end + +@implementation ASTextLayoutKey + +- (instancetype)initForResultWithLayout:(ASTextLayout *)layout +{ + if (self = [super init]) { + _textContainer = layout.container; + _attributedString = layout.text; + _resultSizeRange = ASSizeRangeMakeLoosely(_textContainer.size, layout.textBoundingSize); + } + return self; +} + +- (instancetype)initForQueryWithText:(NSAttributedString *)attributedString container:(ASTextContainer *)container +{ + if (self = [super init]) { + _textContainer = container; + _attributedString = attributedString; + _querySize = container.size; + } + return self; +} + +- (NSUInteger)hash +{ + // Just use attributed string for hash. Previously we included container attributes + // in the hash but it actually deteriorated cache performance (more collisions). + return _attributedString.hash; +} + +- (BOOL)isEqual:(id)object +{ + ASDisplayNodeAssert([object isKindOfClass:[ASTextLayoutKey class]], @"Unexpected object: %@", object); + ASTextLayoutKey *otherKey = object; + if (![_attributedString isEqualToAttributedString:otherKey->_attributedString] || ![_textContainer isEqualToContainerExcludingSize:otherKey->_textContainer]) { + return NO; + } + + ASDisplayNodeAssert(CGSizeEqualToSize(_querySize, CGSizeZero), @"Query-key should not be on the left-hand side of isEqual:."); + + BOOL otherIsResult = CGSizeEqualToSize(otherKey->_querySize, CGSizeZero); + if (otherIsResult) { + return ASSizeRangeEqualToSizeRange(_resultSizeRange, otherKey->_resultSizeRange); + } else { + return ASSizeRangeContainsSize(_resultSizeRange, otherKey->_querySize); + } +} + +@end + +@implementation ASTextLayoutCache + +#define ENABLE_ASTEXT_CACHE_LOG 0 + ++ (ASTextLayout *)layoutForText:(NSAttributedString *)attributedText + container:(ASTextContainer *)container +{ + static NSCache *textLayoutCache; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + textLayoutCache = [[NSCache alloc] init]; + }); + + ASTextLayoutKey *cacheKey = [[ASTextLayoutKey alloc] initForQueryWithText:attributedText container:container]; + ASTextLayout *cachedLayout = [textLayoutCache objectForKey:cacheKey]; + if (cachedLayout != nil) { + return cachedLayout; + } + + // Cache Miss. + + // Compute the text layout. + ASTextLayout *layout = [ASTextLayout layoutWithContainer:container text:attributedText]; + ASTextLayoutKey *key = [[ASTextLayoutKey alloc] initForResultWithLayout:layout]; + [textLayoutCache setObject:layout forKey:key]; + return layout; +} + +@end