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.
*
* @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 */

View File

@@ -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 */

View File

@@ -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<CAAction>)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

View File

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

View File

@@ -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

View File

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

View File

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

View File

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

View File

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