From 21bebb9d2910fea3f3cb02cdc0b825f9721414d1 Mon Sep 17 00:00:00 2001 From: Scott Goodson Date: Wed, 9 Mar 2016 19:36:45 -0800 Subject: [PATCH] [ASRangeController] Improvements to application state change and memory warning handling. Introduces ASLayoutRangeModeVisibleOnly, allowing the preservation of decoded backing stores without any extra padding to strictly minimize memory usage while supporting immediate re-display of content. Set visible range controllers to this mode upon app suspend / memory warning, while more aggressively clearing others to the ASLayoutRangeModeLowMemory mode. By default, when the app is running and recieves a memory warning, we set the range mode for non-visible controllers to ASLayoutRangeModeVisibleOnly. This is because, unlike in the app suspend case where on app resume we can restore controllers from LowMemory to VisibleOnly, the memory warning doesn't provide a good opportunity to do this. A new +Beta API to control this behavior is called +setRangeModeForMemoryWarnings:, as some apps may prefer to use LowMemory in the memory warning scenario. For these apps, optimal user experience will require manually setting the range mode back to some larger value as the user navigates the app, or they will encounter controllers that are temporarily blank and need a moment to re-display their contents as they start to become visible. --- AsyncDisplayKit.xcodeproj/project.pbxproj | 16 -- AsyncDisplayKit/ASCollectionView.mm | 12 +- AsyncDisplayKit/ASImageNode.mm | 6 +- AsyncDisplayKit/ASTableView.mm | 12 +- .../Details/ASAbstractLayoutController.mm | 12 ++ AsyncDisplayKit/Details/ASLayoutRangeType.h | 15 +- AsyncDisplayKit/Details/ASRangeController.h | 8 +- AsyncDisplayKit/Details/ASRangeController.mm | 167 +++++++++++++----- ...SRangeControllerUpdateRangeProtocol+Beta.h | 9 + AsyncDisplayKit/Private/ASWeakSet.m | 5 + AsyncDisplayKit/Private/_ASPendingState.mm | 10 ++ 11 files changed, 185 insertions(+), 87 deletions(-) diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index c1b1ac8b..a8fd8fff 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -1675,7 +1675,6 @@ 058D09B9195D04C000B7D73C /* Frameworks */, 058D09BA195D04C000B7D73C /* Resources */, 3B9D88CDF51B429C8409E4B6 /* Copy Pods Resources */, - BD5CC779F736EBA28F5313FB /* Embed Pods Frameworks */, ); buildRules = ( ); @@ -1805,21 +1804,6 @@ shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests-resources.sh\"\n"; showEnvVarsInLog = 0; }; - BD5CC779F736EBA28F5313FB /* Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Embed Pods Frameworks"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 83aff004..09994745 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -1040,20 +1040,12 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; - (void)clearContents { - for (NSArray *section in [_dataController completedNodes]) { - for (ASDisplayNode *node in section) { - [node exitInterfaceState:ASInterfaceStateDisplay]; - } - } + [_rangeController clearContents]; } - (void)clearFetchedData { - for (NSArray *section in [_dataController completedNodes]) { - for (ASDisplayNode *node in section) { - [node exitInterfaceState:ASInterfaceStateFetchData]; - } - } + [_rangeController clearFetchedData]; } #pragma mark - _ASDisplayView behavior substitutions diff --git a/AsyncDisplayKit/ASImageNode.mm b/AsyncDisplayKit/ASImageNode.mm index ec4141a5..94891a62 100644 --- a/AsyncDisplayKit/ASImageNode.mm +++ b/AsyncDisplayKit/ASImageNode.mm @@ -128,13 +128,15 @@ - (void)setImage:(UIImage *)image { - ASDN::MutexLocker l(_imageLock); + _imageLock.lock(); if (!ASObjectIsEqual(_image, image)) { _image = image; - ASDN::MutexUnlocker u(_imageLock); + _imageLock.unlock(); [self invalidateCalculatedLayout]; [self setNeedsDisplay]; + } else { + _imageLock.unlock(); // We avoid using MutexUnlocker as it needlessly re-locks at the end of the scope. } } diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index ce53236d..1cad0832 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -1037,20 +1037,12 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; - (void)clearContents { - for (NSArray *section in [_dataController completedNodes]) { - for (ASDisplayNode *node in section) { - [node recursivelyClearContents]; - } - } + [_rangeController clearContents]; } - (void)clearFetchedData { - for (NSArray *section in [_dataController completedNodes]) { - for (ASDisplayNode *node in section) { - [node recursivelyClearFetchedData]; - } - } + [_rangeController clearFetchedData]; } #pragma mark - _ASDisplayView behavior substitutions diff --git a/AsyncDisplayKit/Details/ASAbstractLayoutController.mm b/AsyncDisplayKit/Details/ASAbstractLayoutController.mm index 270d0b12..3ed6ccc5 100644 --- a/AsyncDisplayKit/Details/ASAbstractLayoutController.mm +++ b/AsyncDisplayKit/Details/ASAbstractLayoutController.mm @@ -51,6 +51,18 @@ extern BOOL ASRangeTuningParametersEqualToRangeTuningParameters(ASRangeTuningPar .trailingBufferScreenfuls = 2 }; + _tuningParameters[ASLayoutRangeModeVisibleOnly][ASLayoutRangeTypeDisplay] = { + .leadingBufferScreenfuls = 0, + .trailingBufferScreenfuls = 0 + }; + _tuningParameters[ASLayoutRangeModeVisibleOnly][ASLayoutRangeTypeFetchData] = { + .leadingBufferScreenfuls = 0, + .trailingBufferScreenfuls = 0 + }; + + // The Low Memory range mode has special handling. Because a zero range still includes the visible area / bounds, + // in order to implement the behavior of releasing all graphics memory (backing stores), ASRangeController must check + // for this range mode and use an empty set for displayIndexPaths rather than querying the ASLayoutController for the indexPaths. _tuningParameters[ASLayoutRangeModeLowMemory][ASLayoutRangeTypeDisplay] = { .leadingBufferScreenfuls = 0, .trailingBufferScreenfuls = 0 diff --git a/AsyncDisplayKit/Details/ASLayoutRangeType.h b/AsyncDisplayKit/Details/ASLayoutRangeType.h index 44d593cd..1b1e3a3c 100644 --- a/AsyncDisplayKit/Details/ASLayoutRangeType.h +++ b/AsyncDisplayKit/Details/ASLayoutRangeType.h @@ -29,9 +29,18 @@ typedef NS_ENUM(NSUInteger, ASLayoutRangeMode) { ASLayoutRangeModeFull, /** - * Low Memory mode is used when a range controller should limit the amount of work it performs to 0. - * Thus, it discards most of the views/layers that are created and it is trying to save as much system - * resources as possible. + * Visible Only mode is used when a range controller should set its display and fetch data regions to only the size of their bounds. + * This causes all additional backing stores & fetched data to be released, while ensuring a user revisiting the view will + * still be able to see the expected content. This mode is automatically set on all ASRangeControllers when the app suspends, + * allowing the operating system to keep the app alive longer and increase the chance it is still warm when the user returns. + */ + ASLayoutRangeModeVisibleOnly, + + /** + * Low Memory mode is used when a range controller should discard ALL graphics buffers, including for the area that would be visible + * the next time the user views it (bounds). The only range it preserves is Fetch Data, which is limited to the bounds, allowing + * the content to be restored relatively quickly by re-decoding images (the compressed images are ~10% the size of the decoded ones, + * and text is a tiny fraction of its rendered size). */ ASLayoutRangeModeLowMemory, ASLayoutRangeModeCount diff --git a/AsyncDisplayKit/Details/ASRangeController.h b/AsyncDisplayKit/Details/ASRangeController.h index 4169268d..bba570d0 100644 --- a/AsyncDisplayKit/Details/ASRangeController.h +++ b/AsyncDisplayKit/Details/ASRangeController.h @@ -13,8 +13,7 @@ #import #import -#define ASRangeControllerLoggingEnabled 1 -#define ASRangeControllerAutomaticLowMemoryHandling 1 +#define ASRangeControllerLoggingEnabled 0 NS_ASSUME_NONNULL_BEGIN @@ -59,6 +58,11 @@ NS_ASSUME_NONNULL_BEGIN - (ASRangeTuningParameters)tuningParametersForRangeMode:(ASLayoutRangeMode)rangeMode rangeType:(ASLayoutRangeType)rangeType; +// These methods call the corresponding method on each node, visiting each one that +// the range controller has set a non-default interface state on. +- (void)clearContents; +- (void)clearFetchedData; + /** * An object that describes the layout behavior of the ranged component (table view, collection view, etc.) * diff --git a/AsyncDisplayKit/Details/ASRangeController.mm b/AsyncDisplayKit/Details/ASRangeController.mm index 30e6b41d..c466d28f 100644 --- a/AsyncDisplayKit/Details/ASRangeController.mm +++ b/AsyncDisplayKit/Details/ASRangeController.mm @@ -73,6 +73,19 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; return ASLayoutRangeModeFull; } +- (ASInterfaceState)interfaceState +{ + ASInterfaceState selfInterfaceState = ASInterfaceStateNone; + if (_dataSource) { + selfInterfaceState = [_dataSource interfaceStateForRangeController:self]; + } + if (__ApplicationState == UIApplicationStateBackground) { + // If the app is background, pretend to be invisible so that we inform each cell it is no longer being viewed by the user + selfInterfaceState &= ~(ASInterfaceStateVisible); + } + return selfInterfaceState; +} + - (void)visibleNodeIndexPathsDidChangeWithScrollDirection:(ASScrollDirection)scrollDirection { _scrollDirection = scrollDirection; @@ -95,7 +108,7 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; if (_queuedRangeUpdate) { return; } - + // coalesce these events -- handling them multiple times per runloop is noisy and expensive _queuedRangeUpdate = YES; @@ -116,26 +129,37 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; { _layoutController = layoutController; _layoutControllerImplementsSetVisibleIndexPaths = [_layoutController respondsToSelector:@selector(setVisibleNodeIndexPaths:)]; + if (_layoutController && _queuedRangeUpdate) { + [self performRangeUpdate]; + } +} + +- (void)setDataSource:(id)dataSource +{ + _dataSource = dataSource; + if (_dataSource && _queuedRangeUpdate) { + [self performRangeUpdate]; + } } - (void)_updateVisibleNodeIndexPaths { ASDisplayNodeAssert(_layoutController, @"An ASLayoutController is required by ASRangeController"); - if (!_queuedRangeUpdate || !_layoutController) { + if (!_queuedRangeUpdate || !_layoutController || !_dataSource) { return; } - + // TODO: Consider if we need to use this codepath, or can rely on something more similar to the data & display ranges // Example: ... = [_layoutController indexPathsForScrolling:_scrollDirection rangeType:ASLayoutRangeTypeVisible]; NSArray *visibleNodePaths = [_dataSource visibleNodeIndexPathsForRangeController:self]; - + if (visibleNodePaths.count == 0) { // if we don't have any visibleNodes currently (scrolled before or after content)... _queuedRangeUpdate = NO; return; // don't do anything for this update, but leave _rangeIsValid == NO to make sure we update it later } - + [_layoutController setViewportSize:[_dataSource viewportSizeForRangeController:self]]; - + // the layout controller needs to know what the current visible indices are to calculate range offsets if (_layoutControllerImplementsSetVisibleIndexPaths) { [_layoutController setVisibleNodeIndexPaths:visibleNodePaths]; @@ -157,12 +181,7 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; // the network or display queues before preloading (offscreen) nodes are enqueued. NSMutableOrderedSet *allIndexPaths = [[NSMutableOrderedSet alloc] initWithSet:visibleIndexPaths]; - ASInterfaceState selfInterfaceState = [_dataSource interfaceStateForRangeController:self]; - if (__ApplicationState == UIApplicationStateBackground) { - // If the app is background, proceed as if all range controllers are invisible so that we inform each cell it is no longer being viewed by the user - selfInterfaceState &= ~(ASInterfaceStateVisible); - } - + ASInterfaceState selfInterfaceState = [self interfaceState]; ASLayoutRangeMode rangeMode = _currentRangeMode; if (!_didUpdateCurrentRange) { rangeMode = [ASRangeController rangeModeForInterfaceState:selfInterfaceState currentRangeMode:_currentRangeMode]; @@ -177,7 +196,7 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; rangeMode:rangeMode rangeType:ASLayoutRangeTypeFetchData]; } - + ASRangeTuningParameters parametersDisplay = [_layoutController tuningParametersForRangeMode:rangeMode rangeType:ASLayoutRangeTypeDisplay]; if (rangeMode == ASLayoutRangeModeLowMemory) { @@ -212,7 +231,7 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; if (!_rangeIsValid) { [allIndexPaths addObjectsFromArray:ASIndexPathsForMultidimensionalArray(allNodes)]; } - + // TODO Don't register for notifications if this range update doesn't cause any node to enter rendering pipeline. // This can be done once there is an API to observe to (or be notified upon) interface state changes or pipeline enterings [self registerForNotificationsForInterfaceStateIfNeeded:selfInterfaceState]; @@ -283,33 +302,24 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; } } } - + if (_didRegisterForNotifications) { _pendingDisplayNodesTimestamp = CFAbsoluteTimeGetCurrent(); } - + _rangeIsValid = YES; _queuedRangeUpdate = NO; #if ASRangeControllerLoggingEnabled - NSSet *visibleNodePathsSet = [NSSet setWithArray:visibleNodePaths]; - BOOL setsAreEqual = [visibleIndexPaths isEqualToSet:visibleNodePathsSet]; - NSLog(@"visible sets are equal: %d", setsAreEqual); - if (!setsAreEqual) { - NSLog(@"standard: %@", visibleIndexPaths); - NSLog(@"custom: %@", visibleNodePathsSet); - } - +// NSSet *visibleNodePathsSet = [NSSet setWithArray:visibleNodePaths]; +// BOOL setsAreEqual = [visibleIndexPaths isEqualToSet:visibleNodePathsSet]; +// NSLog(@"visible sets are equal: %d", setsAreEqual); +// if (!setsAreEqual) { +// NSLog(@"standard: %@", visibleIndexPaths); +// NSLog(@"custom: %@", visibleNodePathsSet); +// } [modifiedIndexPaths sortUsingSelector:@selector(compare:)]; - - for (NSIndexPath *indexPath in modifiedIndexPaths) { - ASDisplayNode *node = [_dataSource rangeController:self nodeAtIndexPath:indexPath]; - ASInterfaceState interfaceState = node.interfaceState; - BOOL inVisible = ASInterfaceStateIncludesVisible(interfaceState); - BOOL inDisplay = ASInterfaceStateIncludesDisplay(interfaceState); - BOOL inFetchData = ASInterfaceStateIncludesFetchData(interfaceState); - NSLog(@"indexPath %@, Visible: %d, Display: %d, FetchData: %d", indexPath, inVisible, inDisplay, inFetchData); - } + NSLog(@"Range update complete; modifiedIndexPaths: %@", [self descriptionWithIndexPaths:modifiedIndexPaths]); #endif } @@ -319,7 +329,7 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; { if (!_didRegisterForNotifications) { ASLayoutRangeMode nextRangeMode = [ASRangeController rangeModeForInterfaceState:interfaceState - currentRangeMode:_currentRangeMode]; + currentRangeMode:_currentRangeMode]; if (_currentRangeMode != nextRangeMode) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(scheduledNodesDidDisplay:) @@ -423,6 +433,31 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; }); } +#pragma mark - Memory Management + +// Skip the many method calls of the recursive operation if the top level cell node already has the right interfaceState. +- (void)clearContents +{ + for (NSArray *section in [_dataSource completedNodes]) { + for (ASDisplayNode *node in section) { + if (ASInterfaceStateIncludesDisplay(node.interfaceState)) { + [node exitInterfaceState:ASInterfaceStateDisplay]; + } + } + } +} + +- (void)clearFetchedData +{ + for (NSArray *section in [_dataSource completedNodes]) { + for (ASDisplayNode *node in section) { + if (ASInterfaceStateIncludesFetchData(node.interfaceState)) { + [node exitInterfaceState:ASInterfaceStateFetchData]; + } + } + } +} + #pragma mark - Class Methods (Application Notification Handlers) + (ASWeakSet *)allRangeControllersWeakSet @@ -446,36 +481,80 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; [center addObserver:self selector:@selector(willEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; } +static ASLayoutRangeMode __rangeModeForMemoryWarnings = ASLayoutRangeModeVisibleOnly; ++ (void)setRangeModeForMemoryWarnings:(ASLayoutRangeMode)rangeMode +{ + ASDisplayNodeAssert(rangeMode == ASLayoutRangeModeVisibleOnly || rangeMode == ASLayoutRangeModeLowMemory, @"It is highly inadvisable to engage a larger range mode when a memory warning occurs, as this will almost certainly cause app eviction"); + __rangeModeForMemoryWarnings = rangeMode; +} + + (void)didReceiveMemoryWarning:(NSNotification *)notification { +#if ASRangeControllerLoggingEnabled + NSLog(@"+[ASRangeController didReceiveMemoryWarning] with controllers: %@", [self allRangeControllersWeakSet]); +#endif for (ASRangeController *rangeController in [self allRangeControllersWeakSet]) { - if (rangeController.dataSource == nil) { - continue; - } - - ASInterfaceState interfaceState = [rangeController.dataSource interfaceStateForRangeController:rangeController]; - if (ASInterfaceStateIncludesDisplay(interfaceState)) { - continue; - } - - [rangeController updateCurrentRangeWithMode:ASLayoutRangeModeLowMemory]; + BOOL isDisplay = ASInterfaceStateIncludesDisplay([rangeController interfaceState]); + [rangeController updateCurrentRangeWithMode:isDisplay ? ASLayoutRangeModeMinimum : __rangeModeForMemoryWarnings]; + [rangeController performRangeUpdate]; } } + (void)didEnterBackground:(NSNotification *)notification { + for (ASRangeController *rangeController in [self allRangeControllersWeakSet]) { + // We do not want to fully collapse the Display ranges of any visible range controllers so that flashes can be avoided when + // the app is resumed. Non-visible controllers can be more aggressively culled to the LowMemory state (see definitions for documentation) + BOOL isVisible = ASInterfaceStateIncludesVisible([rangeController interfaceState]); + [rangeController updateCurrentRangeWithMode:isVisible ? ASLayoutRangeModeVisibleOnly : ASLayoutRangeModeLowMemory]; + } + + // Because -interfaceState checks __ApplicationState and always clears the "visible" bit if Backgrounded, we must set this after updating the range mode. __ApplicationState = UIApplicationStateBackground; for (ASRangeController *rangeController in [self allRangeControllersWeakSet]) { + // Trigger a range update immediately, as we may not be allowed by the system to run the update block scheduled by changing range mode. [rangeController performRangeUpdate]; } + +#if ASRangeControllerLoggingEnabled + NSLog(@"+[ASRangeController didEnterBackground] with controllers, after backgrounding: %@", [self allRangeControllersWeakSet]); +#endif } + (void)willEnterForeground:(NSNotification *)notification { __ApplicationState = UIApplicationStateActive; for (ASRangeController *rangeController in [self allRangeControllersWeakSet]) { + BOOL isVisible = ASInterfaceStateIncludesVisible([rangeController interfaceState]); + [rangeController updateCurrentRangeWithMode:isVisible ? ASLayoutRangeModeMinimum : ASLayoutRangeModeVisibleOnly]; [rangeController performRangeUpdate]; } + +#if ASRangeControllerLoggingEnabled + NSLog(@"+[ASRangeController willEnterForeground] with controllers, after foregrounding: %@", [self allRangeControllersWeakSet]); +#endif +} + +#pragma mark - Debugging + +- (NSString *)descriptionWithIndexPaths:(NSArray *)indexPaths +{ + NSMutableString *description = [NSMutableString stringWithFormat:@"%@ %@", [super description], @" allPreviousIndexPaths:\n"]; + for (NSIndexPath *indexPath in indexPaths) { + ASDisplayNode *node = [_dataSource rangeController:self nodeAtIndexPath:indexPath]; + ASInterfaceState interfaceState = node.interfaceState; + BOOL inVisible = ASInterfaceStateIncludesVisible(interfaceState); + BOOL inDisplay = ASInterfaceStateIncludesDisplay(interfaceState); + BOOL inFetchData = ASInterfaceStateIncludesFetchData(interfaceState); + [description appendFormat:@"indexPath %@, Visible: %d, Display: %d, FetchData: %d\n", indexPath, inVisible, inDisplay, inFetchData]; + } + return description; +} + +- (NSString *)description +{ + NSArray *indexPaths = [[_allPreviousIndexPaths allObjects] sortedArrayUsingSelector:@selector(compare:)]; + return [self descriptionWithIndexPaths:indexPaths]; } @end diff --git a/AsyncDisplayKit/Details/ASRangeControllerUpdateRangeProtocol+Beta.h b/AsyncDisplayKit/Details/ASRangeControllerUpdateRangeProtocol+Beta.h index 209dd761..1a36e576 100644 --- a/AsyncDisplayKit/Details/ASRangeControllerUpdateRangeProtocol+Beta.h +++ b/AsyncDisplayKit/Details/ASRangeControllerUpdateRangeProtocol+Beta.h @@ -20,6 +20,15 @@ */ - (void)updateCurrentRangeWithMode:(ASLayoutRangeMode)rangeMode; +/** + * Only ASLayoutRangeModeVisibleOnly or ASLayoutRangeModeLowMemory are recommended. Default is ASLayoutRangeModeVisibleOnly, + * because this is the only way to ensure an application will not have blank / flashing views as the user navigates back after + * a memory warning. Apps that wish to use the more effective / aggressive ASLayoutRangeModeLowMemory may need to take steps + * to mitigate this behavior, including: restoring a larger range mode to the next controller before the user navigates there, + * enabling .neverShowPlaceholders on ASCellNodes so that the navigation operation is blocked on redisplay completing, etc. + */ ++ (void)setRangeModeForMemoryWarnings:(ASLayoutRangeMode)rangeMode; + @end diff --git a/AsyncDisplayKit/Private/ASWeakSet.m b/AsyncDisplayKit/Private/ASWeakSet.m index 9ac3020e..516d0563 100644 --- a/AsyncDisplayKit/Private/ASWeakSet.m +++ b/AsyncDisplayKit/Private/ASWeakSet.m @@ -73,4 +73,9 @@ return [_mapTable countByEnumeratingWithState:state objects:buffer count:len]; } +- (NSString *)description +{ + return [[super description] stringByAppendingFormat:@" count: %lu, contents: %@", self.count, _mapTable]; +} + @end diff --git a/AsyncDisplayKit/Private/_ASPendingState.mm b/AsyncDisplayKit/Private/_ASPendingState.mm index ddb827f2..51aa345e 100644 --- a/AsyncDisplayKit/Private/_ASPendingState.mm +++ b/AsyncDisplayKit/Private/_ASPendingState.mm @@ -291,6 +291,11 @@ static UIColor *defaultTintColor = nil; - (void)setBounds:(CGRect)newBounds { + ASDisplayNodeAssert(!isnan(newBounds.size.width) && !isnan(newBounds.size.height), @"Invalid bounds %@ provided to %@", NSStringFromCGRect(newBounds), self); + if (isnan(newBounds.size.width)) + newBounds.size.width = 0.0; + if (isnan(newBounds.size.height)) + newBounds.size.height = 0.0; bounds = newBounds; _flags.setBounds = YES; } @@ -359,6 +364,11 @@ static UIColor *defaultTintColor = nil; - (void)setPosition:(CGPoint)newPosition { + ASDisplayNodeAssert(!isnan(newPosition.x) && !isnan(newPosition.y), @"Invalid position %@ provided to %@", NSStringFromCGPoint(newPosition), self); + if (isnan(newPosition.x)) + newPosition.x = 0.0; + if (isnan(newPosition.y)) + newPosition.y = 0.0; position = newPosition; _flags.setPosition = YES; }