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
This commit is contained in:
Scott Goodson
2014-10-14 18:35:11 -07:00
parent 6a1854ed6a
commit f7d91bb877
9 changed files with 76 additions and 70 deletions

View File

@@ -179,6 +179,7 @@
* @discussion Subclasses should override this if they don't want their contentsScale changed. * @discussion Subclasses should override this if they don't want their contentsScale changed.
* *
* @note This changes an internal property. * @note This changes an internal property.
* -setNeedsDisplay is also available to trigger display without changing contentsScaleForDisplay.
* @see contentsScaleForDisplay * @see contentsScaleForDisplay
*/ */
- (void)setNeedsDisplayAtScale:(CGFloat)contentsScale; - (void)setNeedsDisplayAtScale:(CGFloat)contentsScale;
@@ -271,6 +272,11 @@
// Called after the view is removed from the window. // Called after the view is removed from the window.
- (void)didExitHierarchy; - (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 */ /** @name Description */

View File

@@ -322,6 +322,19 @@
*/ */
- (void)recursiveSetPreventOrCancelDisplay:(BOOL)flag; - (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 */ /** @name Hit Testing */

View File

@@ -56,6 +56,7 @@ BOOL ASDisplayNodeSubclassOverridesSelector(Class subclass, SEL selector)
// Subclasses should never override these // Subclasses should never override these
ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(calculatedSize)), @"Subclass %@ must not override calculatedSize method", NSStringFromClass(self)); 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(measure:)), @"Subclass %@ must not override measure method", NSStringFromClass(self));
ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(recursivelyReclaimMemory)), @"Subclass %@ must not override recursivelyReclaimMemory method", NSStringFromClass(self));
} }
+ (BOOL)layerBackedNodesEnabled + (BOOL)layerBackedNodesEnabled
@@ -575,9 +576,9 @@ static inline CATransform3D _calculateTransformFromReferenceToTarget(ASDisplayNo
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event - (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
{ {
if (event == kCAOnOrderIn) { if (event == kCAOnOrderIn) {
[self __appear]; [self __enterHierarchy];
} else if (event == kCAOnOrderOut) { } else if (event == kCAOnOrderOut) {
[self __disappear]; [self __exitHierarchy];
} }
ASDisplayNodeAssert(_flags.isLayerBacked, @"We shouldn't get called back here if there is no layer"); 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; return NO;
} }
- (void)__appear - (void)__enterHierarchy
{ {
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
ASDisplayNodeAssert(!_flags.isInAppear, @"Should not cause recursive __appear"); ASDisplayNodeAssert(!_flags.isInEnterHierarchy, @"Should not cause recursive __enterHierarchy");
if (!self.inWindow && !_flags.visibilityNotificationsDisabled && ![self __hasParentWithVisibilityNotificationsDisabled]) { if (!self.inWindow && !_flags.visibilityNotificationsDisabled && ![self __hasParentWithVisibilityNotificationsDisabled]) {
self.inWindow = YES; self.inWindow = YES;
_flags.isInAppear = YES; _flags.isInEnterHierarchy = YES;
if (self.shouldRasterizeDescendants) { 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. // 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]; [self _recursiveWillEnterHierarchy];
} else { } else {
[self willEnterHierarchy]; [self willEnterHierarchy];
} }
_flags.isInAppear = NO; _flags.isInEnterHierarchy = NO;
CALayer *layer = self.layer;
if (!self.layer.contents) {
[layer setNeedsDisplay];
}
} }
} }
- (void)__disappear - (void)__exitHierarchy
{ {
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
ASDisplayNodeAssert(!_flags.isInDisappear, @"Should not cause recursive __disappear"); ASDisplayNodeAssert(!_flags.isInExitHierarchy, @"Should not cause recursive __exitHierarchy");
if (self.inWindow && !_flags.visibilityNotificationsDisabled && ![self __hasParentWithVisibilityNotificationsDisabled]) { if (self.inWindow && !_flags.visibilityNotificationsDisabled && ![self __hasParentWithVisibilityNotificationsDisabled]) {
self.inWindow = NO; self.inWindow = NO;
[self.asyncLayer cancelAsyncDisplay]; [self.asyncLayer cancelAsyncDisplay];
_flags.isInDisappear = YES; _flags.isInExitHierarchy = YES;
if (self.shouldRasterizeDescendants) { 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. // 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]; [self _recursiveDidExitHierarchy];
} else { } else {
[self didExitHierarchy]; [self didExitHierarchy];
} }
_flags.isInDisappear = NO; _flags.isInExitHierarchy = NO;
} }
} }
@@ -991,9 +997,9 @@ static NSInteger incrementIfFound(NSInteger i) {
return; return;
} }
_flags.isInAppear = YES; _flags.isInEnterHierarchy = YES;
[self willEnterHierarchy]; [self willEnterHierarchy];
_flags.isInAppear = NO; _flags.isInEnterHierarchy = NO;
for (ASDisplayNode *subnode in self.subnodes) { for (ASDisplayNode *subnode in self.subnodes) {
[subnode _recursiveWillEnterHierarchy]; [subnode _recursiveWillEnterHierarchy];
@@ -1006,9 +1012,9 @@ static NSInteger incrementIfFound(NSInteger i) {
return; return;
} }
_flags.isInDisappear = YES; _flags.isInExitHierarchy = YES;
[self didExitHierarchy]; [self didExitHierarchy];
_flags.isInDisappear = NO; _flags.isInExitHierarchy = NO;
for (ASDisplayNode *subnode in self.subnodes) { for (ASDisplayNode *subnode in self.subnodes) {
[subnode _recursiveDidExitHierarchy]; [subnode _recursiveDidExitHierarchy];
@@ -1077,15 +1083,28 @@ static NSInteger incrementIfFound(NSInteger i) {
- (void)willEnterHierarchy - (void)willEnterHierarchy
{ {
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
ASDisplayNodeAssert(_flags.isInAppear, @"You should never call -willEnterHierarchy directly. Appearance is automatically managed by ASDisplayNode"); ASDisplayNodeAssert(_flags.isInEnterHierarchy, @"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.isInExitHierarchy, @"ASDisplayNode inconsistency. __enterHierarchy and __exitHierarchy are mutually exclusive");
} }
- (void)didExitHierarchy - (void)didExitHierarchy
{ {
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
ASDisplayNodeAssert(_flags.isInDisappear, @"You should never call -didExitHierarchy directly. Appearance is automatically managed by ASDisplayNode"); ASDisplayNodeAssert(_flags.isInExitHierarchy, @"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.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 - (void)layout

View File

@@ -246,20 +246,6 @@
return result; return result;
} }
- (void)didExitHierarchy
{
self.contents = nil;
[super didExitHierarchy];
}
- (void)willEnterHierarchy
{
[super willEnterHierarchy];
if (!self.layer.contents)
[self setNeedsDisplay];
}
- (void)displayDidFinish - (void)displayDidFinish
{ {
[super displayDidFinish]; [super displayDidFinish];

View File

@@ -194,17 +194,6 @@ ASDISPLAYNODE_INLINE CGFloat ceilPixelValue(CGFloat f)
fminf(ceilPixelValue(renderSizePlusShadowPadding.height), constrainedSize.height)); 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 - (void)displayDidFinish
{ {
[super displayDidFinish]; [super displayDidFinish];
@@ -217,16 +206,13 @@ ASDISPLAYNODE_INLINE CGFloat ceilPixelValue(CGFloat f)
[self _invalidateRenderer]; [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 // memory overhead of maintaining these for all text nodes. They can be
// regenerated when layout is necessary. // regenerated when layout is necessary.
self.contents = nil; [super reclaimMemory]; // ASDisplayNode will set layer.contents = nil
[self _invalidateRenderer]; [self _invalidateRenderer];
[super didExitHierarchy];
} }
- (void)didLoad - (void)didLoad

View File

@@ -192,6 +192,11 @@ static BOOL ASRangeIsValid(NSRange range)
[node recursiveSetPreventOrCancelDisplay:YES]; [node recursiveSetPreventOrCancelDisplay:YES];
[node.view removeFromSuperview]; [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]; [_workingIndexPaths removeObject:node.asyncdisplaykit_indexPath];
} }
@@ -209,21 +214,11 @@ static BOOL ASRangeIsValid(NSRange range)
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
ASDisplayNodeAssert(node && view, @"invalid argument, did you mean -removeNodeFromWorkingView:?"); 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]; [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]; [view addSubview:node.view];
if (nodeIsInHierarchy)
ASDisplayNodeEnableHierarchyNotifications(node);
[CATransaction commit]; [CATransaction commit];
} }

View File

@@ -81,9 +81,9 @@
{ {
BOOL visible = newWindow != nil; BOOL visible = newWindow != nil;
if (visible && !_node.inWindow) { if (visible && !_node.inWindow) {
[_node __appear]; [_node __enterHierarchy];
} else if (!visible && _node.inWindow) { } else if (!visible && _node.inWindow) {
[_node __disappear]; [_node __exitHierarchy];
} }
} }

View File

@@ -61,8 +61,8 @@ BOOL ASDisplayNodeSubclassOverridesSelector(Class subclass, SEL selector);
unsigned displaysAsynchronously:1; unsigned displaysAsynchronously:1;
unsigned shouldRasterizeDescendants:1; unsigned shouldRasterizeDescendants:1;
unsigned visibilityNotificationsDisabled:visibilityNotificationsDisabledBits; unsigned visibilityNotificationsDisabled:visibilityNotificationsDisabledBits;
unsigned isInAppear:1; unsigned isInEnterHierarchy:1;
unsigned isInDisappear:1; unsigned isInExitHierarchy:1;
unsigned inWindow:1; unsigned inWindow:1;
unsigned hasWillDisplayAsyncLayer:1; unsigned hasWillDisplayAsyncLayer:1;
unsigned hasDrawParametersForAsyncLayer:1; unsigned hasDrawParametersForAsyncLayer:1;
@@ -102,10 +102,10 @@ BOOL ASDisplayNodeSubclassOverridesSelector(Class subclass, SEL selector);
- (void)__incrementVisibilityNotificationsDisabled; - (void)__incrementVisibilityNotificationsDisabled;
- (void)__decrementVisibilityNotificationsDisabled; - (void)__decrementVisibilityNotificationsDisabled;
// Call willAppear if necessary and set inWindow = YES if visibility notifications are enabled on all of its parents // Call willEnterHierarchy if necessary and set inWindow = YES if visibility notifications are enabled on all of its parents
- (void)__appear; - (void)__enterHierarchy;
// Call willDisappear / didDisappear if necessary and set inWindow = NO if visibility notifications are enabled on all of its parents // Call didExitHierarchy if necessary and set inWindow = NO if visibility notifications are enabled on all of its parents
- (void)__disappear; - (void)__exitHierarchy;
// Returns the ancestor node that rasterizes descendants, or nil if none. // Returns the ancestor node that rasterizes descendants, or nil if none.
- (ASDisplayNode *)__rasterizedContainerNode; - (ASDisplayNode *)__rasterizedContainerNode;

View File

@@ -59,7 +59,8 @@ static const NSInteger kLitterSize = 20;
return self; return self;
} }
- (void)viewDidLoad { - (void)viewDidLoad
{
[super viewDidLoad]; [super viewDidLoad];
[self.view addSubview:_tableView]; [self.view addSubview:_tableView];