From f7d91bb8776992e69ef3b97b9ef25fd5d1196ad4 Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Tue, 14 Oct 2014 18:35:11 -0700 Subject: [PATCH] Implement -reclaimMemory API and switch to manually controlled content clearing. ASDisplayNode and several subclasses had previously cleared memory-heavy objects like the backing store and text layout manager when the node's view or layer is removed from a visible heirarchy. This works great in any system that uses a "working range", where exiting the range removes the node from the hierarchy and reclaiming memory at that time is important. However, for standard UIViewController patterns (unused in Paper), this behavior causes highly undesirable thrashing (leading to visible flashes & wasteful re-rendering of content). After this change, node subclasses should implement -reclaimMemory if they need to perform any other cleanup besides backing store destruction when they leave a working range or other scenario where memory reduction is valuable. To trigger this behavior, calling code should use -recursivelyReclaimMemory. r=nadi --- AsyncDisplayKit/ASDisplayNode+Subclasses.h | 6 ++ AsyncDisplayKit/ASDisplayNode.h | 13 +++++ AsyncDisplayKit/ASDisplayNode.mm | 55 +++++++++++++------ AsyncDisplayKit/ASImageNode.mm | 14 ----- AsyncDisplayKit/ASTextNode.mm | 20 +------ AsyncDisplayKit/Details/ASRangeController.mm | 19 +++---- AsyncDisplayKit/Details/_ASDisplayView.mm | 4 +- .../Private/ASDisplayNodeInternal.h | 12 ++-- examples/Kittens/Sample/ViewController.m | 3 +- 9 files changed, 76 insertions(+), 70 deletions(-) diff --git a/AsyncDisplayKit/ASDisplayNode+Subclasses.h b/AsyncDisplayKit/ASDisplayNode+Subclasses.h index 7bdf58c9c6..f41c597ff6 100644 --- a/AsyncDisplayKit/ASDisplayNode+Subclasses.h +++ b/AsyncDisplayKit/ASDisplayNode+Subclasses.h @@ -179,6 +179,7 @@ * @discussion Subclasses should override this if they don't want their contentsScale changed. * * @note This changes an internal property. + * -setNeedsDisplay is also available to trigger display without changing contentsScaleForDisplay. * @see contentsScaleForDisplay */ - (void)setNeedsDisplayAtScale:(CGFloat)contentsScale; @@ -271,6 +272,11 @@ // Called after the view is removed from the window. - (void)didExitHierarchy; +// Called by -recursivelyReclaimMemory. Provides an opportunity to clear backing store and other memory-intensive intermediates, +// such as text layout managers or downloaded content that can be written to a disk cache. +// Base class implements self.contents = nil, clearing any backing store, for asynchronous regeneration when needed. +- (void)reclaimMemory; + /** @name Description */ diff --git a/AsyncDisplayKit/ASDisplayNode.h b/AsyncDisplayKit/ASDisplayNode.h index 8b0971f28b..2b344e7304 100644 --- a/AsyncDisplayKit/ASDisplayNode.h +++ b/AsyncDisplayKit/ASDisplayNode.h @@ -322,6 +322,19 @@ */ - (void)recursiveSetPreventOrCancelDisplay:(BOOL)flag; +/** + * @abstract Calls -reclaimMemory on the receiver and its subnode hierarchy. + * + * @discussion Clears backing stores and other memory-intensive intermediates. + * If the node is removed from a visible hierarchy and then re-added, it will automatically trigger a new asynchronous display, + * as long as preventOrCancelDisplay is not set. + * If the node remains in the hierarchy throughout, -setNeedsDisplay is required to trigger a new asynchronous display. + * + * @see preventOrCancelDisplay and setNeedsDisplay + */ + +- (void)recursivelyReclaimMemory; + /** @name Hit Testing */ diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index d5b0556a66..b3f9b0ca83 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -56,6 +56,7 @@ BOOL ASDisplayNodeSubclassOverridesSelector(Class subclass, SEL selector) // Subclasses should never override these ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(calculatedSize)), @"Subclass %@ must not override calculatedSize method", NSStringFromClass(self)); ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(measure:)), @"Subclass %@ must not override measure method", NSStringFromClass(self)); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(recursivelyReclaimMemory)), @"Subclass %@ must not override recursivelyReclaimMemory method", NSStringFromClass(self)); } + (BOOL)layerBackedNodesEnabled @@ -575,9 +576,9 @@ static inline CATransform3D _calculateTransformFromReferenceToTarget(ASDisplayNo - (id)actionForLayer:(CALayer *)layer forKey:(NSString *)event { if (event == kCAOnOrderIn) { - [self __appear]; + [self __enterHierarchy]; } else if (event == kCAOnOrderOut) { - [self __disappear]; + [self __exitHierarchy]; } ASDisplayNodeAssert(_flags.isLayerBacked, @"We shouldn't get called back here if there is no layer"); @@ -948,40 +949,45 @@ static NSInteger incrementIfFound(NSInteger i) { return NO; } -- (void)__appear +- (void)__enterHierarchy { ASDisplayNodeAssertMainThread(); - ASDisplayNodeAssert(!_flags.isInAppear, @"Should not cause recursive __appear"); + ASDisplayNodeAssert(!_flags.isInEnterHierarchy, @"Should not cause recursive __enterHierarchy"); if (!self.inWindow && !_flags.visibilityNotificationsDisabled && ![self __hasParentWithVisibilityNotificationsDisabled]) { self.inWindow = YES; - _flags.isInAppear = YES; + _flags.isInEnterHierarchy = YES; if (self.shouldRasterizeDescendants) { // Nodes that are descendants of a rasterized container do not have views or layers, and so cannot receive visibility notifications directly via orderIn/orderOut CALayer actions. Manually send visibility notifications to rasterized descendants. [self _recursiveWillEnterHierarchy]; } else { [self willEnterHierarchy]; } - _flags.isInAppear = NO; + _flags.isInEnterHierarchy = NO; + + CALayer *layer = self.layer; + if (!self.layer.contents) { + [layer setNeedsDisplay]; + } } } -- (void)__disappear +- (void)__exitHierarchy { ASDisplayNodeAssertMainThread(); - ASDisplayNodeAssert(!_flags.isInDisappear, @"Should not cause recursive __disappear"); + ASDisplayNodeAssert(!_flags.isInExitHierarchy, @"Should not cause recursive __exitHierarchy"); if (self.inWindow && !_flags.visibilityNotificationsDisabled && ![self __hasParentWithVisibilityNotificationsDisabled]) { self.inWindow = NO; [self.asyncLayer cancelAsyncDisplay]; - _flags.isInDisappear = YES; + _flags.isInExitHierarchy = YES; if (self.shouldRasterizeDescendants) { // Nodes that are descendants of a rasterized container do not have views or layers, and so cannot receive visibility notifications directly via orderIn/orderOut CALayer actions. Manually send visibility notifications to rasterized descendants. [self _recursiveDidExitHierarchy]; } else { [self didExitHierarchy]; } - _flags.isInDisappear = NO; + _flags.isInExitHierarchy = NO; } } @@ -991,9 +997,9 @@ static NSInteger incrementIfFound(NSInteger i) { return; } - _flags.isInAppear = YES; + _flags.isInEnterHierarchy = YES; [self willEnterHierarchy]; - _flags.isInAppear = NO; + _flags.isInEnterHierarchy = NO; for (ASDisplayNode *subnode in self.subnodes) { [subnode _recursiveWillEnterHierarchy]; @@ -1006,9 +1012,9 @@ static NSInteger incrementIfFound(NSInteger i) { return; } - _flags.isInDisappear = YES; + _flags.isInExitHierarchy = YES; [self didExitHierarchy]; - _flags.isInDisappear = NO; + _flags.isInExitHierarchy = NO; for (ASDisplayNode *subnode in self.subnodes) { [subnode _recursiveDidExitHierarchy]; @@ -1077,15 +1083,28 @@ static NSInteger incrementIfFound(NSInteger i) { - (void)willEnterHierarchy { ASDisplayNodeAssertMainThread(); - ASDisplayNodeAssert(_flags.isInAppear, @"You should never call -willEnterHierarchy directly. Appearance is automatically managed by ASDisplayNode"); - ASDisplayNodeAssert(!_flags.isInDisappear, @"ASDisplayNode inconsistency. __appear and __disappear are mutually exclusive"); + ASDisplayNodeAssert(_flags.isInEnterHierarchy, @"You should never call -willEnterHierarchy directly. Appearance is automatically managed by ASDisplayNode"); + ASDisplayNodeAssert(!_flags.isInExitHierarchy, @"ASDisplayNode inconsistency. __enterHierarchy and __exitHierarchy are mutually exclusive"); } - (void)didExitHierarchy { ASDisplayNodeAssertMainThread(); - ASDisplayNodeAssert(_flags.isInDisappear, @"You should never call -didExitHierarchy directly. Appearance is automatically managed by ASDisplayNode"); - ASDisplayNodeAssert(!_flags.isInAppear, @"ASDisplayNode inconsistency. __appear and __disappear are mutually exclusive"); + ASDisplayNodeAssert(_flags.isInExitHierarchy, @"You should never call -didExitHierarchy directly. Appearance is automatically managed by ASDisplayNode"); + ASDisplayNodeAssert(!_flags.isInEnterHierarchy, @"ASDisplayNode inconsistency. __enterHierarchy and __exitHierarchy are mutually exclusive"); +} + +- (void)reclaimMemory +{ + self.layer.contents = nil; +} + +- (void)recursivelyReclaimMemory +{ + for (ASDisplayNode *subnode in self.subnodes) { + [subnode recursivelyReclaimMemory]; + } + [self reclaimMemory]; } - (void)layout diff --git a/AsyncDisplayKit/ASImageNode.mm b/AsyncDisplayKit/ASImageNode.mm index 9c7c39fbca..41fcd0030f 100644 --- a/AsyncDisplayKit/ASImageNode.mm +++ b/AsyncDisplayKit/ASImageNode.mm @@ -246,20 +246,6 @@ return result; } -- (void)didExitHierarchy -{ - self.contents = nil; - [super didExitHierarchy]; -} - -- (void)willEnterHierarchy -{ - [super willEnterHierarchy]; - - if (!self.layer.contents) - [self setNeedsDisplay]; -} - - (void)displayDidFinish { [super displayDidFinish]; diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index c3b779198c..4cbc684208 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -194,17 +194,6 @@ ASDISPLAYNODE_INLINE CGFloat ceilPixelValue(CGFloat f) fminf(ceilPixelValue(renderSizePlusShadowPadding.height), constrainedSize.height)); } -- (void)willEnterHierarchy -{ - CALayer *layer = self.layer; - if (!layer.contents) { - // This can happen on occasion that the layer will not display unless this - // set. - [layer setNeedsDisplay]; - } - [super willEnterHierarchy]; -} - - (void)displayDidFinish { [super displayDidFinish]; @@ -217,16 +206,13 @@ ASDISPLAYNODE_INLINE CGFloat ceilPixelValue(CGFloat f) [self _invalidateRenderer]; } -- (void)didExitHierarchy +- (void)reclaimMemory { - // We nil out the contents and kill our renderer to prevent the very large + // 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. - self.contents = nil; - + [super reclaimMemory]; // ASDisplayNode will set layer.contents = nil [self _invalidateRenderer]; - - [super didExitHierarchy]; } - (void)didLoad diff --git a/AsyncDisplayKit/Details/ASRangeController.mm b/AsyncDisplayKit/Details/ASRangeController.mm index 0be03e9b1e..5bc3390cfd 100644 --- a/AsyncDisplayKit/Details/ASRangeController.mm +++ b/AsyncDisplayKit/Details/ASRangeController.mm @@ -192,6 +192,11 @@ static BOOL ASRangeIsValid(NSRange range) [node recursiveSetPreventOrCancelDisplay:YES]; [node.view removeFromSuperview]; + + // since this class usually manages large or infinite data sets, the working range + // directly bounds memory usage by requiring redrawing any content that falls outside the range. + [node recursivelyReclaimMemory]; + [_workingIndexPaths removeObject:node.asyncdisplaykit_indexPath]; } @@ -209,21 +214,11 @@ static BOOL ASRangeIsValid(NSRange range) ASDisplayNodeAssertMainThread(); ASDisplayNodeAssert(node && view, @"invalid argument, did you mean -removeNodeFromWorkingView:?"); - // use an explicit transaction to force CoreAnimation to display nodes in order + // use an explicit transaction to force CoreAnimation to display nodes in the order they are added. [CATransaction begin]; - // if this node is in the view hierarchy, moving it will cause a redisplay, so we disable hierarchy notifications. - // if it *isn't* in the view hierarchy, we need it to receive those notifications to trigger its first display. - BOOL nodeIsInHierarchy = node.inWindow; - - if (nodeIsInHierarchy) - ASDisplayNodeDisableHierarchyNotifications(node); - [view addSubview:node.view]; - - if (nodeIsInHierarchy) - ASDisplayNodeEnableHierarchyNotifications(node); - + [CATransaction commit]; } diff --git a/AsyncDisplayKit/Details/_ASDisplayView.mm b/AsyncDisplayKit/Details/_ASDisplayView.mm index aacece0553..0d10bfeb6d 100644 --- a/AsyncDisplayKit/Details/_ASDisplayView.mm +++ b/AsyncDisplayKit/Details/_ASDisplayView.mm @@ -81,9 +81,9 @@ { BOOL visible = newWindow != nil; if (visible && !_node.inWindow) { - [_node __appear]; + [_node __enterHierarchy]; } else if (!visible && _node.inWindow) { - [_node __disappear]; + [_node __exitHierarchy]; } } diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index f2d343b168..fdc32e70c1 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -61,8 +61,8 @@ BOOL ASDisplayNodeSubclassOverridesSelector(Class subclass, SEL selector); unsigned displaysAsynchronously:1; unsigned shouldRasterizeDescendants:1; unsigned visibilityNotificationsDisabled:visibilityNotificationsDisabledBits; - unsigned isInAppear:1; - unsigned isInDisappear:1; + unsigned isInEnterHierarchy:1; + unsigned isInExitHierarchy:1; unsigned inWindow:1; unsigned hasWillDisplayAsyncLayer:1; unsigned hasDrawParametersForAsyncLayer:1; @@ -102,10 +102,10 @@ BOOL ASDisplayNodeSubclassOverridesSelector(Class subclass, SEL selector); - (void)__incrementVisibilityNotificationsDisabled; - (void)__decrementVisibilityNotificationsDisabled; -// Call willAppear if necessary and set inWindow = YES if visibility notifications are enabled on all of its parents -- (void)__appear; -// Call willDisappear / didDisappear if necessary and set inWindow = NO if visibility notifications are enabled on all of its parents -- (void)__disappear; +// Call willEnterHierarchy if necessary and set inWindow = YES if visibility notifications are enabled on all of its parents +- (void)__enterHierarchy; +// Call didExitHierarchy if necessary and set inWindow = NO if visibility notifications are enabled on all of its parents +- (void)__exitHierarchy; // Returns the ancestor node that rasterizes descendants, or nil if none. - (ASDisplayNode *)__rasterizedContainerNode; diff --git a/examples/Kittens/Sample/ViewController.m b/examples/Kittens/Sample/ViewController.m index e458b98511..b0a865deab 100644 --- a/examples/Kittens/Sample/ViewController.m +++ b/examples/Kittens/Sample/ViewController.m @@ -59,7 +59,8 @@ static const NSInteger kLitterSize = 20; return self; } -- (void)viewDidLoad { +- (void)viewDidLoad +{ [super viewDidLoad]; [self.view addSubview:_tableView];