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];