From d332a8c753cea1e7d6009cda7bf54bf0885693e2 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Sun, 11 Mar 2018 19:26:00 -0700 Subject: [PATCH 1/2] Simplify ASTextNode2 --- AsyncDisplayKit.xcodeproj/project.pbxproj | 16 ++ Source/ASCellNode.mm | 1 + Source/ASDisplayNodeExtras.h | 2 +- Source/ASTextNode.mm | 1 + Source/ASTextNode2.mm | 181 +++++++----------- Source/Base/ASCache.h | 39 ++++ Source/Base/ASCache.m | 71 +++++++ Source/Base/ASLog.h | 4 + Source/Base/ASLog.m | 4 + Source/Details/ASHashing.m | 8 +- .../TextExperiment/Component/ASTextCacheKey.h | 32 ++++ .../TextExperiment/Component/ASTextCacheKey.m | 84 ++++++++ .../TextExperiment/Component/ASTextLayout.h | 20 +- .../TextExperiment/Component/ASTextLayout.m | 139 +++++++++++--- 14 files changed, 459 insertions(+), 143 deletions(-) create mode 100644 Source/Base/ASCache.h create mode 100644 Source/Base/ASCache.m create mode 100644 Source/Private/TextExperiment/Component/ASTextCacheKey.h create mode 100644 Source/Private/TextExperiment/Component/ASTextCacheKey.m diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index c9f5cba82..41d338f64 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -329,6 +329,8 @@ CC11F97A1DB181180024D77B /* ASNetworkImageNodeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC11F9791DB181180024D77B /* ASNetworkImageNodeTests.m */; }; CC18248C200D49C800875940 /* ASTextNodeCommon.h in Headers */ = {isa = PBXBuildFile; fileRef = CC18248B200D49C800875940 /* ASTextNodeCommon.h */; settings = {ATTRIBUTES = (Public, ); }; }; CC224E962066CA6D00BBA57F /* configuration.json in Resources */ = {isa = PBXBuildFile; fileRef = CC224E952066CA6D00BBA57F /* configuration.json */; }; + CC23AE4C20B089A700B1CE50 /* ASTextCacheKey.h in Headers */ = {isa = PBXBuildFile; fileRef = CC23AE4A20B089A700B1CE50 /* ASTextCacheKey.h */; }; + CC23AE4D20B089A700B1CE50 /* ASTextCacheKey.m in Sources */ = {isa = PBXBuildFile; fileRef = CC23AE4B20B089A700B1CE50 /* ASTextCacheKey.m */; }; CC2F65EE1E5FFB1600DA57C9 /* ASMutableElementMap.h in Headers */ = {isa = PBXBuildFile; fileRef = CC2F65EC1E5FFB1600DA57C9 /* ASMutableElementMap.h */; }; CC2F65EF1E5FFB1600DA57C9 /* ASMutableElementMap.m in Sources */ = {isa = PBXBuildFile; fileRef = CC2F65ED1E5FFB1600DA57C9 /* ASMutableElementMap.m */; }; CC3B20841C3F76D600798563 /* ASPendingStateController.h in Headers */ = {isa = PBXBuildFile; fileRef = CC3B20811C3F76D600798563 /* ASPendingStateController.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -384,6 +386,8 @@ CCA282CD1E9EB73E0037E8B7 /* ASTipNode.m in Sources */ = {isa = PBXBuildFile; fileRef = CCA282CB1E9EB73E0037E8B7 /* ASTipNode.m */; }; CCA282D01E9EBF6C0037E8B7 /* ASTipsWindow.h in Headers */ = {isa = PBXBuildFile; fileRef = CCA282CE1E9EBF6C0037E8B7 /* ASTipsWindow.h */; }; CCA282D11E9EBF6C0037E8B7 /* ASTipsWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = CCA282CF1E9EBF6C0037E8B7 /* ASTipsWindow.m */; }; + CCA49B8F2057259A0047CF5A /* ASCache.h in Headers */ = {isa = PBXBuildFile; fileRef = CCA49B8D2057259A0047CF5A /* ASCache.h */; }; + CCA49B902057259A0047CF5A /* ASCache.m in Sources */ = {isa = PBXBuildFile; fileRef = CCA49B8E2057259A0047CF5A /* ASCache.m */; }; CCA5F62E1EECC2A80060C137 /* ASAssert.m in Sources */ = {isa = PBXBuildFile; fileRef = CCA5F62D1EECC2A80060C137 /* ASAssert.m */; }; CCAA0B7F206ADBF30057B336 /* ASRecursiveUnfairLock.h in Headers */ = {isa = PBXBuildFile; fileRef = CCAA0B7D206ADBF30057B336 /* ASRecursiveUnfairLock.h */; settings = {ATTRIBUTES = (Public, ); }; }; CCAA0B80206ADBF30057B336 /* ASRecursiveUnfairLock.m in Sources */ = {isa = PBXBuildFile; fileRef = CCAA0B7E206ADBF30057B336 /* ASRecursiveUnfairLock.m */; }; @@ -831,6 +835,8 @@ CC11F9791DB181180024D77B /* ASNetworkImageNodeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASNetworkImageNodeTests.m; sourceTree = ""; }; CC18248B200D49C800875940 /* ASTextNodeCommon.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASTextNodeCommon.h; sourceTree = ""; }; CC224E952066CA6D00BBA57F /* configuration.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = configuration.json; sourceTree = ""; }; + CC23AE4A20B089A700B1CE50 /* ASTextCacheKey.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = ASTextCacheKey.h; path = Component/ASTextCacheKey.h; sourceTree = ""; }; + CC23AE4B20B089A700B1CE50 /* ASTextCacheKey.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = ASTextCacheKey.m; path = Component/ASTextCacheKey.m; sourceTree = ""; }; CC2E317F1DAC353700EEE891 /* ASCollectionView+Undeprecated.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASCollectionView+Undeprecated.h"; sourceTree = ""; }; CC2F65EC1E5FFB1600DA57C9 /* ASMutableElementMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASMutableElementMap.h; sourceTree = ""; }; CC2F65ED1E5FFB1600DA57C9 /* ASMutableElementMap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASMutableElementMap.m; sourceTree = ""; }; @@ -894,6 +900,8 @@ CCA282CB1E9EB73E0037E8B7 /* ASTipNode.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTipNode.m; sourceTree = ""; }; CCA282CE1E9EBF6C0037E8B7 /* ASTipsWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTipsWindow.h; sourceTree = ""; }; CCA282CF1E9EBF6C0037E8B7 /* ASTipsWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASTipsWindow.m; sourceTree = ""; }; + CCA49B8D2057259A0047CF5A /* ASCache.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASCache.h; sourceTree = ""; }; + CCA49B8E2057259A0047CF5A /* ASCache.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASCache.m; sourceTree = ""; }; CCA5F62D1EECC2A80060C137 /* ASAssert.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASAssert.m; sourceTree = ""; }; CCAA0B7D206ADBF30057B336 /* ASRecursiveUnfairLock.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASRecursiveUnfairLock.h; sourceTree = ""; }; CCAA0B7E206ADBF30057B336 /* ASRecursiveUnfairLock.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASRecursiveUnfairLock.m; sourceTree = ""; }; @@ -1512,6 +1520,8 @@ 1950C4481A3BB5C1005C8279 /* ASEqualityHelpers.h */, 0516FA3B1A15563400B4EBED /* ASLog.h */, CCB1F9591EFB60A5009C7475 /* ASLog.m */, + CCA49B8D2057259A0047CF5A /* ASCache.h */, + CCA49B8E2057259A0047CF5A /* ASCache.m */, CCB1F95B1EFB6316009C7475 /* ASSignpost.h */, ); path = Base; @@ -1668,6 +1678,8 @@ CCCCCCC11EC3EF060087FE10 /* TextExperiment */ = { isa = PBXGroup; children = ( + CC23AE4A20B089A700B1CE50 /* ASTextCacheKey.h */, + CC23AE4B20B089A700B1CE50 /* ASTextCacheKey.m */, CCCCCCC21EC3EF060087FE10 /* Component */, CCCCCCCB1EC3EF060087FE10 /* String */, CCCCCCD01EC3EF060087FE10 /* Utility */, @@ -1811,6 +1823,7 @@ files = ( 1A6C000D1FAB4E2100D05926 /* ASCornerLayoutSpec.h in Headers */, E54E00721F1D3828000B30D7 /* ASPagerNode+Beta.h in Headers */, + CCA49B8F2057259A0047CF5A /* ASCache.h in Headers */, E5B225281F1790D6001E1431 /* ASHashing.h in Headers */, CC034A131E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.h in Headers */, 693A1DCA1ECC944E00D0C9D2 /* IGListAdapter+AsyncDisplayKit.h in Headers */, @@ -1964,6 +1977,7 @@ 698DFF441E36B6C9002891F1 /* ASStackLayoutSpecUtilities.h in Headers */, CCF18FF41D2575E300DF5895 /* NSIndexSet+ASHelpers.h in Headers */, 83A7D95C1D44548100BF333E /* ASWeakMap.h in Headers */, + CC23AE4C20B089A700B1CE50 /* ASTextCacheKey.h in Headers */, E5711A2C1C840C81009619D4 /* ASCollectionElement.h in Headers */, 9019FBBD1ED8061D00C45F72 /* ASYogaLayoutSpec.h in Headers */, 6947B0BE1E36B4E30007C478 /* ASStackUnpositionedLayout.h in Headers */, @@ -2308,6 +2322,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + CC23AE4D20B089A700B1CE50 /* ASTextCacheKey.m in Sources */, E5B225291F1790EE001E1431 /* ASHashing.m in Sources */, DEB8ED7C1DD003D300DBDE55 /* ASLayoutTransition.mm in Sources */, CCA5F62E1EECC2A80060C137 /* ASAssert.m in Sources */, @@ -2428,6 +2443,7 @@ CCA282B51E9EA7310037E8B7 /* ASTipsController.m in Sources */, B35062271B010EFD0018CF92 /* ASRangeController.mm in Sources */, 0442850A1BAA63FE00D16268 /* ASBatchFetching.m in Sources */, + CCA49B902057259A0047CF5A /* ASCache.m in Sources */, 68FC85E61CE29B9400EDD713 /* ASNavigationController.m in Sources */, CC4C2A791D88E3BF0039ACAB /* ASTraceEvent.m in Sources */, 34EFC76F1B701CF700AD841F /* ASRatioLayoutSpec.mm in Sources */, diff --git a/Source/ASCellNode.mm b/Source/ASCellNode.mm index c93da2db6..c3c6f87b7 100644 --- a/Source/ASCellNode.mm +++ b/Source/ASCellNode.mm @@ -267,6 +267,7 @@ + (BOOL)requestsVisibilityNotifications static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ cache = [[NSCache alloc] init]; + cache.name = @"org.TextureGroup.Texture.CellNodeVisibilityHandlersCache"; }); NSNumber *result = [cache objectForKey:self]; if (result == nil) { diff --git a/Source/ASDisplayNodeExtras.h b/Source/ASDisplayNodeExtras.h index ba0a38172..838b3424c 100644 --- a/Source/ASDisplayNodeExtras.h +++ b/Source/ASDisplayNodeExtras.h @@ -30,7 +30,7 @@ #define ASSetDebugName(node, format, ...) node.debugName = [NSString stringWithFormat:format, __VA_ARGS__] #define ASSetDebugNames(...) _ASSetDebugNames(self.class, @"" # __VA_ARGS__, __VA_ARGS__, nil) #else - #define ASSetDebugName(node, name) + #define ASSetDebugName(node, format, ...) #define ASSetDebugNames(...) #endif diff --git a/Source/ASTextNode.mm b/Source/ASTextNode.mm index 08f5f90bc..def347d86 100644 --- a/Source/ASTextNode.mm +++ b/Source/ASTextNode.mm @@ -110,6 +110,7 @@ - (BOOL)isEqual:(ASTextNodeRendererKey *)object dispatch_once(&onceToken, ^{ __rendererCache = [[NSCache alloc] init]; __rendererCache.countLimit = 500; // 500 renders cache + __rendererCache.name = @"org.TextureGroup.Texture.TextNodeRendererCache"; }); return __rendererCache; } diff --git a/Source/ASTextNode2.mm b/Source/ASTextNode2.mm index 376f8b316..abf2713b7 100644 --- a/Source/ASTextNode2.mm +++ b/Source/ASTextNode2.mm @@ -14,7 +14,6 @@ #import // Definition of ASTextNodeDelegate #import -#import #import #import @@ -30,18 +29,11 @@ #import #import +#import #import +#import #import -@interface ASTextCacheValue : NSObject { - @package - ASDN::Mutex _m; - std::deque> _layouts; -} -@end -@implementation ASTextCacheValue -@end - /** * If set, we will record all values set to attributedText into an array * and once we get 2000, we'll write them all out into a plist file. @@ -78,6 +70,12 @@ @implementation ASTextNode2 { NSAttributedString *_composedTruncationText; NSArray *_pointSizeScaleFactors; + // If nil, regenerate. + NSAttributedString *_preparedAttributedText; + + // Just a fast cache. May not be valid but good place to check first. + ASTextLayout *_lastUsedLayout; + NSString *_highlightedLinkAttributeName; id _highlightedLinkAttributeValue; ASTextNodeHighlightStyle _highlightStyle; @@ -228,18 +226,16 @@ - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize { ASDisplayNodeAssert(constrainedSize.width >= 0, @"Constrained width for text (%f) is too narrow", constrainedSize.width); ASDisplayNodeAssert(constrainedSize.height >= 0, @"Constrained height for text (%f) is too short", constrainedSize.height); - + ASLockScopeSelf(); - + + [self _ensureTruncationText]; ASTextContainer *container = [_textContainer copy]; - NSAttributedString *attributedText = self.attributedText; container.size = constrainedSize; - [self _ensureTruncationText]; - NSMutableAttributedString *mutableText = [attributedText mutableCopy]; - [self prepareAttributedString:mutableText]; - ASTextLayout *layout = [ASTextNode2 compatibleLayoutWithContainer:container text:mutableText]; + ASTextLayout *layout = [self compatibleLayoutWithContainer:container text:[self l_preparedAttributedText]]; + // Is this necessary? [self setNeedsDisplay]; return layout.textBoundingSize; @@ -281,6 +277,7 @@ - (void)setAttributedText:(NSAttributedString *)attributedText if (!ASCompareAssignCopy(_attributedText, attributedText)) { return; } + _preparedAttributedText = nil; // Since truncation text matches style of attributedText, invalidate it now. [self _invalidateTruncationText]; @@ -323,10 +320,17 @@ - (NSArray *)exclusionPaths return _textContainer.exclusionPaths; } -- (void)prepareAttributedString:(NSMutableAttributedString *)attributedString +- (NSAttributedString *)l_preparedAttributedText { - ASLockScopeSelf(); + if (_preparedAttributedText) { + return _preparedAttributedText; + } + + if (!_attributedText) { + return [[NSAttributedString alloc] init]; + } + 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 == _truncationMode) { @@ -350,6 +354,8 @@ - (void)prepareAttributedString:(NSMutableAttributedString *)attributedString shadow.shadowBlurRadius = _shadowRadius; [attributedString addAttribute:NSShadowAttributeName value:shadow range:NSMakeRange(0, attributedString.length)]; } + _preparedAttributedText = [attributedString copy]; + return _preparedAttributedText; } #pragma mark - Drawing @@ -360,107 +366,54 @@ - (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer [self _ensureTruncationText]; ASTextContainer *copiedContainer = [_textContainer copy]; copiedContainer.size = self.bounds.size; - NSMutableAttributedString *mutableText = [self.attributedText mutableCopy] ?: [[NSMutableAttributedString alloc] init]; - - [self prepareAttributedString:mutableText]; return @{ - @"container": copiedContainer, - @"text": mutableText, - @"bgColor": self.backgroundColor ?: [NSNull null] - }; + @"container": copiedContainer, + @"text": [self l_preparedAttributedText], + @"bgColor": self.backgroundColor ?: [NSNull null], + // Pass along self as a hack so that we can update _lastUsedLayout. + // We'll be careful + @"instance" : self + }; } /** * If it can't find a compatible layout, this method creates one. * - * NOTE: Be careful to copy `text` if needed. + * NOTE: The `container` you pass into this should be a copy, as it may be + * retained by the cache in the event of a cache miss. */ -+ (ASTextLayout *)compatibleLayoutWithContainer:(ASTextContainer *)container +- (ASTextLayout *)compatibleLayoutWithContainer:(ASTextContainer *)container text:(NSAttributedString *)text NS_RETURNS_RETAINED - { - // Allocate layoutCacheLock on the heap to prevent destruction at app exit (https://github.com/TextureGroup/Texture/issues/136) - static ASDN::StaticMutex& layoutCacheLock = *new ASDN::StaticMutex; - static NSCache *textLayoutCache; + // First quick check – if the last layout we computed is useable. + // On a cache miss, this method is going to run long and in some + // scenarios it may make sense to run concurrently with different + // sizes, so don't hold the lock the whole time. + auto lastLayout = ASLockedSelf(_lastUsedLayout); + if ([lastLayout isCompatibleWithContainer:container text:text]) { + return lastLayout; + } + + // All the keys inside of the cache will have their layout set on them. + // So the value kind of doesn't matter – if NSCache had a "set" mode + // where you used the -member: method, then we would use that. + static ASCache *textLayoutCache; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - textLayoutCache = [[NSCache alloc] init]; + textLayoutCache = [[ASCache alloc] init]; + textLayoutCache.name = @"org.TextureGroup.Texture.TextNode2LayoutCache"; }); - layoutCacheLock.lock(); - - ASTextCacheValue *cacheValue = [textLayoutCache objectForKey:text]; - if (cacheValue == nil) { - cacheValue = [[ASTextCacheValue alloc] init]; - [textLayoutCache setObject:cacheValue forKey:[text copy]]; - } - - // Lock the cache item for the rest of the method. Only after acquiring can we release the NSCache. - ASDN::MutexLocker lock(cacheValue->_m); - layoutCacheLock.unlock(); - - CGRect containerBounds = (CGRect){ .size = container.size }; - { - for (auto &t : cacheValue->_layouts) { - CGSize constrainedSize = std::get<0>(t); - ASTextLayout *layout = std::get<1>(t); - - CGSize layoutSize = layout.textBoundingSize; - // 1. CoreText can return frames that are narrower than the constrained width, for obvious reasons. - // 2. CoreText can return frames that are slightly wider than the constrained width, for some reason. - // We have to trust that somehow it's OK to try and draw within our size constraint, despite the return value. - // 3. Thus, those two values (constrained width & returned width) form a range, where - // intermediate values in that range will be snapped. Thus, we can use a given layout as long as our - // width is in that range, between the min and max of those two values. - CGRect minRect = CGRectMake(0, 0, MIN(layoutSize.width, constrainedSize.width), MIN(layoutSize.height, constrainedSize.height)); - if (!CGRectContainsRect(containerBounds, minRect)) { - continue; - } - CGRect maxRect = CGRectMake(0, 0, MAX(layoutSize.width, constrainedSize.width), MAX(layoutSize.height, constrainedSize.height)); - if (!CGRectContainsRect(maxRect, containerBounds)) { - continue; - } - if (!CGSizeEqualToSize(container.size, constrainedSize)) { - continue; - } - - // Now check container params. - ASTextContainer *otherContainer = layout.container; - if (!UIEdgeInsetsEqualToEdgeInsets(container.insets, otherContainer.insets)) { - continue; - } - if (!ASObjectIsEqual(container.exclusionPaths, otherContainer.exclusionPaths)) { - continue; - } - if (container.maximumNumberOfRows != otherContainer.maximumNumberOfRows) { - continue; - } - if (container.truncationType != otherContainer.truncationType) { - continue; - } - if (!ASObjectIsEqual(container.truncationToken, otherContainer.truncationToken)) { - continue; - } - // TODO: When we get a cache hit, move this entry to the front (LRU). - return layout; - } - } - - // Cache Miss. Compute the text layout. - ASTextLayout *layout = [ASTextLayout layoutWithContainer:container text:text]; - - // Store the result in the cache. - { - // This is a critical section. However we also must hold the lock until this point, in case - // another thread requests this cache item while a layout is being calculated, so they don't race. - cacheValue->_layouts.push_front(std::make_tuple(container.size, layout)); - if (cacheValue->_layouts.size() > 3) { - cacheValue->_layouts.pop_back(); - } - } - - return layout; + + ASTextCacheKey *key = [[ASTextCacheKey alloc] initWithContainer:container attributedString:text]; + + ASTextLayout *result = [textLayoutCache objectForKey:key constructedWithBlock:^(ASTextCacheKey *keyParam) { + return [keyParam createLayout]; + }]; + + ASLockedSelf(_lastUsedLayout = result); + return result; } + (void)drawRect:(CGRect)bounds withParameters:(NSDictionary *)layoutDict isCancelled:(asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing @@ -468,7 +421,10 @@ + (void)drawRect:(CGRect)bounds withParameters:(NSDictionary *)layoutDict isCanc ASTextContainer *container = layoutDict[@"container"]; NSAttributedString *text = layoutDict[@"text"]; UIColor *bgColor = layoutDict[@"bgColor"]; - ASTextLayout *layout = [self compatibleLayoutWithContainer:container text:text]; + + // We try to limit our use of instance, but we should update + // its _lastUsedLayout to get MAXIMUM PERFORMANCE + ASTextLayout *layout = [layoutDict[@"instance"] compatibleLayoutWithContainer:container text:text]; if (isCancelledBlock()) { return; @@ -517,7 +473,7 @@ - (id)_linkAttributeValueAtPoint:(CGPoint)point // See discussion in https://github.com/TextureGroup/Texture/pull/396 ASTextContainer *containerCopy = [_textContainer copy]; containerCopy.size = self.calculatedSize; - ASTextLayout *layout = [ASTextNode2 compatibleLayoutWithContainer:containerCopy text:_attributedText]; + ASTextLayout *layout = [self compatibleLayoutWithContainer:containerCopy text:_attributedText]; NSRange visibleRange = layout.visibleRange; NSRange clampedRange = NSIntersectionRange(visibleRange, NSMakeRange(0, _attributedText.length)); @@ -531,8 +487,8 @@ - (id)_linkAttributeValueAtPoint:(CGPoint)point } NSRange effectiveRange = NSMakeRange(0, 0); - for (__strong NSString *attributeName in self.linkAttributeNames) { - id value = [self.attributedText attribute:attributeName atIndex:range.start.offset longestEffectiveRange:&effectiveRange inRange:clampedRange]; + for (__strong NSString *attributeName in _linkAttributeNames) { + id value = [_attributedText attribute:attributeName atIndex:range.start.offset longestEffectiveRange:&effectiveRange inRange:clampedRange]; if (value == nil) { // Didn't find any links specified with this attribute. continue; @@ -768,7 +724,7 @@ - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event // See discussion in https://github.com/TextureGroup/Texture/pull/396 ASTextContainer *containerCopy = [_textContainer copy]; containerCopy.size = self.calculatedSize; - ASTextLayout *layout = [ASTextNode2 compatibleLayoutWithContainer:containerCopy text:_attributedText]; + ASTextLayout *layout = [self compatibleLayoutWithContainer:containerCopy text:_attributedText]; visibleRange = layout.visibleRange; } NSRange truncationMessageRange = [self _additionalTruncationMessageRangeWithVisibleRange:visibleRange]; @@ -1093,8 +1049,7 @@ - (NSAttributedString *)_locked_composedTruncationText } /** - * - cleanses it of core text attributes so TextKit doesn't crash - * - Adds whole-string attributes so the truncation message matches the styling + * Adds whole-string attributes so the truncation message matches the styling * of the body text */ - (NSAttributedString *)_locked_prepareTruncationStringForDrawing:(NSAttributedString *)truncationString diff --git a/Source/Base/ASCache.h b/Source/Base/ASCache.h new file mode 100644 index 000000000..eeb347bc0 --- /dev/null +++ b/Source/Base/ASCache.h @@ -0,0 +1,39 @@ +// +// ASCache.h +// Texture +// +// Copyright (c) 2018-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 + +NS_ASSUME_NONNULL_BEGIN + +/** + * A cache that coalesces requests for the same key. + * + * It also prints hit rate statistics if ASCachingLogEnabled is defined. + */ +AS_SUBCLASSING_RESTRICTED +@interface ASCache : NSCache + +/** + * Get an object for the specified key. If no object for the + * given key exists, construct one with the given block. + * + * This method can be called from any thread. If another thread is + * already constructing a value for the given key, this one will wait + * for it to finish. In practice this is extraordinarily rare. + */ +- (ObjectType)objectForKey:(KeyType)key + constructedWithBlock:(ObjectType (NS_NOESCAPE ^)(KeyType key))block; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Base/ASCache.m b/Source/Base/ASCache.m new file mode 100644 index 000000000..ec3b77b21 --- /dev/null +++ b/Source/Base/ASCache.m @@ -0,0 +1,71 @@ +// +// ASCache.m +// Texture +// +// Copyright (c) 2018-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 +#import +#import + +@implementation ASCache { +#if ASCachingLogEnabled + NSUInteger _hitCount; + NSUInteger _missCount; +#endif +} + +// If cache logging is on, override this method and track stats. +#if ASCachingLogEnabled +- (id)objectForKey:(id)key +{ + id result = [super objectForKey:key]; + + { + static NSLock *l; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + l = [[NSLock alloc] init]; + }); + ASLockScopeUnowned(l); + if (result) { + _hitCount += 1; + } else { + _missCount += 1; + } + NSUInteger totalReads = _hitCount + _missCount; + if (totalReads % 10 == 0) { + as_log_info(ASCachingLog(), "%@ hit rate: %d/%d (%.2f%%)", self.name.length ? self.name : self.debugDescription, _hitCount, totalReads, 100.0 * (_hitCount / (double)totalReads)); + } + } + + return result; +} +#endif + +- (id)objectForKey:(id)key constructedWithBlock:(id (^)(id))block +{ + // We could do lots of interesting stuff here, including this working + // implementation of request coalescing, but at the moment none + // of it is justified. We only coalesce a few hits out of 300 requests. + // https://gist.github.com/Adlai-Holler/f1e71e94c9fefc27ed87d2b309e98f98 + id object = [self objectForKey:key]; + if (object) { + return object; + } + + object = block(key); + + // Could do this async but that might cause more misses than its worth. + [self setObject:object forKey:key]; + return object; +} + +@end diff --git a/Source/Base/ASLog.h b/Source/Base/ASLog.h index e87404631..431d57848 100644 --- a/Source/Base/ASLog.h +++ b/Source/Base/ASLog.h @@ -72,6 +72,10 @@ os_log_t ASCollectionLog(void); #define ASImageLoadingLogEnabled 1 os_log_t ASImageLoadingLog(void); +/// Log for cache hit rates. +#define ASCachingLogEnabled 0 +os_log_t ASCachingLog(void); + /// Specialized log for our main thread deallocation trampoline. #define ASMainThreadDeallocationLogEnabled 0 os_log_t ASMainThreadDeallocationLog(void); diff --git a/Source/Base/ASLog.m b/Source/Base/ASLog.m index e1c42ea79..1d96ef673 100644 --- a/Source/Base/ASLog.m +++ b/Source/Base/ASLog.m @@ -47,6 +47,10 @@ os_log_t ASImageLoadingLog() { return (ASImageLoadingLogEnabled && ASLoggingIsEnabled()) ? ASCreateOnce(as_log_create("org.TextureGroup.Texture", "ImageLoading")) : OS_LOG_DISABLED; } +os_log_t ASCachingLog() { + return (ASCachingLogEnabled && ASLoggingIsEnabled()) ? ASCreateOnce(as_log_create("org.TextureGroup.Texture", "Caching")) : OS_LOG_DISABLED; +} + os_log_t ASMainThreadDeallocationLog() { return (ASMainThreadDeallocationLogEnabled && ASLoggingIsEnabled()) ? ASCreateOnce(as_log_create("org.TextureGroup.Texture", "MainDealloc")) : OS_LOG_DISABLED; } diff --git a/Source/Details/ASHashing.m b/Source/Details/ASHashing.m index 9f6d0f234..016c6c66c 100644 --- a/Source/Details/ASHashing.m +++ b/Source/Details/ASHashing.m @@ -12,7 +12,11 @@ #import +#ifdef __LP64__ +#define ELF_STEP(B) T1 = (H << 4) + B; T2 = T1 & 0xF000000000000000; if (T2) T1 ^= (T2 >> 56); T1 &= (~T2); H = T1; +#else #define ELF_STEP(B) T1 = (H << 4) + B; T2 = T1 & 0xF0000000; if (T2) T1 ^= (T2 >> 24); T1 &= (~T2); H = T1; +#endif /** * The hashing algorithm copied from CoreFoundation CFHashBytes function. @@ -21,8 +25,8 @@ NSUInteger ASHashBytes(void *bytesarg, size_t length) { /* The ELF hash algorithm, used in the ELF object file format */ uint8_t *bytes = (uint8_t *)bytesarg; - UInt32 H = 0, T1, T2; - SInt32 rem = (SInt32)length; + NSUInteger H = 0, T1, T2; + size_t rem = length; while (3 < rem) { ELF_STEP(bytes[length - rem]); ELF_STEP(bytes[length - rem + 1]); diff --git a/Source/Private/TextExperiment/Component/ASTextCacheKey.h b/Source/Private/TextExperiment/Component/ASTextCacheKey.h new file mode 100644 index 000000000..af6a76960 --- /dev/null +++ b/Source/Private/TextExperiment/Component/ASTextCacheKey.h @@ -0,0 +1,32 @@ +// +// ASTextCacheKey.h +// AsyncDisplayKit +// +// Created by Adlai on 5/19/18. +// Copyright © 2018 Pinterest. All rights reserved. +// + +#import +#import + +@class ASTextLayout, ASTextContainer; + +NS_ASSUME_NONNULL_BEGIN + +AS_SUBCLASSING_RESTRICTED +@interface ASTextCacheKey : NSObject + +/// The container you pass in will not be copied. The attributedString however will be. +- (instancetype)initWithContainer:(ASTextContainer *)container + attributedString:(NSAttributedString *)attributedString; + +// nil if we don't have a layout yet. +// non-null for entries stored in the cache. +@property (atomic, readonly, nullable) ASTextLayout *layout; + +// Cache miss method. Compute the layout and store it on self. +- (ASTextLayout *)createLayout; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Private/TextExperiment/Component/ASTextCacheKey.m b/Source/Private/TextExperiment/Component/ASTextCacheKey.m new file mode 100644 index 000000000..3088e31b6 --- /dev/null +++ b/Source/Private/TextExperiment/Component/ASTextCacheKey.m @@ -0,0 +1,84 @@ +// +// ASTextCacheKey.m +// AsyncDisplayKit +// +// Created by Adlai on 5/19/18. +// Copyright © 2018 Pinterest. All rights reserved. +// + +#import + +#import +#import + + +@interface ASTextCacheKey () +@property (atomic) NSUInteger cachedHash; +@property (atomic) ASTextLayout *layout; +@end + +@implementation ASTextCacheKey { + NSAttributedString *_attributedString; + ASTextContainer *_container; +} + +- (instancetype)initWithContainer:(ASTextContainer *)container attributedString:(NSAttributedString *)attributedString +{ + if (self = [super init]) { + _container = container; + _attributedString = [attributedString copy]; + self.cachedHash = NSUIntegerMax; + } + return self; +} + +- (NSUInteger)hash +{ + NSUInteger cached = self.cachedHash; + if (cached != NSUIntegerMax) { + return cached; + } + + // Don't include size in hash. Size -> layout mapping is many-to-one (fuzzy). +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wpadded" + struct { + size_t attributedStringHash; + size_t containerHash; +#pragma clang diagnostic pop + } data = { + _attributedString.hash, + [_container hashIncludingSize:NO], + }; + NSUInteger result = ASHashBytes(&data, sizeof(data)); + self.cachedHash = result; + return result; +} + +- (ASTextLayout *)createLayout +{ + NSAssert(self.layout == nil, @"Multiple calls to -createLayout."); + ASTextLayout *l = [ASTextLayout layoutWithContainer:_container text:_attributedString]; + self.layout = l; + return l; +} + +- (BOOL)isEqual:(ASTextCacheKey *)otherKey +{ + // NOTE: Skip the class check for this specialized "Key" object. + + // Either we have the layout (we are inside the cache) + // or we do not (we are being checked against an entry + // in the cache). + + ASTextLayout *layout = self.layout; + if (layout) { + // We have the layout, they are being checked for compatibility. + return [layout isCompatibleWithContainer:otherKey->_container text:otherKey->_attributedString]; + } else { + // They have the layout, we are being checked for compatibility. + return [otherKey.layout isCompatibleWithContainer:_container text:_attributedString]; + } +} + +@end diff --git a/Source/Private/TextExperiment/Component/ASTextLayout.h b/Source/Private/TextExperiment/Component/ASTextLayout.h index 5de6f057c..720e84f08 100755 --- a/Source/Private/TextExperiment/Component/ASTextLayout.h +++ b/Source/Private/TextExperiment/Component/ASTextLayout.h @@ -102,6 +102,10 @@ extern const CGSize ASTextContainerMaxSize; /// This modifier is applied to the lines before the layout is completed, /// give you a chance to modify the line position. Default is nil. @property (nullable, copy) id linePositionModifier; + +/// The hash of this container, optionally including the size in the hash. +- (NSUInteger)hashIncludingSize:(BOOL)includeSize; + @end @@ -163,7 +167,7 @@ extern const CGSize ASTextContainerMaxSize; @param text The text (if nil, returns nil). @return A new layout, or nil when an error occurs. */ -+ (nullable ASTextLayout *)layoutWithContainerSize:(CGSize)size text:(NSAttributedString *)text; ++ (nullable ASTextLayout *)layoutWithContainerSize:(CGSize)size text:(NSAttributedString *)text NS_RETURNS_RETAINED; /** Generate a layout with the given container and text. @@ -172,7 +176,7 @@ extern const CGSize ASTextContainerMaxSize; @param text The text (if nil, returns nil). @return A new layout, or nil when an error occurs. */ -+ (nullable ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttributedString *)text; ++ (nullable ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttributedString *)text NS_RETURNS_RETAINED; /** Generate a layout with the given container and text. @@ -183,7 +187,7 @@ extern const CGSize ASTextContainerMaxSize; length of the range is 0, it means the length is no limit. @return A new layout, or nil when an error occurs. */ -+ (nullable ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range; ++ (nullable ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range NS_RETURNS_RETAINED; /** Generate layouts with the given containers and text. @@ -194,7 +198,7 @@ extern const CGSize ASTextContainerMaxSize; or nil when an error occurs. */ + (nullable NSArray *)layoutWithContainers:(NSArray *)containers - text:(NSAttributedString *)text; + text:(NSAttributedString *)text NS_RETURNS_RETAINED; /** Generate layouts with the given containers and text. @@ -208,7 +212,7 @@ extern const CGSize ASTextContainerMaxSize; */ + (nullable NSArray *)layoutWithContainers:(NSArray *)containers text:(NSAttributedString *)text - range:(NSRange)range; + range:(NSRange)range NS_RETURNS_RETAINED; - (instancetype)init UNAVAILABLE_ATTRIBUTE; + (instancetype)new UNAVAILABLE_ATTRIBUTE; @@ -549,6 +553,12 @@ extern const CGSize ASTextContainerMaxSize; size:(CGSize)size debug:(nullable ASTextDebugOption *)debug; +/** + * Whether this layout can be used for the given container-text pair. + */ +- (BOOL)isCompatibleWithContainer:(ASTextContainer *)container + text:(NSAttributedString *)text; + @end NS_ASSUME_NONNULL_END diff --git a/Source/Private/TextExperiment/Component/ASTextLayout.m b/Source/Private/TextExperiment/Component/ASTextLayout.m index 3707e3d94..0056ab06b 100755 --- a/Source/Private/TextExperiment/Component/ASTextLayout.m +++ b/Source/Private/TextExperiment/Component/ASTextLayout.m @@ -19,7 +19,11 @@ #import #import #import +#import #import +#import + +#import const CGSize ASTextContainerMaxSize = (CGSize){0x100000, 0x100000}; @@ -90,7 +94,7 @@ - (id)copyWithZone:(NSZone *)zone { @implementation ASTextContainer { @package BOOL _readonly; ///< used only in ASTextLayout.implementation - dispatch_semaphore_t _lock; + pthread_mutex_t _lock; CGSize _size; UIEdgeInsets _insets; @@ -125,26 +129,31 @@ + (instancetype)containerWithPath:(UIBezierPath *)path NS_RETURNS_RETAINED { - (instancetype)init { self = [super init]; if (!self) return nil; - _lock = dispatch_semaphore_create(1); + pthread_mutex_init(&_lock, NULL); _pathFillEvenOdd = YES; return self; } +- (void)dealloc +{ + pthread_mutex_destroy(&_lock); +} + - (id)copyWithZone:(NSZone *)zone { ASTextContainer *one = [self.class new]; - dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); + pthread_mutex_lock(&_lock); one->_size = _size; one->_insets = _insets; one->_path = _path; - one->_exclusionPaths = _exclusionPaths.copy; + one->_exclusionPaths = [_exclusionPaths copy]; one->_pathFillEvenOdd = _pathFillEvenOdd; one->_pathLineWidth = _pathLineWidth; one->_verticalForm = _verticalForm; one->_maximumNumberOfRows = _maximumNumberOfRows; one->_truncationType = _truncationType; - one->_truncationToken = _truncationToken.copy; + one->_truncationToken = [_truncationToken copy]; one->_linePositionModifier = [(NSObject *)_linePositionModifier copy]; - dispatch_semaphore_signal(_lock); + pthread_mutex_unlock(&_lock); return one; } @@ -186,9 +195,9 @@ - (id)initWithCoder:(NSCoder *)aDecoder { } #define Getter(...) \ -dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); \ +pthread_mutex_lock(&_lock); \ __VA_ARGS__; \ -dispatch_semaphore_signal(_lock); +pthread_mutex_unlock(&_lock); #define Setter(...) \ if (_readonly) { \ @@ -196,9 +205,9 @@ - (id)initWithCoder:(NSCoder *)aDecoder { reason:@"Cannot change the property of the 'container' in 'ASTextLayout'." userInfo:nil]; \ return; \ } \ -dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); \ +pthread_mutex_lock(&_lock); \ __VA_ARGS__; \ -dispatch_semaphore_signal(_lock); +pthread_mutex_unlock(&_lock); - (CGSize)size { Getter(CGSize size = _size) return size; @@ -228,7 +237,7 @@ - (UIBezierPath *)path { - (void)setPath:(UIBezierPath *)path { Setter( - _path = path.copy; + _path = [path copy]; if (_path) { CGRect bounds = _path.bounds; CGSize size = bounds.size; @@ -248,7 +257,7 @@ - (NSArray *)exclusionPaths { } - (void)setExclusionPaths:(NSArray *)exclusionPaths { - Setter(_exclusionPaths = exclusionPaths.copy); + Setter(_exclusionPaths = [exclusionPaths copy]); } - (BOOL)isPathFillEvenOdd { @@ -296,7 +305,7 @@ - (NSAttributedString *)truncationToken { } - (void)setTruncationToken:(NSAttributedString *)truncationToken { - Setter(_truncationToken = truncationToken.copy); + Setter(_truncationToken = [truncationToken copy]); } - (void)setLinePositionModifier:(id)linePositionModifier { @@ -307,6 +316,46 @@ - (void)setLinePositionModifier:(id)linePositionModi Getter(id m = _linePositionModifier) return m; } +- (NSUInteger)hash +{ + return [self hashIncludingSize:YES]; +} + +- (NSUInteger)hashIncludingSize:(BOOL)includeSize +{ + pthread_mutex_lock(&_lock); +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wpadded" + struct { + CGSize size; + ASTextTruncationType truncationType; + NSUInteger truncationTokenHash; + NSUInteger maximumNumberOfRows; + NSUInteger verticalForm; + CGFloat pathLineWidth; + NSUInteger pathFillEvenOdd; + NSUInteger exclusionPathsHash; + NSUInteger pathHash; + UIEdgeInsets insets; + NSUInteger linePositionModifierHash; +#pragma clang diagnostic pop + } data = { + includeSize ? _size : CGSizeZero, + _truncationType, + _truncationToken.hash, + _maximumNumberOfRows, + (NSUInteger)_verticalForm, + _pathLineWidth, + (NSUInteger)_pathFillEvenOdd, + _exclusionPaths.hash, + _path.hash, + _insets, + _linePositionModifier.hash + }; + pthread_mutex_unlock(&_lock); + return ASHashBytes(&data, sizeof(data)); +} + #undef Getter #undef Setter @end @@ -360,16 +409,16 @@ - (instancetype)_init { return self; } -+ (ASTextLayout *)layoutWithContainerSize:(CGSize)size text:(NSAttributedString *)text { ++ (ASTextLayout *)layoutWithContainerSize:(CGSize)size text:(NSAttributedString *)text NS_RETURNS_RETAINED { ASTextContainer *container = [ASTextContainer containerWithSize:size]; return [self layoutWithContainer:container text:text]; } -+ (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttributedString *)text { ++ (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttributedString *)text NS_RETURNS_RETAINED { return [self layoutWithContainer:container text:text range:NSMakeRange(0, text.length)]; } -+ (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range { ++ (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range NS_RETURNS_RETAINED { ASTextLayout *layout = NULL; CGPathRef cgPath = nil; CGRect cgPathBox = {0}; @@ -404,8 +453,8 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri if (lineRowsIndex) free(lineRowsIndex); \ return nil; } - text = text.mutableCopy; - container = container.copy; + text = [text copy]; + container = [container copy]; if (!text || !container) return nil; if (range.location + range.length > text.length) return nil; container->_readonly = YES; @@ -689,7 +738,7 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri if (runCount > 0) { CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, runCount - 1); attrs = (id)CTRunGetAttributes(run); - attrs = attrs ? attrs.mutableCopy : [NSMutableArray new]; + attrs = attrs ? [attrs mutableCopy] : [NSMutableArray new]; [attrs removeObjectsForKeys:[NSMutableAttributedString as_allDiscontinuousAttributeKeys]]; CTFontRef font = (__bridge CTFontRef)attrs[(id)kCTFontAttributeName]; CGFloat fontSize = font ? CTFontGetSize(font) : 12.0; @@ -721,7 +770,7 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri } else if (container.truncationType == ASTextTruncationTypeMiddle) { type = kCTLineTruncationMiddle; } - NSMutableAttributedString *lastLineText = [text attributedSubstringFromRange:lastLine.range].mutableCopy; + NSMutableAttributedString *lastLineText = [[text attributedSubstringFromRange:lastLine.range] mutableCopy]; [lastLineText appendAttributedString:truncationToken]; CTLineRef ctLastLineExtend = CTLineCreateWithAttributedString((CFAttributedStringRef)lastLineText); if (ctLastLineExtend) { @@ -2111,7 +2160,7 @@ - (NSArray *)selectionRectsForRange:(ASTextRange *)range { } - (NSArray *)selectionRectsWithoutStartAndEndForRange:(ASTextRange *)range { - NSMutableArray *rects = [self selectionRectsForRange:range].mutableCopy; + NSMutableArray *rects = [[self selectionRectsForRange:range] mutableCopy]; for (NSInteger i = 0, max = rects.count; i < max; i++) { ASTextSelectionRect *rect = rects[i]; if (rect.containsStart || rect.containsEnd) { @@ -2124,7 +2173,7 @@ - (NSArray *)selectionRectsWithoutStartAndEndForRange:(ASTextRange *)range { } - (NSArray *)selectionRectsWithOnlyStartAndEndForRange:(ASTextRange *)range { - NSMutableArray *rects = [self selectionRectsForRange:range].mutableCopy; + NSMutableArray *rects = [[self selectionRectsForRange:range] mutableCopy]; for (NSInteger i = 0, max = rects.count; i < max; i++) { ASTextSelectionRect *rect = rects[i]; if (!rect.containsStart && !rect.containsEnd) { @@ -3356,4 +3405,50 @@ - (void)drawInContext:(CGContextRef)context [self drawInContext:context size:size point:CGPointZero view:nil layer:nil debug:debug cancel:nil]; } +- (BOOL)isCompatibleWithContainer:(ASTextContainer *)otherContainer text:(NSAttributedString *)otherText +{ + // Text must be the same. + if (![_text isEqualToAttributedString:otherText]) { + return NO; + } + + CGRect containerBounds = (CGRect){ .size = _container.size }; + CGSize layoutSize = self.textBoundingSize; + // 1. CoreText can return frames that are narrower than the constrained width, for obvious reasons. + // 2. CoreText can return frames that are slightly wider than the constrained width, for some reason. + // We have to trust that somehow it's OK to try and draw within our size constraint, despite the return value. + // 3. Thus, those two values (constrained width & returned width) form a range, where + // intermediate values in that range will be snapped. Thus, we can use a given layout as long as our + // width is in that range, between the min and max of those two values. + CGRect minRect = CGRectMake(0, 0, MIN(layoutSize.width, containerBounds.size.width), MIN(layoutSize.height, containerBounds.size.height)); + if (!CGRectContainsRect(containerBounds, minRect)) { + return NO; + } + CGRect maxRect = CGRectMake(0, 0, MAX(layoutSize.width, containerBounds.size.width), MAX(layoutSize.height, containerBounds.size.height)); + if (!CGRectContainsRect(maxRect, containerBounds)) { + return NO; + } + + // Now check container params. + if (!UIEdgeInsetsEqualToEdgeInsets(_container.insets, otherContainer.insets)) { + return NO; + } + if (!ASObjectIsEqual(_container.exclusionPaths, otherContainer.exclusionPaths)) { + return NO; + } + if (!ASObjectIsEqual(_container.path, otherContainer.path)) { + return NO; + } + if (_container.maximumNumberOfRows != otherContainer.maximumNumberOfRows) { + return NO; + } + if (_container.truncationType != otherContainer.truncationType) { + return NO; + } + if (!ASObjectIsEqual(_container.truncationToken, otherContainer.truncationToken)) { + return NO; + } + return YES; +} + @end From c2dc9ad5611ef12d0ac6a313f316daf5ad7ed29e Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Sun, 20 May 2018 09:07:28 -0700 Subject: [PATCH 2/2] Address CI warnings --- CHANGELOG.md | 1 + .../Private/TextExperiment/Component/ASTextCacheKey.h | 10 +++++++--- .../Private/TextExperiment/Component/ASTextCacheKey.m | 10 +++++++--- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3057240b..f70dc622d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ - Prevent UITextView from updating contentOffset while deallocating [Michael Schneider](https://github.com/maicki) - [ASCollectionNode/ASTableNode] Fix a crash occurs while remeasuring cell nodes. [Huy Nguyen](https://github.com/nguyenhuy) [#917](https://github.com/TextureGroup/Texture/pull/917) - Fix an issue where ASConfigurationDelegate would not call out for "control" users. If set, it now receives events whenever an experimental feature decision point occurs, whether it's enabled or not. [Adlai Holler](https://github.com/Adlai-Holler) +- Simplified and improved performance of ASTextNode2. [Adlai Holler](https://github.com/Adlai-Holler) ## 2.6 - [Xcode 9] Updated to require Xcode 9 (to fix warnings) [Garrett Moon](https://github.com/garrettmoon) diff --git a/Source/Private/TextExperiment/Component/ASTextCacheKey.h b/Source/Private/TextExperiment/Component/ASTextCacheKey.h index af6a76960..9a005cfeb 100644 --- a/Source/Private/TextExperiment/Component/ASTextCacheKey.h +++ b/Source/Private/TextExperiment/Component/ASTextCacheKey.h @@ -1,9 +1,13 @@ // // ASTextCacheKey.h -// AsyncDisplayKit +// Texture // -// Created by Adlai on 5/19/18. -// Copyright © 2018 Pinterest. All rights reserved. +// Copyright (c) 2018-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 diff --git a/Source/Private/TextExperiment/Component/ASTextCacheKey.m b/Source/Private/TextExperiment/Component/ASTextCacheKey.m index 3088e31b6..dd499905b 100644 --- a/Source/Private/TextExperiment/Component/ASTextCacheKey.m +++ b/Source/Private/TextExperiment/Component/ASTextCacheKey.m @@ -1,9 +1,13 @@ // // ASTextCacheKey.m -// AsyncDisplayKit +// Texture // -// Created by Adlai on 5/19/18. -// Copyright © 2018 Pinterest. All rights reserved. +// Copyright (c) 2018-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