From 9adb6554fc5ca08058e55e28a0f870e50dcdc4d7 Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Sun, 4 Dec 2016 07:15:28 -0800 Subject: [PATCH 01/16] Add assertion against externally setting .image in specific ASImageNode subclasses --- AsyncDisplayKit.xcodeproj/project.pbxproj | 4 +++ AsyncDisplayKit/ASImageNode.mm | 5 ++++ AsyncDisplayKit/ASMultiplexImageNode.mm | 17 ++++++++---- AsyncDisplayKit/ASNetworkImageNode.mm | 26 ++++++++++++------- AsyncDisplayKit/ASVideoNode.mm | 3 ++- AsyncDisplayKit/Private/ASImageNode+Private.h | 22 ++++++++++++++++ .../ASMultiplexImageNodeTests.m | 8 +++++- .../ASNetworkImageNodeTests.m | 6 +++++ 8 files changed, 75 insertions(+), 16 deletions(-) create mode 100644 AsyncDisplayKit/Private/ASImageNode+Private.h diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 7e7bd1a9c4..afcf74a6ad 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -204,6 +204,7 @@ 6907C2591DC4ECFE00374C66 /* ASObjectDescriptionHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 6907C2571DC4ECFE00374C66 /* ASObjectDescriptionHelpers.m */; }; 6907C25A1DC4ECFE00374C66 /* ASObjectDescriptionHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 6907C2571DC4ECFE00374C66 /* ASObjectDescriptionHelpers.m */; }; 69127CFE1DD2B387004BF6E2 /* ASEventLog.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 696F01EA1DD2AF450049FBD5 /* ASEventLog.h */; }; + 69309D461DF3B1B50089FA48 /* ASImageNode+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 69309D451DF3B1B50089FA48 /* ASImageNode+Private.h */; }; 693117CE1DC7C72700DE4784 /* ASDisplayNode+Deprecated.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 683489271D70DE3400327501 /* ASDisplayNode+Deprecated.h */; }; 69527B121DC84292004785FB /* ASLayoutElementStylePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 69527B111DC84292004785FB /* ASLayoutElementStylePrivate.h */; }; 6959433E1D70815300B0EE1F /* ASDisplayNodeLayout.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6959433C1D70815300B0EE1F /* ASDisplayNodeLayout.mm */; }; @@ -1002,6 +1003,7 @@ 68FC85E81CE29C7D00EDD713 /* ASVisibilityProtocols.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASVisibilityProtocols.m; sourceTree = ""; }; 6907C2561DC4ECFE00374C66 /* ASObjectDescriptionHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASObjectDescriptionHelpers.h; sourceTree = ""; }; 6907C2571DC4ECFE00374C66 /* ASObjectDescriptionHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASObjectDescriptionHelpers.m; sourceTree = ""; }; + 69309D451DF3B1B50089FA48 /* ASImageNode+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASImageNode+Private.h"; sourceTree = ""; }; 69527B111DC84292004785FB /* ASLayoutElementStylePrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASLayoutElementStylePrivate.h; path = AsyncDisplayKit/Layout/ASLayoutElementStylePrivate.h; sourceTree = SOURCE_ROOT; }; 6959433C1D70815300B0EE1F /* ASDisplayNodeLayout.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDisplayNodeLayout.mm; sourceTree = ""; }; 6959433D1D70815300B0EE1F /* ASDisplayNodeLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDisplayNodeLayout.h; sourceTree = ""; }; @@ -1609,6 +1611,7 @@ 68B8A4DB1CBD911D007E4543 /* ASImageNode+AnimatedImagePrivate.h */, 058D0A0D195D050800B7D73C /* ASImageNode+CGExtras.h */, 058D0A0E195D050800B7D73C /* ASImageNode+CGExtras.m */, + 69309D451DF3B1B50089FA48 /* ASImageNode+Private.h */, ACF6ED431B17847A00DA7C62 /* ASInternalHelpers.h */, ACF6ED441B17847A00DA7C62 /* ASInternalHelpers.m */, 69C4CAF51DA3147000B1EC9B /* ASLayoutElementStylePrivate.h */, @@ -1857,6 +1860,7 @@ 34EFC7631B701CBF00AD841F /* ASCenterLayoutSpec.h in Headers */, 9C70F20C1CDBE9B6007D6C76 /* ASCollectionDataController.h in Headers */, 18C2ED7F1B9B7DE800F627B3 /* ASCollectionNode.h in Headers */, + 69309D461DF3B1B50089FA48 /* ASImageNode+Private.h in Headers */, 9C8898BD1C738BB800D6B02E /* ASTextKitFontSizeAdjuster.h in Headers */, B35061F51B010EFD0018CF92 /* ASCollectionView.h in Headers */, ACE87A2C1D73696800D7FF06 /* ASSectionContext.h in Headers */, diff --git a/AsyncDisplayKit/ASImageNode.mm b/AsyncDisplayKit/ASImageNode.mm index 2678a7e8a9..153c8d95c3 100644 --- a/AsyncDisplayKit/ASImageNode.mm +++ b/AsyncDisplayKit/ASImageNode.mm @@ -199,6 +199,11 @@ struct ASImageNodeDrawParameters { #pragma mark - Setter / Getter - (void)setImage:(UIImage *)image +{ + [self __setImage:image]; +} + +- (void)__setImage:(UIImage *)image { ASDN::MutexLocker l(__instanceLock__); if (!ASObjectIsEqual(_image, image)) { diff --git a/AsyncDisplayKit/ASMultiplexImageNode.mm b/AsyncDisplayKit/ASMultiplexImageNode.mm index f5bc472509..633562c873 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.mm +++ b/AsyncDisplayKit/ASMultiplexImageNode.mm @@ -11,16 +11,17 @@ #if TARGET_OS_IOS #import "ASMultiplexImageNode.h" +#import "ASImageNode+Private.h" #import #import "ASAvailability.h" #import "ASDisplayNode+Subclasses.h" #import "ASDisplayNode+FrameworkPrivate.h" +#import "ASDisplayNodeExtras.h" #import "ASLog.h" #import "ASPhotosFrameworkImageRequest.h" #import "ASEqualityHelpers.h" #import "ASInternalHelpers.h" -#import "ASDisplayNodeExtras.h" #if !AS_IOS8_SDK_OR_LATER #error ASMultiplexImageNode can be used on iOS 7, but must be linked against the iOS 8 SDK. @@ -233,7 +234,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent // setting this to nil makes the node fetch images the next time its display starts _loadedImageIdentifier = nil; - self.image = nil; + [self __setImage:nil]; } - (void)didEnterPreloadState @@ -325,6 +326,12 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent #pragma mark - Core +- (void)setImage:(UIImage *)image +{ + ASDisplayNodeAssert(NO, @"Setting the image directly to an ASMultiplexImageNode is not allowed."); + [self __setImage:image]; +} + - (void)setDelegate:(id )delegate { if (_delegate == delegate) @@ -520,7 +527,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent if (ASObjectIsEqual(strongSelf->_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) { return; } - strongSelf.image = progressImage; + [self __setImage:progressImage]; }; } [_downloader setProgressImageBlock:progress callbackQueue:dispatch_get_main_queue() withDownloadIdentifier:_downloadIdentifier]; @@ -538,7 +545,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent if (shouldReleaseImageOnBackgroundThread) { ASPerformBackgroundDeallocation(image); } - self.image = nil; + [self __setImage:nil]; } #pragma mark - @@ -867,7 +874,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent UIImage *previousImage = self.image; self.loadedImageIdentifier = imageIdentifier; - self.image = image; + [self __setImage:image]; if (_delegateFlags.updatedImage) { [_delegate multiplexImageNode:self didUpdateImage:image withIdentifier:imageIdentifier fromImage:previousImage withIdentifier:previousIdentifier]; diff --git a/AsyncDisplayKit/ASNetworkImageNode.mm b/AsyncDisplayKit/ASNetworkImageNode.mm index bb106aa4e6..7ab5b7dc6a 100755 --- a/AsyncDisplayKit/ASNetworkImageNode.mm +++ b/AsyncDisplayKit/ASNetworkImageNode.mm @@ -9,15 +9,16 @@ // #import "ASNetworkImageNode.h" +#import "ASImageNode+Private.h" #import "ASBasicImageDownloader.h" #import "ASDisplayNodeInternal.h" +#import "ASDisplayNodeExtras.h" #import "ASDisplayNode+Subclasses.h" #import "ASDisplayNode+FrameworkPrivate.h" #import "ASEqualityHelpers.h" #import "ASInternalHelpers.h" #import "ASImageContainerProtocolCategories.h" -#import "ASDisplayNodeExtras.h" #if PIN_REMOTE_IMAGE #import "ASPINRemoteImageDownloader.h" @@ -67,6 +68,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; unsigned int cacheSupportsSynchronousFetch:1; } _cacheFlags; } + @end @implementation ASNetworkImageNode @@ -116,6 +118,12 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; #pragma mark - Public methods -- must lock +- (void)setImage:(UIImage *)image +{ + ASDisplayNodeAssert(NO, @"Setting the image directly to an ASNetworkImageNode is not allowed. Please either use the defaultImage property or move to an ASImageNode"); + [self __setImage:image]; +} + - (void)setURL:(NSURL *)URL { [self setURL:URL resetToDefault:YES]; @@ -136,7 +144,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; BOOL hasURL = _URL == nil; if (reset || hasURL) { - self.image = _defaultImage; + [self __setImage:_defaultImage]; /* We want to maintain the order that currentImageQuality is set regardless of the calling thread, so always use a dispatch_async to ensure that we queue the operations in the correct order. (see comment in displayDidFinish) */ @@ -171,7 +179,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; dispatch_async(dispatch_get_main_queue(), ^{ self.currentImageQuality = hasURL ? 0.0 : 1.0; }); - self.image = defaultImage; + [self __setImage:defaultImage]; } } @@ -256,7 +264,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; if (_imageLoaded == NO && _URL && _downloadIdentifier == nil) { UIImage *result = [[_cache synchronouslyFetchedCachedImageWithURL:_URL] asdk_image]; if (result) { - self.image = result; + [self __setImage:result]; _imageLoaded = YES; dispatch_async(dispatch_get_main_queue(), ^{ _currentImageQuality = 1.0; @@ -340,7 +348,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; if (ASObjectIsEqual(_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) { return; } - self.image = progressImage; + [self __setImage:progressImage]; dispatch_async(dispatch_get_main_queue(), ^{ // See comment in -displayDidFinish for why this must be dispatched to main self.currentImageQuality = progress; @@ -396,7 +404,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; ASPerformBackgroundDeallocation(image); } self.animatedImage = nil; - self.image = _defaultImage; + [self __setImage:_defaultImage]; _imageLoaded = NO; // See comment in -displayDidFinish for why this must be dispatched to main dispatch_async(dispatch_get_main_queue(), ^{ @@ -456,7 +464,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; dispatch_async(dispatch_get_main_queue(), ^{ if (self.shouldCacheImage) { - self.image = [UIImage imageNamed:_URL.path.lastPathComponent]; + [self __setImage:[UIImage imageNamed:_URL.path.lastPathComponent]]; } else { // First try to load the path directly, for efficiency assuming a developer who // doesn't want caching is trying to be as minimal as possible. @@ -486,7 +494,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; if (animatedImage != nil) { self.animatedImage = animatedImage; } else { - self.image = nonAnimatedImage; + [self __setImage:nonAnimatedImage]; } } @@ -522,7 +530,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; if ([imageContainer asdk_animatedImageData] && _downloaderFlags.downloaderImplementsAnimatedImage) { strongSelf.animatedImage = [_downloader animatedImageWithData:[imageContainer asdk_animatedImageData]]; } else { - strongSelf.image = [imageContainer asdk_image]; + [strongSelf __setImage:[imageContainer asdk_image]]; } dispatch_async(dispatch_get_main_queue(), ^{ strongSelf->_currentImageQuality = 1.0; diff --git a/AsyncDisplayKit/ASVideoNode.mm b/AsyncDisplayKit/ASVideoNode.mm index 878189deb3..5cd4447af2 100644 --- a/AsyncDisplayKit/ASVideoNode.mm +++ b/AsyncDisplayKit/ASVideoNode.mm @@ -16,6 +16,7 @@ #import "ASEqualityHelpers.h" #import "ASInternalHelpers.h" #import "ASDisplayNodeExtras.h" +#import "ASImageNode+Private.h" static BOOL ASAssetIsEqual(AVAsset *asset1, AVAsset *asset2) { return ASObjectIsEqual(asset1, asset2) @@ -300,7 +301,7 @@ static NSString * const kRate = @"rate"; if (image != nil) { self.contentMode = ASContentModeFromVideoGravity(_gravity); } - self.image = image; + [self __setImage:image]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context diff --git a/AsyncDisplayKit/Private/ASImageNode+Private.h b/AsyncDisplayKit/Private/ASImageNode+Private.h new file mode 100644 index 0000000000..d380953dbb --- /dev/null +++ b/AsyncDisplayKit/Private/ASImageNode+Private.h @@ -0,0 +1,22 @@ +// +// ASImageNode+Private.h +// AsyncDisplayKit +// +// Created by Michael Schneider on 12/3/16. +// Copyright © 2016 Facebook. All rights reserved. +// + +#pragma mark - ASImageNode + +#import "ASImageNode.h" + +@interface ASImageNode (Private) + +/* + * Set the image property of the ASImageNode. Subclasses like ASNetworkImageNode do not allow setting the + * image property directly and throw an assertion. There still needs to be a way for subclasses of + * ASNetworkImageNode to set the image. + */ +- (void)__setImage:(UIImage *)image; + +@end diff --git a/AsyncDisplayKitTests/ASMultiplexImageNodeTests.m b/AsyncDisplayKitTests/ASMultiplexImageNodeTests.m index 4fec68aadf..49a424b5c9 100644 --- a/AsyncDisplayKitTests/ASMultiplexImageNodeTests.m +++ b/AsyncDisplayKitTests/ASMultiplexImageNodeTests.m @@ -302,4 +302,10 @@ [mockDelegate verify]; } -@end \ No newline at end of file +- (void)testThatSettingAnImageExternallyWillThrow +{ + ASMultiplexImageNode *multiplexImageNode = [[ASMultiplexImageNode alloc] init]; + XCTAssertThrows(multiplexImageNode.image = [UIImage imageNamed:@""]); +} + +@end diff --git a/AsyncDisplayKitTests/ASNetworkImageNodeTests.m b/AsyncDisplayKitTests/ASNetworkImageNodeTests.m index b7086e6c85..8ea8ee8c5e 100644 --- a/AsyncDisplayKitTests/ASNetworkImageNodeTests.m +++ b/AsyncDisplayKitTests/ASNetworkImageNodeTests.m @@ -71,6 +71,12 @@ [downloader verifyWithDelay:5]; } +- (void)testThatSettingAnImageExternallyWillThrow +{ + ASNetworkImageNode *networkImageNode = [[ASNetworkImageNode alloc] init]; + XCTAssertThrows(networkImageNode.image = [UIImage imageNamed:@""]); +} + @end @implementation ASTestImageCache From 143abdaa96a08c4160027092f5ec20dc9b3fa0dd Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Sun, 4 Dec 2016 08:04:49 -0800 Subject: [PATCH 02/16] Change Header of ASImageNode+Private --- AsyncDisplayKit/Private/ASImageNode+Private.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/AsyncDisplayKit/Private/ASImageNode+Private.h b/AsyncDisplayKit/Private/ASImageNode+Private.h index d380953dbb..96271db52f 100644 --- a/AsyncDisplayKit/Private/ASImageNode+Private.h +++ b/AsyncDisplayKit/Private/ASImageNode+Private.h @@ -2,8 +2,10 @@ // ASImageNode+Private.h // AsyncDisplayKit // -// Created by Michael Schneider on 12/3/16. -// Copyright © 2016 Facebook. All rights reserved. +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the root directory of this source tree. An additional grant +// of patent rights can be found in the PATENTS file in the same directory. // #pragma mark - ASImageNode From 70b647c2b9816282c1a77874c4afc03a1e544f68 Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Mon, 5 Dec 2016 15:10:16 -0800 Subject: [PATCH 03/16] Add support for setting a static image to ASNetworkImageNode --- AsyncDisplayKit/ASNetworkImageNode.mm | 69 ++++++++++++++----- .../ASNetworkImageNodeTests.m | 9 ++- 2 files changed, 60 insertions(+), 18 deletions(-) diff --git a/AsyncDisplayKit/ASNetworkImageNode.mm b/AsyncDisplayKit/ASNetworkImageNode.mm index 7ab5b7dc6a..93ee34ac86 100755 --- a/AsyncDisplayKit/ASNetworkImageNode.mm +++ b/AsyncDisplayKit/ASNetworkImageNode.mm @@ -43,6 +43,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; id _downloadIdentifierForProgressBlock; BOOL _imageLoaded; + BOOL _imageWasSetExternally; CGFloat _currentImageQuality; CGFloat _renderedImageQuality; BOOL _shouldRenderProgressImages; @@ -120,7 +121,25 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; - (void)setImage:(UIImage *)image { - ASDisplayNodeAssert(NO, @"Setting the image directly to an ASNetworkImageNode is not allowed. Please either use the defaultImage property or move to an ASImageNode"); + __instanceLock__.lock(); + _imageWasSetExternally = (image != nil); + if (_imageWasSetExternally) { + [self __cancelDownloadAndClearImage]; + } + __instanceLock__.unlock(); + + [self __setImage:image]; +} + +- (void)_setImage:(UIImage *)image +{ + __instanceLock__.lock(); + if (_imageWasSetExternally) { + __instanceLock__.unlock(); + return; + } + __instanceLock__.unlock(); + [self __setImage:image]; } @@ -133,7 +152,13 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; { ASDN::MutexLocker l(__instanceLock__); - if (ASObjectIsEqual(URL, _URL)) { +#ifdef DEBUG + if (_imageWasSetExternally) { + NSLog(@"Image was already set via the .image property. Setting an image explicitly and setting an URL is not supported."); + } +#endif + + if (ASObjectIsEqual(URL, _URL) || _imageWasSetExternally) { return; } @@ -144,7 +169,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; BOOL hasURL = _URL == nil; if (reset || hasURL) { - [self __setImage:_defaultImage]; + [self _setImage:_defaultImage]; /* We want to maintain the order that currentImageQuality is set regardless of the calling thread, so always use a dispatch_async to ensure that we queue the operations in the correct order. (see comment in displayDidFinish) */ @@ -179,7 +204,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; dispatch_async(dispatch_get_main_queue(), ^{ self.currentImageQuality = hasURL ? 0.0 : 1.0; }); - [self __setImage:defaultImage]; + [self _setImage:defaultImage]; } } @@ -264,7 +289,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; if (_imageLoaded == NO && _URL && _downloadIdentifier == nil) { UIImage *result = [[_cache synchronouslyFetchedCachedImageWithURL:_URL] asdk_image]; if (result) { - [self __setImage:result]; + [self _setImage:result]; _imageLoaded = YES; dispatch_async(dispatch_get_main_queue(), ^{ _currentImageQuality = 1.0; @@ -320,12 +345,12 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; { ASDN::MutexLocker l(__instanceLock__); - - [self _cancelImageDownload]; - [self _clearImage]; - if (_cacheFlags.cacheSupportsClearing) { - [_cache clearFetchedImageFromCacheWithURL:_URL]; + // If the image was set explicitly we don't want to remove it while exiting the preload state + if (_imageWasSetExternally) { + return; } + + [self __cancelDownloadAndClearImage]; } } @@ -335,7 +360,10 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; { ASDN::MutexLocker l(__instanceLock__); - [self _lazilyLoadImageIfNecessary]; + // Image was set externally no need to load an image + if (_imageWasSetExternally == NO) { + [self _lazilyLoadImageIfNecessary]; + } } } @@ -348,7 +376,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; if (ASObjectIsEqual(_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) { return; } - [self __setImage:progressImage]; + [self _setImage:progressImage]; dispatch_async(dispatch_get_main_queue(), ^{ // See comment in -displayDidFinish for why this must be dispatched to main self.currentImageQuality = progress; @@ -391,6 +419,15 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; _downloadIdentifierForProgressBlock = newDownloadIDForProgressBlock; } +- (void)__cancelDownloadAndClearImage +{ + [self _cancelImageDownload]; + [self _clearImage]; + if (_cacheFlags.cacheSupportsClearing) { + [_cache clearFetchedImageFromCacheWithURL:_URL]; + } +} + - (void)_clearImage { // Destruction of bigger images on the main thread can be expensive @@ -404,7 +441,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; ASPerformBackgroundDeallocation(image); } self.animatedImage = nil; - [self __setImage:_defaultImage]; + [self _setImage:_defaultImage]; _imageLoaded = NO; // See comment in -displayDidFinish for why this must be dispatched to main dispatch_async(dispatch_get_main_queue(), ^{ @@ -464,7 +501,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; dispatch_async(dispatch_get_main_queue(), ^{ if (self.shouldCacheImage) { - [self __setImage:[UIImage imageNamed:_URL.path.lastPathComponent]]; + [self _setImage:[UIImage imageNamed:_URL.path.lastPathComponent]]; } else { // First try to load the path directly, for efficiency assuming a developer who // doesn't want caching is trying to be as minimal as possible. @@ -494,7 +531,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; if (animatedImage != nil) { self.animatedImage = animatedImage; } else { - [self __setImage:nonAnimatedImage]; + [self _setImage:nonAnimatedImage]; } } @@ -530,7 +567,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; if ([imageContainer asdk_animatedImageData] && _downloaderFlags.downloaderImplementsAnimatedImage) { strongSelf.animatedImage = [_downloader animatedImageWithData:[imageContainer asdk_animatedImageData]]; } else { - [strongSelf __setImage:[imageContainer asdk_image]]; + [strongSelf _setImage:[imageContainer asdk_image]]; } dispatch_async(dispatch_get_main_queue(), ^{ strongSelf->_currentImageQuality = 1.0; diff --git a/AsyncDisplayKitTests/ASNetworkImageNodeTests.m b/AsyncDisplayKitTests/ASNetworkImageNodeTests.m index 8ea8ee8c5e..18896487b1 100644 --- a/AsyncDisplayKitTests/ASNetworkImageNodeTests.m +++ b/AsyncDisplayKitTests/ASNetworkImageNodeTests.m @@ -71,10 +71,15 @@ [downloader verifyWithDelay:5]; } -- (void)testThatSettingAnImageExternallyWillThrow +- (void)testThatSettingAnImageWillStayForEnteringAndExitingPreloadState { + UIImage *image = [[UIImage alloc] init]; ASNetworkImageNode *networkImageNode = [[ASNetworkImageNode alloc] init]; - XCTAssertThrows(networkImageNode.image = [UIImage imageNamed:@""]); + networkImageNode.image = image; + [networkImageNode enterInterfaceState:ASInterfaceStatePreload]; + XCTAssertEqualObjects(image, networkImageNode.image); + [networkImageNode exitInterfaceState:ASInterfaceStatePreload]; + XCTAssertEqualObjects(image, networkImageNode.image); } @end From 9fefd4a5b03d9c57a2c37536de71902d6ae7d9b6 Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Mon, 5 Dec 2016 15:10:22 -0800 Subject: [PATCH 04/16] Cleanup ASVideoNode a bit --- AsyncDisplayKit/ASVideoNode.mm | 41 +++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/AsyncDisplayKit/ASVideoNode.mm b/AsyncDisplayKit/ASVideoNode.mm index 5cd4447af2..fedaf02f34 100644 --- a/AsyncDisplayKit/ASVideoNode.mm +++ b/AsyncDisplayKit/ASVideoNode.mm @@ -90,7 +90,8 @@ static NSString * const kRate = @"rate"; // TODO: Support preview images with HTTP Live Streaming videos. -#pragma mark - Construction and Layout +#pragma mark - Lifecycle + - (instancetype)init { @@ -228,6 +229,22 @@ static NSString * const kRate = @"rate"; } } +- (void)_fetchDataForVideoNode +{ + [self setNeedsPreload]; +} + +- (void)_clearVideoNode +{ + self.videoPlaceholderImage = nil; + self.player = nil; + self.currentItem = nil; + self.playerState = ASVideoNodePlayerStateUnknown; +} + +#pragma mark - Layout + + - (void)layout { [super layout]; @@ -301,7 +318,7 @@ static NSString * const kRate = @"rate"; if (image != nil) { self.contentMode = ASContentModeFromVideoGravity(_gravity); } - [self __setImage:image]; + self.image = image; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context @@ -413,10 +430,7 @@ static NSString * const kRate = @"rate"; { ASDN::MutexLocker l(__instanceLock__); - - self.player = nil; - self.currentItem = nil; - self.playerState = ASVideoNodePlayerStateUnknown; + [self _clearVideoNode]; } } @@ -507,10 +521,15 @@ static NSString * const kRate = @"rate"; - (void)_setAndFetchAsset:(AVAsset *)asset url:(NSURL *)assetURL { - [self didExitPreloadState]; - _asset = asset; - _assetURL = assetURL; - [self setNeedsPreload]; + [self _clearVideoNode]; + + { + ASDN::MutexLocker l(__instanceLock__); + _asset = asset; + _assetURL = assetURL; + } + + [self _fetchDataForVideoNode]; } - (void)setVideoComposition:(AVVideoComposition *)videoComposition @@ -619,7 +638,7 @@ static NSString * const kRate = @"rate"; } if (_player == nil) { - [self setNeedsPreload]; + [self _fetchDataForVideoNode]; } if (_playerNode == nil) { From d57e33b2f0db7b564692040e22cf3b477ea7ddd4 Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Mon, 5 Dec 2016 15:23:40 -0800 Subject: [PATCH 05/16] Some further improvements --- AsyncDisplayKit/ASMultiplexImageNode.mm | 2 +- AsyncDisplayKit/ASNetworkImageNode.mm | 11 ++++++++++- AsyncDisplayKit/Private/ASImageNode+Private.h | 3 +++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/AsyncDisplayKit/ASMultiplexImageNode.mm b/AsyncDisplayKit/ASMultiplexImageNode.mm index 633562c873..a406050009 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.mm +++ b/AsyncDisplayKit/ASMultiplexImageNode.mm @@ -328,7 +328,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent - (void)setImage:(UIImage *)image { - ASDisplayNodeAssert(NO, @"Setting the image directly to an ASMultiplexImageNode is not allowed."); + ASDisplayNodeAssert(NO, @"Setting the image directly on an ASMultiplexImageNode is unsafe. It will be cleared in didExitPreloadRange and will have no way to restore in didEnterPreloadRange"); [self __setImage:image]; } diff --git a/AsyncDisplayKit/ASNetworkImageNode.mm b/AsyncDisplayKit/ASNetworkImageNode.mm index 93ee34ac86..4f744e747c 100755 --- a/AsyncDisplayKit/ASNetworkImageNode.mm +++ b/AsyncDisplayKit/ASNetworkImageNode.mm @@ -119,6 +119,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; #pragma mark - Public methods -- must lock +/// Setter for public image property. It has the side effect to set an internal _imageWasSetExternally that prevents setting an image internally. Setting an image internally should happen with the _setImage: method - (void)setImage:(UIImage *)image { __instanceLock__.lock(); @@ -131,9 +132,17 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; [self __setImage:image]; } +/// Internal image setter that will first check if an image already was set externally and will return otherwise will set it - (void)_setImage:(UIImage *)image { __instanceLock__.lock(); + +#ifdef DEBUG + if (_URL != nil) { + NSLog(@"Setting the image directly on an %@ and setting an URL is not supported. If you decide to set an image direclty this node will work the same ways as an plain ASImageNode and not consider the image loaded via URL.", NSStringFromClass([self class])); + } +#endif + if (_imageWasSetExternally) { __instanceLock__.unlock(); return; @@ -154,7 +163,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; #ifdef DEBUG if (_imageWasSetExternally) { - NSLog(@"Image was already set via the .image property. Setting an image explicitly and setting an URL is not supported."); + NSLog(@"Setting the image directly on an %@ and setting an URL is not supported. If you decide to set an image direclty this node will work the same ways as an plain ASImageNode and not consider the image loaded via URL.", NSStringFromClass([self class])); } #endif diff --git a/AsyncDisplayKit/Private/ASImageNode+Private.h b/AsyncDisplayKit/Private/ASImageNode+Private.h index 96271db52f..676af72ec0 100644 --- a/AsyncDisplayKit/Private/ASImageNode+Private.h +++ b/AsyncDisplayKit/Private/ASImageNode+Private.h @@ -18,6 +18,9 @@ * Set the image property of the ASImageNode. Subclasses like ASNetworkImageNode do not allow setting the * image property directly and throw an assertion. There still needs to be a way for subclasses of * ASNetworkImageNode to set the image. + * + * This is exposed to library subclasses, i.e. ASNetworkImageNode, ASMultiplexImageNode and ASVideoNode for setting + * the image directly without going throug the setter of the superclass */ - (void)__setImage:(UIImage *)image; From 018ad148bc6ce3f3fbfadeb74b8feb5f8a072ca7 Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Mon, 5 Dec 2016 16:50:58 -0800 Subject: [PATCH 06/16] Further work --- AsyncDisplayKit/ASNetworkImageNode.mm | 45 ++++++++++----------------- 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/AsyncDisplayKit/ASNetworkImageNode.mm b/AsyncDisplayKit/ASNetworkImageNode.mm index 4f744e747c..9278885664 100755 --- a/AsyncDisplayKit/ASNetworkImageNode.mm +++ b/AsyncDisplayKit/ASNetworkImageNode.mm @@ -121,31 +121,19 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; /// Setter for public image property. It has the side effect to set an internal _imageWasSetExternally that prevents setting an image internally. Setting an image internally should happen with the _setImage: method - (void)setImage:(UIImage *)image -{ - __instanceLock__.lock(); - _imageWasSetExternally = (image != nil); - if (_imageWasSetExternally) { - [self __cancelDownloadAndClearImage]; - } - __instanceLock__.unlock(); - - [self __setImage:image]; -} - -/// Internal image setter that will first check if an image already was set externally and will return otherwise will set it -- (void)_setImage:(UIImage *)image { __instanceLock__.lock(); #ifdef DEBUG if (_URL != nil) { - NSLog(@"Setting the image directly on an %@ and setting an URL is not supported. If you decide to set an image direclty this node will work the same ways as an plain ASImageNode and not consider the image loaded via URL.", NSStringFromClass([self class])); + NSLog(@"Setting the image directly on an %@ and setting and setting an URL is not supported. If you want to use a placeholder image please use defaultImage .", NSStringFromClass([self class])); } #endif + _imageWasSetExternally = (image != nil); if (_imageWasSetExternally) { - __instanceLock__.unlock(); - return; + [self __cancelDownloadAndClearImage]; + _URL = nil; } __instanceLock__.unlock(); @@ -163,11 +151,12 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; #ifdef DEBUG if (_imageWasSetExternally) { - NSLog(@"Setting the image directly on an %@ and setting an URL is not supported. If you decide to set an image direclty this node will work the same ways as an plain ASImageNode and not consider the image loaded via URL.", NSStringFromClass([self class])); + NSLog(@"Setting the image directly on an %@ and setting and setting an URL is not supported. If you want to use a placeholder image please use defaultImage .", NSStringFromClass([self class])); } #endif + _imageWasSetExternally = NO; - if (ASObjectIsEqual(URL, _URL) || _imageWasSetExternally) { + if (ASObjectIsEqual(URL, _URL)) { return; } @@ -178,7 +167,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; BOOL hasURL = _URL == nil; if (reset || hasURL) { - [self _setImage:_defaultImage]; + [self __setImage:_defaultImage]; /* We want to maintain the order that currentImageQuality is set regardless of the calling thread, so always use a dispatch_async to ensure that we queue the operations in the correct order. (see comment in displayDidFinish) */ @@ -213,7 +202,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; dispatch_async(dispatch_get_main_queue(), ^{ self.currentImageQuality = hasURL ? 0.0 : 1.0; }); - [self _setImage:defaultImage]; + [self __setImage:defaultImage]; } } @@ -298,7 +287,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; if (_imageLoaded == NO && _URL && _downloadIdentifier == nil) { UIImage *result = [[_cache synchronouslyFetchedCachedImageWithURL:_URL] asdk_image]; if (result) { - [self _setImage:result]; + [self __setImage:result]; _imageLoaded = YES; dispatch_async(dispatch_get_main_queue(), ^{ _currentImageQuality = 1.0; @@ -370,9 +359,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; { ASDN::MutexLocker l(__instanceLock__); // Image was set externally no need to load an image - if (_imageWasSetExternally == NO) { - [self _lazilyLoadImageIfNecessary]; - } + [self _lazilyLoadImageIfNecessary]; } } @@ -385,7 +372,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; if (ASObjectIsEqual(_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) { return; } - [self _setImage:progressImage]; + [self __setImage:progressImage]; dispatch_async(dispatch_get_main_queue(), ^{ // See comment in -displayDidFinish for why this must be dispatched to main self.currentImageQuality = progress; @@ -450,7 +437,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; ASPerformBackgroundDeallocation(image); } self.animatedImage = nil; - [self _setImage:_defaultImage]; + [self __setImage:_defaultImage]; _imageLoaded = NO; // See comment in -displayDidFinish for why this must be dispatched to main dispatch_async(dispatch_get_main_queue(), ^{ @@ -510,7 +497,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; dispatch_async(dispatch_get_main_queue(), ^{ if (self.shouldCacheImage) { - [self _setImage:[UIImage imageNamed:_URL.path.lastPathComponent]]; + [self __setImage:[UIImage imageNamed:_URL.path.lastPathComponent]]; } else { // First try to load the path directly, for efficiency assuming a developer who // doesn't want caching is trying to be as minimal as possible. @@ -540,7 +527,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; if (animatedImage != nil) { self.animatedImage = animatedImage; } else { - [self _setImage:nonAnimatedImage]; + [self __setImage:nonAnimatedImage]; } } @@ -576,7 +563,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; if ([imageContainer asdk_animatedImageData] && _downloaderFlags.downloaderImplementsAnimatedImage) { strongSelf.animatedImage = [_downloader animatedImageWithData:[imageContainer asdk_animatedImageData]]; } else { - [strongSelf _setImage:[imageContainer asdk_image]]; + [strongSelf __setImage:[imageContainer asdk_image]]; } dispatch_async(dispatch_get_main_queue(), ^{ strongSelf->_currentImageQuality = 1.0; From ef84e996730015fb14322768731dac2d9124e79b Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Mon, 5 Dec 2016 20:00:06 -0800 Subject: [PATCH 07/16] Tremendously make the implementation easier --- AsyncDisplayKit.xcodeproj/project.pbxproj | 4 -- AsyncDisplayKit/ASImageNode.mm | 5 --- AsyncDisplayKit/ASMultiplexImageNode.mm | 16 +++++--- AsyncDisplayKit/ASNetworkImageNode.mm | 24 ++++++----- AsyncDisplayKit/ASVideoNode.mm | 41 +++++-------------- AsyncDisplayKit/Private/ASImageNode+Private.h | 27 ------------ AsyncDisplayKitTests/ASVideoNodeTests.m | 1 - 7 files changed, 35 insertions(+), 83 deletions(-) delete mode 100644 AsyncDisplayKit/Private/ASImageNode+Private.h diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index afcf74a6ad..7e7bd1a9c4 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -204,7 +204,6 @@ 6907C2591DC4ECFE00374C66 /* ASObjectDescriptionHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 6907C2571DC4ECFE00374C66 /* ASObjectDescriptionHelpers.m */; }; 6907C25A1DC4ECFE00374C66 /* ASObjectDescriptionHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 6907C2571DC4ECFE00374C66 /* ASObjectDescriptionHelpers.m */; }; 69127CFE1DD2B387004BF6E2 /* ASEventLog.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 696F01EA1DD2AF450049FBD5 /* ASEventLog.h */; }; - 69309D461DF3B1B50089FA48 /* ASImageNode+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 69309D451DF3B1B50089FA48 /* ASImageNode+Private.h */; }; 693117CE1DC7C72700DE4784 /* ASDisplayNode+Deprecated.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 683489271D70DE3400327501 /* ASDisplayNode+Deprecated.h */; }; 69527B121DC84292004785FB /* ASLayoutElementStylePrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 69527B111DC84292004785FB /* ASLayoutElementStylePrivate.h */; }; 6959433E1D70815300B0EE1F /* ASDisplayNodeLayout.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6959433C1D70815300B0EE1F /* ASDisplayNodeLayout.mm */; }; @@ -1003,7 +1002,6 @@ 68FC85E81CE29C7D00EDD713 /* ASVisibilityProtocols.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASVisibilityProtocols.m; sourceTree = ""; }; 6907C2561DC4ECFE00374C66 /* ASObjectDescriptionHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASObjectDescriptionHelpers.h; sourceTree = ""; }; 6907C2571DC4ECFE00374C66 /* ASObjectDescriptionHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASObjectDescriptionHelpers.m; sourceTree = ""; }; - 69309D451DF3B1B50089FA48 /* ASImageNode+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASImageNode+Private.h"; sourceTree = ""; }; 69527B111DC84292004785FB /* ASLayoutElementStylePrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASLayoutElementStylePrivate.h; path = AsyncDisplayKit/Layout/ASLayoutElementStylePrivate.h; sourceTree = SOURCE_ROOT; }; 6959433C1D70815300B0EE1F /* ASDisplayNodeLayout.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDisplayNodeLayout.mm; sourceTree = ""; }; 6959433D1D70815300B0EE1F /* ASDisplayNodeLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASDisplayNodeLayout.h; sourceTree = ""; }; @@ -1611,7 +1609,6 @@ 68B8A4DB1CBD911D007E4543 /* ASImageNode+AnimatedImagePrivate.h */, 058D0A0D195D050800B7D73C /* ASImageNode+CGExtras.h */, 058D0A0E195D050800B7D73C /* ASImageNode+CGExtras.m */, - 69309D451DF3B1B50089FA48 /* ASImageNode+Private.h */, ACF6ED431B17847A00DA7C62 /* ASInternalHelpers.h */, ACF6ED441B17847A00DA7C62 /* ASInternalHelpers.m */, 69C4CAF51DA3147000B1EC9B /* ASLayoutElementStylePrivate.h */, @@ -1860,7 +1857,6 @@ 34EFC7631B701CBF00AD841F /* ASCenterLayoutSpec.h in Headers */, 9C70F20C1CDBE9B6007D6C76 /* ASCollectionDataController.h in Headers */, 18C2ED7F1B9B7DE800F627B3 /* ASCollectionNode.h in Headers */, - 69309D461DF3B1B50089FA48 /* ASImageNode+Private.h in Headers */, 9C8898BD1C738BB800D6B02E /* ASTextKitFontSizeAdjuster.h in Headers */, B35061F51B010EFD0018CF92 /* ASCollectionView.h in Headers */, ACE87A2C1D73696800D7FF06 /* ASSectionContext.h in Headers */, diff --git a/AsyncDisplayKit/ASImageNode.mm b/AsyncDisplayKit/ASImageNode.mm index 153c8d95c3..2678a7e8a9 100644 --- a/AsyncDisplayKit/ASImageNode.mm +++ b/AsyncDisplayKit/ASImageNode.mm @@ -199,11 +199,6 @@ struct ASImageNodeDrawParameters { #pragma mark - Setter / Getter - (void)setImage:(UIImage *)image -{ - [self __setImage:image]; -} - -- (void)__setImage:(UIImage *)image { ASDN::MutexLocker l(__instanceLock__); if (!ASObjectIsEqual(_image, image)) { diff --git a/AsyncDisplayKit/ASMultiplexImageNode.mm b/AsyncDisplayKit/ASMultiplexImageNode.mm index a406050009..4275516078 100644 --- a/AsyncDisplayKit/ASMultiplexImageNode.mm +++ b/AsyncDisplayKit/ASMultiplexImageNode.mm @@ -11,7 +11,6 @@ #if TARGET_OS_IOS #import "ASMultiplexImageNode.h" -#import "ASImageNode+Private.h" #import #import "ASAvailability.h" @@ -234,7 +233,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent // setting this to nil makes the node fetch images the next time its display starts _loadedImageIdentifier = nil; - [self __setImage:nil]; + [self _setImage:nil]; } - (void)didEnterPreloadState @@ -329,7 +328,12 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent - (void)setImage:(UIImage *)image { ASDisplayNodeAssert(NO, @"Setting the image directly on an ASMultiplexImageNode is unsafe. It will be cleared in didExitPreloadRange and will have no way to restore in didEnterPreloadRange"); - [self __setImage:image]; + super.image = image; +} + +- (void)_setImage:(UIImage *)image +{ + super.image = image; } - (void)setDelegate:(id )delegate @@ -527,7 +531,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent if (ASObjectIsEqual(strongSelf->_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) { return; } - [self __setImage:progressImage]; + [strongSelf _setImage:progressImage]; }; } [_downloader setProgressImageBlock:progress callbackQueue:dispatch_get_main_queue() withDownloadIdentifier:_downloadIdentifier]; @@ -545,7 +549,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent if (shouldReleaseImageOnBackgroundThread) { ASPerformBackgroundDeallocation(image); } - [self __setImage:nil]; + [self _setImage:nil]; } #pragma mark - @@ -874,7 +878,7 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent UIImage *previousImage = self.image; self.loadedImageIdentifier = imageIdentifier; - [self __setImage:image]; + [self _setImage:image]; if (_delegateFlags.updatedImage) { [_delegate multiplexImageNode:self didUpdateImage:image withIdentifier:imageIdentifier fromImage:previousImage withIdentifier:previousIdentifier]; diff --git a/AsyncDisplayKit/ASNetworkImageNode.mm b/AsyncDisplayKit/ASNetworkImageNode.mm index 9278885664..5b0d3d6cb4 100755 --- a/AsyncDisplayKit/ASNetworkImageNode.mm +++ b/AsyncDisplayKit/ASNetworkImageNode.mm @@ -9,7 +9,6 @@ // #import "ASNetworkImageNode.h" -#import "ASImageNode+Private.h" #import "ASBasicImageDownloader.h" #import "ASDisplayNodeInternal.h" @@ -137,7 +136,12 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; } __instanceLock__.unlock(); - [self __setImage:image]; + [self _setImage:image]; +} + +- (void)_setImage:(UIImage *)image +{ + super.image = image; } - (void)setURL:(NSURL *)URL @@ -167,7 +171,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; BOOL hasURL = _URL == nil; if (reset || hasURL) { - [self __setImage:_defaultImage]; + [self _setImage:_defaultImage]; /* We want to maintain the order that currentImageQuality is set regardless of the calling thread, so always use a dispatch_async to ensure that we queue the operations in the correct order. (see comment in displayDidFinish) */ @@ -202,7 +206,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; dispatch_async(dispatch_get_main_queue(), ^{ self.currentImageQuality = hasURL ? 0.0 : 1.0; }); - [self __setImage:defaultImage]; + [self _setImage:defaultImage]; } } @@ -287,7 +291,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; if (_imageLoaded == NO && _URL && _downloadIdentifier == nil) { UIImage *result = [[_cache synchronouslyFetchedCachedImageWithURL:_URL] asdk_image]; if (result) { - [self __setImage:result]; + [self _setImage:result]; _imageLoaded = YES; dispatch_async(dispatch_get_main_queue(), ^{ _currentImageQuality = 1.0; @@ -372,7 +376,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; if (ASObjectIsEqual(_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) { return; } - [self __setImage:progressImage]; + [self _setImage:progressImage]; dispatch_async(dispatch_get_main_queue(), ^{ // See comment in -displayDidFinish for why this must be dispatched to main self.currentImageQuality = progress; @@ -437,7 +441,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; ASPerformBackgroundDeallocation(image); } self.animatedImage = nil; - [self __setImage:_defaultImage]; + [self _setImage:_defaultImage]; _imageLoaded = NO; // See comment in -displayDidFinish for why this must be dispatched to main dispatch_async(dispatch_get_main_queue(), ^{ @@ -497,7 +501,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; dispatch_async(dispatch_get_main_queue(), ^{ if (self.shouldCacheImage) { - [self __setImage:[UIImage imageNamed:_URL.path.lastPathComponent]]; + [self _setImage:[UIImage imageNamed:_URL.path.lastPathComponent]]; } else { // First try to load the path directly, for efficiency assuming a developer who // doesn't want caching is trying to be as minimal as possible. @@ -527,7 +531,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; if (animatedImage != nil) { self.animatedImage = animatedImage; } else { - [self __setImage:nonAnimatedImage]; + [self _setImage:nonAnimatedImage]; } } @@ -563,7 +567,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; if ([imageContainer asdk_animatedImageData] && _downloaderFlags.downloaderImplementsAnimatedImage) { strongSelf.animatedImage = [_downloader animatedImageWithData:[imageContainer asdk_animatedImageData]]; } else { - [strongSelf __setImage:[imageContainer asdk_image]]; + [strongSelf _setImage:[imageContainer asdk_image]]; } dispatch_async(dispatch_get_main_queue(), ^{ strongSelf->_currentImageQuality = 1.0; diff --git a/AsyncDisplayKit/ASVideoNode.mm b/AsyncDisplayKit/ASVideoNode.mm index fedaf02f34..86e19ebe98 100644 --- a/AsyncDisplayKit/ASVideoNode.mm +++ b/AsyncDisplayKit/ASVideoNode.mm @@ -16,7 +16,6 @@ #import "ASEqualityHelpers.h" #import "ASInternalHelpers.h" #import "ASDisplayNodeExtras.h" -#import "ASImageNode+Private.h" static BOOL ASAssetIsEqual(AVAsset *asset1, AVAsset *asset2) { return ASObjectIsEqual(asset1, asset2) @@ -90,8 +89,7 @@ static NSString * const kRate = @"rate"; // TODO: Support preview images with HTTP Live Streaming videos. -#pragma mark - Lifecycle - +#pragma mark - Construction and Layout - (instancetype)init { @@ -229,22 +227,6 @@ static NSString * const kRate = @"rate"; } } -- (void)_fetchDataForVideoNode -{ - [self setNeedsPreload]; -} - -- (void)_clearVideoNode -{ - self.videoPlaceholderImage = nil; - self.player = nil; - self.currentItem = nil; - self.playerState = ASVideoNodePlayerStateUnknown; -} - -#pragma mark - Layout - - - (void)layout { [super layout]; @@ -430,7 +412,10 @@ static NSString * const kRate = @"rate"; { ASDN::MutexLocker l(__instanceLock__); - [self _clearVideoNode]; + + self.player = nil; + self.currentItem = nil; + self.playerState = ASVideoNodePlayerStateUnknown; } } @@ -521,15 +506,11 @@ static NSString * const kRate = @"rate"; - (void)_setAndFetchAsset:(AVAsset *)asset url:(NSURL *)assetURL { - [self _clearVideoNode]; - - { - ASDN::MutexLocker l(__instanceLock__); - _asset = asset; - _assetURL = assetURL; - } - - [self _fetchDataForVideoNode]; + [self setVideoPlaceholderImage:nil]; + [self didExitPreloadState]; + _asset = asset; + _assetURL = assetURL; + [self setNeedsPreload]; } - (void)setVideoComposition:(AVVideoComposition *)videoComposition @@ -638,7 +619,7 @@ static NSString * const kRate = @"rate"; } if (_player == nil) { - [self _fetchDataForVideoNode]; + [self setNeedsPreload]; } if (_playerNode == nil) { diff --git a/AsyncDisplayKit/Private/ASImageNode+Private.h b/AsyncDisplayKit/Private/ASImageNode+Private.h deleted file mode 100644 index 676af72ec0..0000000000 --- a/AsyncDisplayKit/Private/ASImageNode+Private.h +++ /dev/null @@ -1,27 +0,0 @@ -// -// ASImageNode+Private.h -// AsyncDisplayKit -// -// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. -// This source code is licensed under the BSD-style license found in the -// LICENSE file in the root directory of this source tree. An additional grant -// of patent rights can be found in the PATENTS file in the same directory. -// - -#pragma mark - ASImageNode - -#import "ASImageNode.h" - -@interface ASImageNode (Private) - -/* - * Set the image property of the ASImageNode. Subclasses like ASNetworkImageNode do not allow setting the - * image property directly and throw an assertion. There still needs to be a way for subclasses of - * ASNetworkImageNode to set the image. - * - * This is exposed to library subclasses, i.e. ASNetworkImageNode, ASMultiplexImageNode and ASVideoNode for setting - * the image directly without going throug the setter of the superclass - */ -- (void)__setImage:(UIImage *)image; - -@end diff --git a/AsyncDisplayKitTests/ASVideoNodeTests.m b/AsyncDisplayKitTests/ASVideoNodeTests.m index edbc57ece1..3c835c5e2f 100644 --- a/AsyncDisplayKitTests/ASVideoNodeTests.m +++ b/AsyncDisplayKitTests/ASVideoNodeTests.m @@ -410,7 +410,6 @@ [_videoNode didExitPreloadState]; XCTAssertNil(_videoNode.player); XCTAssertNil(_videoNode.currentItem); - XCTAssertNil(_videoNode.image); } - (void)testDelegateProperlySetForClassHierarchy From cacc36670617804623b44011d082ec81637d9348 Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Tue, 6 Dec 2016 06:55:47 -0800 Subject: [PATCH 08/16] Some video node improvements --- AsyncDisplayKit/ASVideoNode.mm | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/AsyncDisplayKit/ASVideoNode.mm b/AsyncDisplayKit/ASVideoNode.mm index 86e19ebe98..afd133a683 100644 --- a/AsyncDisplayKit/ASVideoNode.mm +++ b/AsyncDisplayKit/ASVideoNode.mm @@ -472,7 +472,7 @@ static NSString * const kRate = @"rate"; ASDN::MutexLocker l(__instanceLock__); if (ASObjectIsEqual(assetURL, self.assetURL) == NO) { - [self _setAndFetchAsset:[AVURLAsset assetWithURL:assetURL] url:assetURL]; + [self locked_setAndFetchAsset:[AVURLAsset assetWithURL:assetURL] url:assetURL]; } } @@ -494,7 +494,7 @@ static NSString * const kRate = @"rate"; ASDN::MutexLocker l(__instanceLock__); if (ASAssetIsEqual(asset, _asset) == NO) { - [self _setAndFetchAsset:asset url:nil]; + [self locked_setAndFetchAsset:asset url:nil]; } } @@ -504,10 +504,10 @@ static NSString * const kRate = @"rate"; return _asset; } -- (void)_setAndFetchAsset:(AVAsset *)asset url:(NSURL *)assetURL +- (void)locked_setAndFetchAsset:(AVAsset *)asset url:(NSURL *)assetURL { - [self setVideoPlaceholderImage:nil]; [self didExitPreloadState]; + self.videoPlaceholderImage = nil; _asset = asset; _assetURL = assetURL; [self setNeedsPreload]; From ee0cc9b1037d25626fa2275d7aa6e2d9ae33f335 Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Tue, 6 Dec 2016 14:06:56 -0800 Subject: [PATCH 09/16] Further logic and documentation improvements --- AsyncDisplayKit/ASNetworkImageNode.h | 24 ++++++++++--- AsyncDisplayKit/ASNetworkImageNode.mm | 50 +++++++++++---------------- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/AsyncDisplayKit/ASNetworkImageNode.h b/AsyncDisplayKit/ASNetworkImageNode.h index dddc0c426c..149f12b99b 100644 --- a/AsyncDisplayKit/ASNetworkImageNode.h +++ b/AsyncDisplayKit/ASNetworkImageNode.h @@ -26,7 +26,7 @@ NS_ASSUME_NONNULL_BEGIN @interface ASNetworkImageNode : ASImageNode /** - * The designated initializer. Cache and Downloader are WEAK references. + * The designated initializer. Cache and Downloader are WEAK references. * * @param cache The object that implements a cache of images for the image node. Weak reference. * @param downloader The object that implements image downloading for the image node. Must not be nil. Weak reference. @@ -38,7 +38,7 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithCache:(nullable id)cache downloader:(id)downloader NS_DESIGNATED_INITIALIZER; /** - * Convenience initialiser. + * Convenience initializer. * * @return An ASNetworkImageNode configured to use the NSURLSession-powered ASBasicImageDownloader, and no extra cache. */ @@ -49,6 +49,17 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nullable, nonatomic, weak, readwrite) id delegate; +/** + * The image to display. + * + * @discussion By setting an image to the image property the ASNetworkImageNode will act like a plain ASImageNode. + * As soon as the URL is set the ASNetworkImageNode will act like an ASNetworkImageNode and the image property + * will be managed internally. This means the image property will be cleared out and replaced by the placeholder + * () image while loading and the final image after the new image data was downloaded and processed. + * If you want to use a placholder image functionality use the defaultImage property instead. + */ +@property (nullable, nonatomic, strong) UIImage *image; + /** * A placeholder image to display while the URL is loading. */ @@ -57,7 +68,9 @@ NS_ASSUME_NONNULL_BEGIN /** * The URL of a new image to download and display. * - * @discussion Changing this property will reset the displayed image to a placeholder () while loading. + * @discussion By setting an URL, the image property of this node will be managed internally. This means previously + * directly set images to the image property will be cleared out and replaced by the placeholder () image + * while loading and the final image after the new image data was downloaded and processed. */ @property (nullable, nonatomic, strong, readwrite) NSURL *URL; @@ -65,8 +78,11 @@ NS_ASSUME_NONNULL_BEGIN * Download and display a new image. * * @param URL The URL of a new image to download and display. - * * @param reset Whether to display a placeholder () while loading the new image. + * + * @discussion By setting an URL, the image property of this node will be managed internally. This means previously + * directly set images to the image property will be cleared out and replaced by the placeholder () image + * while loading and the final image after the new image data was downloaded and processed. */ - (void)setURL:(nullable NSURL *)URL resetToDefault:(BOOL)reset; diff --git a/AsyncDisplayKit/ASNetworkImageNode.mm b/AsyncDisplayKit/ASNetworkImageNode.mm index 5b0d3d6cb4..87ab85031b 100755 --- a/AsyncDisplayKit/ASNetworkImageNode.mm +++ b/AsyncDisplayKit/ASNetworkImageNode.mm @@ -73,6 +73,8 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; @implementation ASNetworkImageNode +@dynamic image; + - (instancetype)initWithCache:(id)cache downloader:(id)downloader { if (!(self = [super init])) @@ -121,20 +123,13 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; /// Setter for public image property. It has the side effect to set an internal _imageWasSetExternally that prevents setting an image internally. Setting an image internally should happen with the _setImage: method - (void)setImage:(UIImage *)image { - __instanceLock__.lock(); - -#ifdef DEBUG - if (_URL != nil) { - NSLog(@"Setting the image directly on an %@ and setting and setting an URL is not supported. If you want to use a placeholder image please use defaultImage .", NSStringFromClass([self class])); - } -#endif + ASDN::MutexLocker l(__instanceLock__); _imageWasSetExternally = (image != nil); if (_imageWasSetExternally) { - [self __cancelDownloadAndClearImage]; + [self _cancelDownloadAndClearImage]; _URL = nil; } - __instanceLock__.unlock(); [self _setImage:image]; } @@ -153,11 +148,6 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; { ASDN::MutexLocker l(__instanceLock__); -#ifdef DEBUG - if (_imageWasSetExternally) { - NSLog(@"Setting the image directly on an %@ and setting and setting an URL is not supported. If you want to use a placeholder image please use defaultImage .", NSStringFromClass([self class])); - } -#endif _imageWasSetExternally = NO; if (ASObjectIsEqual(URL, _URL)) { @@ -352,7 +342,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; return; } - [self __cancelDownloadAndClearImage]; + [self _cancelDownloadAndClearImage]; } } @@ -419,7 +409,7 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; _downloadIdentifierForProgressBlock = newDownloadIDForProgressBlock; } -- (void)__cancelDownloadAndClearImage +- (void)_cancelDownloadAndClearImage { [self _cancelImageDownload]; [self _clearImage]; @@ -428,6 +418,20 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; } } +- (void)_cancelImageDownload +{ + if (!_downloadIdentifier) { + return; + } + + if (_downloadIdentifier) { + [_downloader cancelImageDownloadForIdentifier:_downloadIdentifier]; + } + _downloadIdentifier = nil; + + _cacheUUID = nil; +} + - (void)_clearImage { // Destruction of bigger images on the main thread can be expensive @@ -449,20 +453,6 @@ static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; }); } -- (void)_cancelImageDownload -{ - if (!_downloadIdentifier) { - return; - } - - if (_downloadIdentifier) { - [_downloader cancelImageDownloadForIdentifier:_downloadIdentifier]; - } - _downloadIdentifier = nil; - - _cacheUUID = nil; -} - - (void)_downloadImageWithCompletion:(void (^)(id imageContainer, NSError*, id downloadIdentifier))finished { ASPerformBlockOnBackgroundThread(^{ From 54eba883d8b93a3e443e0f51ef9308da7f0738ab Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Tue, 6 Dec 2016 15:11:00 -0800 Subject: [PATCH 10/16] Update to RC 2 (#2723) --- AsyncDisplayKit.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AsyncDisplayKit.podspec b/AsyncDisplayKit.podspec index f23bdaa276..ebb7a56d9a 100644 --- a/AsyncDisplayKit.podspec +++ b/AsyncDisplayKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = 'AsyncDisplayKit' - spec.version = '2.0-rc.1' + spec.version = '2.0-rc.2' spec.license = { :type => 'BSD' } spec.homepage = 'http://asyncdisplaykit.org' spec.authors = { 'Scott Goodson' => 'scottgoodson@gmail.com' } From c3cbd3b5836eeda5690466785f5c41b9b69b09f8 Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Tue, 6 Dec 2016 15:53:09 -0800 Subject: [PATCH 11/16] Just call super to change the size of the cell node that will resize itself instead of setting the frame directly (#2725) --- AsyncDisplayKit/ASCellNode.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AsyncDisplayKit/ASCellNode.mm b/AsyncDisplayKit/ASCellNode.mm index 7ddd50cab7..d99b2e2384 100644 --- a/AsyncDisplayKit/ASCellNode.mm +++ b/AsyncDisplayKit/ASCellNode.mm @@ -121,8 +121,8 @@ static NSMutableSet *__cellClassesForVisibilityNotifications = nil; // See +init - (void)_locked_displayNodeDidInvalidateSizeNewSize:(CGSize)newSize { CGSize oldSize = self.bounds.size; + [super _locked_displayNodeDidInvalidateSizeNewSize:newSize]; if (CGSizeEqualToSize(oldSize, newSize) == NO) { - self.frame = {self.frame.origin, newSize}; [self didRelayoutFromOldSize:oldSize toNewSize:newSize]; } } From beb98b448ec6f325d2f30c83851c8a2bbb5f131a Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Sun, 20 Nov 2016 21:13:37 -0800 Subject: [PATCH 12/16] [ASTextKit] Remove internal side effects related to constrainedSize. --- AsyncDisplayKit/ASTextNode.mm | 254 ++++++------------ AsyncDisplayKit/TextKit/ASTextKitAttributes.h | 3 +- AsyncDisplayKit/TextKit/ASTextKitContext.h | 2 - AsyncDisplayKit/TextKit/ASTextKitContext.mm | 12 - AsyncDisplayKit/TextKit/ASTextKitRenderer.mm | 77 ++---- .../ASTextNodeSnapshotTests.m | 7 + AsyncDisplayKitTests/ASTextNodeTests.m | 4 +- ...sIncludedWithSmallerConstrainedSize@2x.png | Bin 5993 -> 5918 bytes 8 files changed, 119 insertions(+), 240 deletions(-) diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index 9eb503a13e..5539a95116 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -50,6 +50,77 @@ struct ASTextNodeDrawParameter { UIColor *backgroundColor; }; +#pragma mark - ASTextKitRenderer + +// Not used at the moment but handy to have +/*ASDISPLAYNODE_INLINE NSUInteger ASHashFromCGRect(CGRect rect) +{ + return ((*(NSUInteger *)&rect.origin.x << 10 ^ *(NSUInteger *)&rect.origin.y) + (*(NSUInteger *)&rect.size.width << 10 ^ *(NSUInteger *)&rect.size.height)); +}*/ + +ASDISPLAYNODE_INLINE NSUInteger ASHashFromCGSize(CGSize size) +{ + return ((*(NSUInteger *)&size.width << 10 ^ *(NSUInteger *)&size.height)); +} + +@interface ASTextNodeRendererKey : NSObject +@property (assign, nonatomic) ASTextKitAttributes attributes; +@property (assign, nonatomic) CGSize constrainedSize; +@end + +@implementation ASTextNodeRendererKey + +- (NSUInteger)hash +{ + return _attributes.hash() ^ ASHashFromCGSize(_constrainedSize); +} + +- (BOOL)isEqual:(ASTextNodeRendererKey *)object +{ + if (self == object) { + return YES; + } + + return _attributes.hash() == object.attributes.hash() + && CGSizeEqualToSize(_constrainedSize, object.constrainedSize); +} + +@end + +static NSCache *sharedRendererCache() +{ + static dispatch_once_t onceToken; + static NSCache *__rendererCache = nil; + dispatch_once(&onceToken, ^{ + __rendererCache = [[NSCache alloc] init]; + __rendererCache.countLimit = 500; // 500 renders cache + }); + return __rendererCache; +} + +/** + The concept here is that neither the node nor layout should ever have a strong reference to the renderer object. + This is to reduce memory load when loading thousands and thousands of text nodes into memory at once. Instead + we maintain a LRU renderer cache that is queried via a unique key based on text kit attributes and constrained size. + */ + +static ASTextKitRenderer *rendererForAttributes(ASTextKitAttributes attributes, CGSize constrainedSize) +{ + NSCache *cache = sharedRendererCache(); + + ASTextNodeRendererKey *key = [[ASTextNodeRendererKey alloc] init]; + key.attributes = attributes; + key.constrainedSize = constrainedSize; + + ASTextKitRenderer *renderer = [cache objectForKey:key]; + if (renderer == nil) { + renderer = [[ASTextKitRenderer alloc] initWithTextKitAttributes:attributes constrainedSize:constrainedSize]; + [cache setObject:renderer forKey:key]; + } + + return renderer; +} + @interface ASTextNode () @end @@ -73,10 +144,6 @@ struct ASTextNodeDrawParameter { NSRange _highlightRange; ASHighlightOverlayLayer *_activeHighlightLayer; - CGSize _constrainedSize; - - ASTextKitRenderer *_renderer; - ASTextNodeDrawParameter _drawParameter; UILongPressGestureRecognizer *_longPressGestureRecognizer; @@ -123,8 +190,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; self.isAccessibilityElement = YES; self.accessibilityTraits = UIAccessibilityTraitStaticText; - _constrainedSize = CGSizeMake(-INFINITY, -INFINITY); - // Placeholders // Disabled by default in ASDisplayNode, but add a few options for those who toggle // on the special placeholder behavior of ASTextNode. @@ -139,8 +204,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; { CGColorRelease(_shadowColor); - [self _invalidateRenderer]; - if (_longPressGestureRecognizer) { _longPressGestureRecognizer.delegate = nil; [_longPressGestureRecognizer removeTarget:nil action:NULL]; @@ -181,27 +244,12 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; #pragma mark - ASDisplayNode -// FIXME: Re-evaluate if it is still the right decision to clear the renderer at this stage. -// This code was written before TextKit and when 512MB devices were still the overwhelming majority. -- (void)displayDidFinish -{ - [super displayDidFinish]; - - // We invalidate our renderer here to clear the very high memory cost of - // keeping this around. _invalidateRenderer will dealloc this onto a bg - // thread resulting in less stutters on the main thread than if it were - // to be deallocated in dealloc. This is also helpful in opportunistically - // reducing memory consumption and reducing the overall footprint of the app. - [self _invalidateRenderer]; -} - - (void)clearContents { // 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. [super clearContents]; // ASDisplayNode will set layer.contents = nil - [self _invalidateRenderer]; } - (void)didLoad @@ -218,45 +266,23 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; } } -- (void)setFrame:(CGRect)frame -{ - [super setFrame:frame]; - [self _invalidateRendererIfNeededForBoundsSize:frame.size]; -} - -- (void)setBounds:(CGRect)bounds -{ - [super setBounds:bounds]; - [self _invalidateRendererIfNeededForBoundsSize:bounds.size]; -} - #pragma mark - Renderer Management - (ASTextKitRenderer *)_renderer { - return [self _rendererWithBounds:self.threadSafeBounds]; + CGSize constrainedSize = self.threadSafeBounds.size; + return [self _rendererWithBoundsSlow:{.size = constrainedSize}]; } -- (ASTextKitRenderer *)_rendererWithBounds:(CGRect)bounds +- (ASTextKitRenderer *)_rendererWithBoundsSlow:(CGRect)bounds { ASDN::MutexLocker l(__instanceLock__); - - if (_renderer == nil) { - CGSize constrainedSize; - if (_constrainedSize.width != -INFINITY) { - constrainedSize = _constrainedSize; - } else { - constrainedSize = bounds.size; - constrainedSize.width -= (_textContainerInset.left + _textContainerInset.right); - constrainedSize.height -= (_textContainerInset.top + _textContainerInset.bottom); - } - - _renderer = [[ASTextKitRenderer alloc] initWithTextKitAttributes:[self _rendererAttributes] - constrainedSize:constrainedSize]; - } - return _renderer; + bounds.size.width -= (_textContainerInset.left + _textContainerInset.right); + bounds.size.height -= (_textContainerInset.top + _textContainerInset.bottom); + return rendererForAttributes([self _rendererAttributes], bounds.size); } + - (ASTextKitAttributes)_rendererAttributes { ASDN::MutexLocker l(__instanceLock__); @@ -276,38 +302,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; }; } -- (void)_invalidateRendererIfNeeded -{ - [self _invalidateRendererIfNeededForBoundsSize:self.threadSafeBounds.size]; -} - -- (void)_invalidateRendererIfNeededForBoundsSize:(CGSize)boundsSize -{ - if ([self _needInvalidateRendererForBoundsSize:boundsSize]) { - // Our bounds have changed to a size that is not identical to our constraining size, - // so our previous layout information is invalid, and TextKit may draw at the - // incorrect origin. - { - ASDN::MutexLocker l(__instanceLock__); - _constrainedSize = CGSizeMake(-INFINITY, -INFINITY); - } - [self _invalidateRenderer]; - } -} - -- (void)_invalidateRenderer -{ - ASDN::MutexLocker l(__instanceLock__); - - if (_renderer) { - // Destruction of the layout managers/containers/text storage is quite - // expensive, and can take some time, so we dispatch onto a bg queue to - // actually dealloc. - ASPerformBackgroundDeallocation(_renderer); - _renderer = nil; - } -} - #pragma mark - Layout and Sizing - (void)setTextContainerInset:(UIEdgeInsets)textContainerInset @@ -327,60 +321,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; return _textContainerInset; } -- (BOOL)_needInvalidateRendererForBoundsSize:(CGSize)boundsSize -{ - ASDN::MutexLocker l(__instanceLock__); - - if (_renderer == nil) { - return YES; - } - - // If the size is not the same as the constraint we provided to the renderer, start out assuming we need - // a new one. However, there are common cases where the constrained size doesn't need to be the same as calculated. - CGSize rendererConstrainedSize = _renderer.constrainedSize; - - //inset bounds - boundsSize.width -= _textContainerInset.left + _textContainerInset.right; - boundsSize.height -= _textContainerInset.top + _textContainerInset.bottom; - - if (CGSizeEqualToSize(boundsSize, rendererConstrainedSize)) { - return NO; - } else { - // It is very common to have a constrainedSize with a concrete, specific width but +Inf height. - // In this case, as long as the text node has bounds as large as the full calculatedLayout suggests, - // it means that the text has all the room it needs (as it was not vertically bounded). So, we will not - // experience truncation and don't need to recreate the renderer with the size it already calculated, - // as this would essentially serve to set its constrainedSize to be its calculatedSize (unnecessary). - ASLayout *layout = self.calculatedLayout; - if (layout != nil && CGSizeEqualToSize(boundsSize, layout.size)) { - return (boundsSize.width != rendererConstrainedSize.width); - } else { - return YES; - } - } -} - -- (void)calculatedLayoutDidChange -{ - [super calculatedLayoutDidChange]; - - ASLayout *layout = self.calculatedLayout; - - if (layout != nil) { - ASDN::MutexLocker l(__instanceLock__); - CGSize layoutSize = layout.size; - - // Apply textContainerInset - layoutSize.width -= (_textContainerInset.left + _textContainerInset.right); - layoutSize.height -= (_textContainerInset.top + _textContainerInset.bottom); - - if (CGSizeEqualToSize(_constrainedSize, layoutSize) == NO) { - _constrainedSize = layoutSize; - [self _invalidateRenderer]; - } - } -} - - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize { ASDN::MutexLocker l(__instanceLock__); @@ -390,27 +330,18 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; // Cache the original constrained size for final size calculateion CGSize originalConstrainedSize = constrainedSize; - - // Adjust constrainedSize for textContainerInset before assigning it - constrainedSize.width -= (_textContainerInset.left + _textContainerInset.right); - constrainedSize.height -= (_textContainerInset.top + _textContainerInset.bottom); - - _constrainedSize = constrainedSize; - - if (_renderer != nil && CGSizeEqualToSize(constrainedSize, _renderer.constrainedSize) == NO) { - [self _invalidateRenderer]; - } [self setNeedsDisplay]; - CGSize size = [self _renderer].size; + ASTextKitRenderer *renderer = [self _rendererWithBoundsSlow:{.size = constrainedSize}]; + CGSize size = renderer.size; if (_attributedText.length > 0) { self.style.ascender = [[self class] ascenderWithAttributedString:_attributedText]; self.style.descender = [[_attributedText attribute:NSFontAttributeName atIndex:_attributedText.length - 1 effectiveRange:NULL] descender]; - if (_renderer.currentScaleFactor > 0 && _renderer.currentScaleFactor < 1.0) { + if (renderer.currentScaleFactor > 0 && renderer.currentScaleFactor < 1.0) { // while not perfect, this is a good estimate of what the ascender of the scaled font will be. - self.style.ascender *= _renderer.currentScaleFactor; - self.style.descender *= _renderer.currentScaleFactor; + self.style.ascender *= renderer.currentScaleFactor; + self.style.descender *= renderer.currentScaleFactor; } } @@ -461,9 +392,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; // Without this, the size calculation of the text with truncation applied will // not take into account the attributes of attributedText in the last line [self _updateComposedTruncationText]; - - // We need an entirely new renderer - [self _invalidateRenderer]; } NSUInteger length = attributedText.length; @@ -495,7 +423,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; } _exclusionPaths = [exclusionPaths copy]; - [self _invalidateRenderer]; [self setNeedsLayout]; [self setNeedsDisplay]; } @@ -536,7 +463,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; CGContextTranslateCTM(context, _textContainerInset.left, _textContainerInset.top); - ASTextKitRenderer *renderer = [self _rendererWithBounds:drawParameterBounds]; + ASTextKitRenderer *renderer = [self _rendererWithBoundsSlow:drawParameterBounds]; // Fill background if (backgroundColor != nil) { @@ -790,11 +717,12 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ]; if (highlightTargetLayer != nil) { ASDN::MutexLocker l(__instanceLock__); + ASTextKitRenderer *renderer = [self _renderer]; - NSArray *highlightRects = [[self _renderer] rectsForTextRange:highlightRange measureOption:ASTextKitRendererMeasureOptionBlock]; + NSArray *highlightRects = [renderer rectsForTextRange:highlightRange measureOption:ASTextKitRendererMeasureOptionBlock]; NSMutableArray *converted = [NSMutableArray arrayWithCapacity:highlightRects.count]; for (NSValue *rectValue in highlightRects) { - UIEdgeInsets shadowPadding = _renderer.shadower.shadowPadding; + UIEdgeInsets shadowPadding = renderer.shadower.shadowPadding; CGRect rendererRect = ASTextNodeAdjustRenderRectForShadowPadding(rectValue.CGRectValue, shadowPadding); // The rects returned from renderer don't have `textContainerInset`, @@ -1119,7 +1047,6 @@ static CGRect ASTextNodeAdjustRenderRectForShadowPadding(CGRect rendererRect, UI CGColorRelease(_shadowColor); _shadowColor = CGColorRetain(shadowColor); _cachedShadowUIColor = [UIColor colorWithCGColor:shadowColor]; - [self _invalidateRenderer]; [self setNeedsDisplay]; } } @@ -1137,7 +1064,6 @@ static CGRect ASTextNodeAdjustRenderRectForShadowPadding(CGRect rendererRect, UI if (!CGSizeEqualToSize(_shadowOffset, shadowOffset)) { _shadowOffset = shadowOffset; - [self _invalidateRenderer]; [self setNeedsDisplay]; } } @@ -1155,7 +1081,6 @@ static CGRect ASTextNodeAdjustRenderRectForShadowPadding(CGRect rendererRect, UI if (_shadowOpacity != shadowOpacity) { _shadowOpacity = shadowOpacity; - [self _invalidateRenderer]; [self setNeedsDisplay]; } } @@ -1173,7 +1098,6 @@ static CGRect ASTextNodeAdjustRenderRectForShadowPadding(CGRect rendererRect, UI if (_shadowRadius != shadowRadius) { _shadowRadius = shadowRadius; - [self _invalidateRenderer]; [self setNeedsDisplay]; } } @@ -1232,7 +1156,6 @@ static NSAttributedString *DefaultTruncationAttributedString() if (_truncationMode != truncationMode) { _truncationMode = truncationMode; - [self _invalidateRenderer]; [self setNeedsDisplay]; } } @@ -1251,7 +1174,6 @@ static NSAttributedString *DefaultTruncationAttributedString() if ([_pointSizeScaleFactors isEqualToArray:pointSizeScaleFactors] == NO) { _pointSizeScaleFactors = pointSizeScaleFactors; - [self _invalidateRenderer]; [self setNeedsDisplay]; }} @@ -1261,7 +1183,6 @@ static NSAttributedString *DefaultTruncationAttributedString() if (_maximumNumberOfLines != maximumNumberOfLines) { _maximumNumberOfLines = maximumNumberOfLines; - [self _invalidateRenderer]; [self setNeedsDisplay]; } } @@ -1285,7 +1206,6 @@ static NSAttributedString *DefaultTruncationAttributedString() - (void)_invalidateTruncationText { [self _updateComposedTruncationText]; - [self _invalidateRenderer]; [self setNeedsDisplay]; } diff --git a/AsyncDisplayKit/TextKit/ASTextKitAttributes.h b/AsyncDisplayKit/TextKit/ASTextKitAttributes.h index 0021ec94cb..5b77fcfc52 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitAttributes.h +++ b/AsyncDisplayKit/TextKit/ASTextKitAttributes.h @@ -110,7 +110,8 @@ struct ASTextKitAttributes { && maximumNumberOfLines == other.maximumNumberOfLines && shadowOpacity == other.shadowOpacity && shadowRadius == other.shadowRadius - && [pointSizeScaleFactors isEqualToArray:other.pointSizeScaleFactors] + && (pointSizeScaleFactors == other.pointSizeScaleFactors + || [pointSizeScaleFactors isEqualToArray:other.pointSizeScaleFactors]) && CGSizeEqualToSize(shadowOffset, other.shadowOffset) && ASObjectIsEqual(exclusionPaths, other.exclusionPaths) && ASObjectIsEqual(avoidTailTruncationSet, other.avoidTailTruncationSet) diff --git a/AsyncDisplayKit/TextKit/ASTextKitContext.h b/AsyncDisplayKit/TextKit/ASTextKitContext.h index 58257efbab..9e90fac710 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitContext.h +++ b/AsyncDisplayKit/TextKit/ASTextKitContext.h @@ -29,8 +29,6 @@ exclusionPaths:(NSArray *)exclusionPaths constrainedSize:(CGSize)constrainedSize; -@property (nonatomic, assign, readwrite) CGSize constrainedSize; - /** All operations on TextKit values MUST occur within this locked context. Simultaneous access (even non-mutative) to TextKit components may cause crashes. diff --git a/AsyncDisplayKit/TextKit/ASTextKitContext.mm b/AsyncDisplayKit/TextKit/ASTextKitContext.mm index ba7477c500..7eca01d531 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitContext.mm +++ b/AsyncDisplayKit/TextKit/ASTextKitContext.mm @@ -54,18 +54,6 @@ return self; } -- (CGSize)constrainedSize -{ - ASDN::MutexSharedLocker l(__instanceLock__); - return _textContainer.size; -} - -- (void)setConstrainedSize:(CGSize)constrainedSize -{ - ASDN::MutexSharedLocker l(__instanceLock__); - _textContainer.size = constrainedSize; -} - - (void)performBlockWithLockedTextKitComponents:(void (^)(NSLayoutManager *, NSTextStorage *, NSTextContainer *))block diff --git a/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm b/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm index d4246b2a1c..1967b98264 100755 --- a/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm +++ b/AsyncDisplayKit/TextKit/ASTextKitRenderer.mm @@ -37,7 +37,6 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() @implementation ASTextKitRenderer { CGSize _calculatedSize; - BOOL _sizeIsCalculated; } @synthesize attributes = _attributes, context = _context, shadower = _shadower, truncater = _truncater, fontSizeAdjuster = _fontSizeAdjuster; @@ -49,62 +48,38 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() if (self = [super init]) { _constrainedSize = constrainedSize; _attributes = attributes; - _sizeIsCalculated = NO; _currentScaleFactor = 1; - } - return self; -} - -- (ASTextKitShadower *)shadower -{ - if (!_shadower) { - ASTextKitAttributes attributes = _attributes; + + // As the renderer should be thread safe, create all subcomponents in the initialization method _shadower = [ASTextKitShadower shadowerWithShadowOffset:attributes.shadowOffset shadowColor:attributes.shadowColor shadowOpacity:attributes.shadowOpacity shadowRadius:attributes.shadowRadius]; - } - return _shadower; -} - -- (ASTextKitTailTruncater *)truncater -{ - if (!_truncater) { - ASTextKitAttributes attributes = _attributes; - NSCharacterSet *avoidTailTruncationSet = attributes.avoidTailTruncationSet ? : _defaultAvoidTruncationCharacterSet(); - _truncater = [[ASTextKitTailTruncater alloc] initWithContext:[self context] - truncationAttributedString:attributes.truncationAttributedString - avoidTailTruncationSet:avoidTailTruncationSet]; - } - return _truncater; -} - -- (ASTextKitFontSizeAdjuster *)fontSizeAdjuster -{ - if (!_fontSizeAdjuster) { - ASTextKitAttributes attributes = _attributes; - // We must inset the constrained size by the size of the shadower. - CGSize shadowConstrainedSize = [[self shadower] insetSizeWithConstrainedSize:_constrainedSize]; - _fontSizeAdjuster = [[ASTextKitFontSizeAdjuster alloc] initWithContext:[self context] - constrainedSize:shadowConstrainedSize - textKitAttributes:attributes]; - } - return _fontSizeAdjuster; -} - -- (ASTextKitContext *)context -{ - if (!_context) { - ASTextKitAttributes attributes = _attributes; + // We must inset the constrained size by the size of the shadower. CGSize shadowConstrainedSize = [[self shadower] insetSizeWithConstrainedSize:_constrainedSize]; + _context = [[ASTextKitContext alloc] initWithAttributedString:attributes.attributedString lineBreakMode:attributes.lineBreakMode maximumNumberOfLines:attributes.maximumNumberOfLines exclusionPaths:attributes.exclusionPaths constrainedSize:shadowConstrainedSize]; + + NSCharacterSet *avoidTailTruncationSet = attributes.avoidTailTruncationSet ?: _defaultAvoidTruncationCharacterSet(); + _truncater = [[ASTextKitTailTruncater alloc] initWithContext:[self context] + truncationAttributedString:attributes.truncationAttributedString + avoidTailTruncationSet:avoidTailTruncationSet]; + + ASTextKitAttributes attributes = _attributes; + // We must inset the constrained size by the size of the shadower. + _fontSizeAdjuster = [[ASTextKitFontSizeAdjuster alloc] initWithContext:[self context] + constrainedSize:shadowConstrainedSize + textKitAttributes:attributes]; + + // Calcualate size immediately + [self _calculateSize]; } - return _context; + return self; } - (NSStringDrawingContext *)stringDrawingContext @@ -127,10 +102,6 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() - (CGSize)size { - if (!_sizeIsCalculated) { - [self _calculateSize]; - _sizeIsCalculated = YES; - } return _calculatedSize; } @@ -222,12 +193,6 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() { // We add an assertion so we can track the rare conditions where a graphics context is not present ASDisplayNodeAssertNotNil(context, @"This is no good without a context."); - - // This renderer may not be the one that did the sizing. If that is the case its truncation and currentScaleFactor may not have been evaluated. - // If there's any possibility we need to truncate or scale (i.e. width is not infinite), perform the size calculation. - if (_sizeIsCalculated == NO && isinf(_constrainedSize.width) == NO) { - [self _calculateSize]; - } bounds = CGRectIntersection(bounds, { .size = _constrainedSize }); CGRect shadowInsetBounds = [[self shadower] insetRectWithConstrainedRect:bounds]; @@ -298,9 +263,7 @@ static NSCharacterSet *_defaultAvoidTruncationCharacterSet() - (std::vector)visibleRanges { - ASTextKitTailTruncater *truncater = [self truncater]; - [truncater truncate]; - return truncater.visibleRanges; + return _truncater.visibleRanges; } @end diff --git a/AsyncDisplayKitTests/ASTextNodeSnapshotTests.m b/AsyncDisplayKitTests/ASTextNodeSnapshotTests.m index 2a2a1b704d..c0acf7ad4c 100644 --- a/AsyncDisplayKitTests/ASTextNodeSnapshotTests.m +++ b/AsyncDisplayKitTests/ASTextNodeSnapshotTests.m @@ -18,6 +18,13 @@ @implementation ASTextNodeSnapshotTests +- (void)setUp +{ + [super setUp]; + + self.recordMode = NO; +} + - (void)testTextContainerInset { // trivial test case to ensure ASSnapshotTestCase works diff --git a/AsyncDisplayKitTests/ASTextNodeTests.m b/AsyncDisplayKitTests/ASTextNodeTests.m index ff1620cd72..7395e15b3b 100644 --- a/AsyncDisplayKitTests/ASTextNodeTests.m +++ b/AsyncDisplayKitTests/ASTextNodeTests.m @@ -171,7 +171,9 @@ ASTextNodeTestDelegate *delegate = [ASTextNodeTestDelegate new]; _textNode.delegate = delegate; - [_textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 100))]; + ASLayout *layout = [_textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 100))]; + _textNode.frame = CGRectMake(0, 0, layout.size.width, layout.size.height); + NSRange returnedLinkRange; NSString *returnedAttributeName; NSString *returnedLinkAttributeValue = [_textNode linkAttributeValueAtPoint:CGPointMake(3, 3) attributeName:&returnedAttributeName range:&returnedLinkRange]; diff --git a/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testTextContainerInsetIsIncludedWithSmallerConstrainedSize@2x.png b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASTextNodeSnapshotTests/testTextContainerInsetIsIncludedWithSmallerConstrainedSize@2x.png index 4f8364937f04fb60b0b90dc97e5abdb8386f4dfd..65c801d4dffbb0a823db8d1e9387adddb7d12a5f 100644 GIT binary patch literal 5918 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 XmgyTwS%kDOlR0|W+qL3Q@Qr@~>>R$a literal 5993 zcmeHL`8$;B|F`9+bTTQVF@%)tOV(kWC`+Q0uO@3IWiJV1nUPL0NuJ7@ZTjXQS;i6( zMhm90g)A9Mp&87KX^a`i%$WHO&gZ($`3JtAU(Rz~_jNzd`+l$Y`}Mr<<=z<=`&~N} zcZi6H>~eIla}yB}1%%B^N>WHEc27Ji>_j8o?2n67_kkCLAKR}xz#~OOr2dc>_V$i;znw>lQYTJ54~qo<^3&*;q2aQ_ zkuQbFO#4hv>0Nubn@So!+-`VgkEGKh>4)>ADgQ1Tr`MVbCnZ$@x2D&clFJUCEE5u! z#v7_e%(=XND{eJviBrjG@TD}sDDC+6hE?m;>Z&JFGAcH=L`20T{(bp(68;NFaCZH6 zr}amM+u<^ov(jlvg1);JhZiUGK`Qzx*Zqrz^c57uDL|5iy|aW=&-O3wS)cvKIt)YsgG|}FgUYqMODxbGY0gXSu9ShSF^4%hRe_@HRF$y)3 z0ZT~T6nj^!14W)Do)Ypsjcr=&Q}`@Rl-^KLc^(xhKuGm#BT3`;|C;)L znZOIBqs`_T@~mJFh+bs=$RvqaXc8_ZbK&k!Go6&*9IC6VKq-lF4KTptyTaELHNwG^ z_jb2zwx=oW0In*d9o+*VPTm=c@z}0$JgJsLuzo+ZIlM2kVe%KZbuAipd19Lyee{Ax z9-(PA!iZpHyU7qrx&t49VlQ*oNoN7NQfzxR3oT#e!BmKW>RMQ{Uf&Z*a`7`d+*3zd zQ|0P^fkJwi-Q%RAI1lE9_1URS7Zr@0vA-s^$MPPLqszJ`aJ7w9v6cLY9EA<6A+`mC ztyX@al*7f#mHX&U51L&O;hsWa1onH7s?Af0F6r*CjkphJFKiwMO9_Tl7(lMh-k3Le z|7F8}=|YvF1B4Sy1wvau>#1F05*A6>vV_*|U8cQ=W<1dVy=~*CAbdhC4nmE0zqRut zNQDxHbE86C=Yt@`*`^*w6T&6b(#h|uST}S)u8LdN+Kg(8(*{g0F5#zm4ZPu<*ZND_y@wm@B)cYH27@D0CeeN9NwjY`aX0DRzGpSyb8Ujh>%vI zge4O;W)MutxQ31=*fN_ACAXJc5pB&P?ylqQFE0*)lwDcd!LJH!Z|^aM_m`BfA%5^d zK#i~n9Z52J*6O)m z+>}eu4lMGMM4r13?C>$tpBFmAQaUyP7<+qZjuhcT9ohLUSfRh9`V>pNsd}F7fGWt_ zO?fR?coR8%UQVg_fT3$xeLJ3`xcK8sT>EqZb~G54c$$?{Md&Y;3yTlN{YTw0eU8L9 zNlg}8B}1nL2NOOtp~=XA!l)ixj8X6GWD<*4L|)7o_}s|t_byn&D_yWZhV{<(vQI1| zP-d@h1?Re4I1Wu_w}=U@2gejIs69!)kkL_lO55mzBHZ-B3yTE=dfWE@NLb6PEtWH@SxeE};^oms$EAIy~&{y*g4w$HlvN#Oww~Wd;n3?Z${#YxyV;_`X_b zqpQ&Aa2l4TmpgQ2YHhuIki2k?lW;m<8vLGFN`=wKey}P^SGZ3Oadp;X;8@@N?{|A3 zdAbO%RqMBx4%(<}8!_K+T8s38T%p}IwY%NxKfGj$#nmATJomo`5JU^yBQ@ICe>lyB zoD<+HG4!i}~8lpmYpM_63 z;=J}M{(6TaC^jWSxwdd$gilAl+LUZWP)8Dm-Q3I^Gq=C};rK0^9cgFxzGgQEb*j;S zU3w6Bcq~%#M&%*8b?0oRf-BlIW7~I6(5sUZvICHq9C9k5&wNkXmeFU*LZey`LW+mZ zck5UcLif>iJvV3{hV6U*`M##{rk2oi(CEHL-x_389*QCM-mON3Uu2jWv<7cqol64i zm9I>fAPZz@)m1O69z+dz`p${xYUpP=5$jiM&>;;Kjowq@KEN!pv6bEYd{-Qfr%*7Phl z-R~mf&oHX*<48DJe#q8ry<~8;a)vSZiEmy?R9%*Lzn|{N$h{_4x#7LvfP~Q;AC*do zG9W#E*=>;+JRai(S9gN-vR=+GdOz_!^dfkMOF_O}{YN3n&(jXkpSjS?i(a{Nn6<-w zAzGM?5NsTMC>K^WA=n27rWpy=c>jA$WNb-0HS!{(>3N-C+TT+{5FkAbUoa&YR3N=P zgP4!TMqWZ}LPEj$<&!z*?1J$mKCjG7nU5Kf61Vp)F#k2wmjsL{&PPobX zJkJEqaSGj`Xy2d=Wh>S?k@tEKJ5xJkwB^FIosnIO5C)YBkuAsx5HXz+p$h;e4SH{` z?gOB7TM*5Fr`2es@eJsW*ILiw3#qX4Nc{EdOUe1jI$SL z6Hdv6e9G=svz(C9m8^9a?%2_OD?tXN7m-820<`<%bM2rtN+!Q<8s0f;Q{s)ae9DVa zZ8)Au@PJ11PygPr0a)!kYVt(8D`RXQqEGMcsdu`RL_*_`>g#m+WR|s)Z*!456tV{= z2zTOM>tLsxxVhh+f2T28w>P5aC`8_)>)|?$hU9@~86eY>#=zHvRtE;se}(x8bM?ec zb3tAI(wLwJ!}~z1-PzbX!%fA=+ks`za=;-xE~i_*rGoZ0Ft*$fX=jl1em}j zK^xMCn>Eln^XOM>IBS@?I)&7^@4#Nq#?-4BepSK*YETC?U?vu@XHY!~JO;8mZ9S3~ zFwN$0kUBeQd`zpB13iKN5tFZ$$Gs}rYne=HOqeJ^wl)sx649#uQ>@1Vy-c2^^G?z zS4UiInLvNcj9$AIex<>a-LMsyw&j!Tp!QJ42Lu)zfw0pNJxz?Zr3&UThU|b;`vpYe zN(}OC1bAaUO8{Y-i1d3OfDff@88|r4nB5Q5e`0XufXHItqCh!x#XEh#$JZl>iydR% zF%QG;$BpiAReT8;%rUpDNt@To(LyjYj|e?6f@_#2iF6j8?6LHF%s97>C!n&EQ0TL^{;~d;P6KB6w*?Saby*3mL9PXeRM`E+S=GVsnjpdn zV_#(sqxhMCr+XA=7H$hp}|4=CeJqaP(G(@PIt?-8;rhoZ{S(j|{!H>5GPo zA0m5YG-{WF@F;u)G8IKQR#UCsT29E?5?@p|3BQ-9^bc`|H~9(9e~G(ZM&$53P=6O+ zoNHMiWNe9ZE}NjY`DOiGyu!JacwKu-+{8JM{8XxP@h|6FDd{18?v62@>1vx<9Lsj3 zLHFKv_Y&VDJpQDn#7su)s#N_RK4hT&tC)m_7}2=4@V>u3do$@zk;qY{(Kllsqluiq zVkgc;Tkm+6`*-UIjqxz5=N1ee$WEAR?m&3|O?z6_&f~Y<*nLCSLV|BBr2H`P6*u<~F7?h5OR|)(qMv=e z(=pG5xK0(~I{6K1mT^Yb8K(V;q4>ev|DerN)ra&K%Z(E;Dvt#w8Ukx_X5>Ng9m{kZ s;ga-UxaaxT=)YI2|Ga0Gl>HOdSg~eor*-rG##OlE2^YKS<9@gQ4|zJT1poj5 From a6b2166244bcc7d0586d3290770c1b04780fbeca Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Wed, 7 Dec 2016 09:57:48 -0800 Subject: [PATCH 13/16] =?UTF-8?q?Don=E2=80=99t=20compare=20the=20hash=20in?= =?UTF-8?q?stead=20compare=20the=20attributes=20in=20ASTextNodeRendererKey?= =?UTF-8?q?=20isEqual=20(#2726)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AsyncDisplayKit/ASTextNode.mm | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/AsyncDisplayKit/ASTextNode.mm b/AsyncDisplayKit/ASTextNode.mm index 5539a95116..07079628e4 100644 --- a/AsyncDisplayKit/ASTextNode.mm +++ b/AsyncDisplayKit/ASTextNode.mm @@ -81,8 +81,7 @@ ASDISPLAYNODE_INLINE NSUInteger ASHashFromCGSize(CGSize size) return YES; } - return _attributes.hash() == object.attributes.hash() - && CGSizeEqualToSize(_constrainedSize, object.constrainedSize); + return _attributes == object.attributes && CGSizeEqualToSize(_constrainedSize, object.constrainedSize); } @end From 7de2decdcb79cf6df2b3f4d2be9d296291314c2f Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Wed, 7 Dec 2016 13:36:15 -0800 Subject: [PATCH 14/16] Allow section-indexpaths in collection & table validation (#2727) --- AsyncDisplayKit/ASCollectionView.mm | 4 +++- AsyncDisplayKit/ASTableView.mm | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 082832d66d..e0dc28f6b0 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -642,7 +642,9 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; return nil; } - if (indexPath.item >= [self numberOfItemsInSection:section]) { + NSInteger item = indexPath.item; + // item == NSNotFound means e.g. "scroll to this section" and is acceptable + if (item != NSNotFound && item >= [self numberOfItemsInSection:section]) { ASDisplayNodeFailAssert(@"Collection view index path has invalid item %lu in section %lu, item count = %lu", (unsigned long)indexPath.item, (unsigned long)section, (unsigned long)[self numberOfItemsInSection:section]); return nil; } diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index cdd36fa9e8..60bf5df05b 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -579,7 +579,9 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; return nil; } - if (indexPath.item >= [self numberOfRowsInSection:section]) { + NSInteger item = indexPath.item; + // item == NSNotFound means e.g. "scroll to this section" and is acceptable + if (item != NSNotFound && item >= [self numberOfRowsInSection:section]) { ASDisplayNodeFailAssert(@"Table view index path has invalid item %lu in section %lu, item count = %lu", (unsigned long)indexPath.item, (unsigned long)section, (unsigned long)[self numberOfRowsInSection:section]); return nil; } From 80ab695cd046101a1a4ea8ef5d97bcbfa3915f6e Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Wed, 7 Dec 2016 14:38:12 -0800 Subject: [PATCH 15/16] Add lint mode to build.sh (#2728) --- build.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/build.sh b/build.sh index 0c6ed44c77..ce07dc7312 100755 --- a/build.sh +++ b/build.sh @@ -292,4 +292,12 @@ if [ "$MODE" = "framework" ]; then exit 0 fi +if [ "$MODE" = "cocoapods-lint" ]; then + echo "Verifying that podspec lints." + + set -o pipefail && pod lib lint + trap - EXIT + exit 0 +fi + echo "Unrecognised mode '$MODE'." From 4355f4d2eef9472642a8a8029aeef1c7fdad034f Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Wed, 7 Dec 2016 14:58:34 -0800 Subject: [PATCH 16/16] [Layout] Don't crash if layout elements are created in `layoutSpecThatFits:` (#2694) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix crash if layout elements are created with no owner and referenced in layoutSpecThatFits: * Add failing test for nodes deallocated while creating in layoutSpecThatFits: * Some more * Some cleanup * Added more complexity to tests * Only cache sublayouts if the layout get’s flattened * Address comments * Address comments --- AsyncDisplayKit/Layout/ASLayout.mm | 41 ++++++++++++++- .../ASDisplayNodeLayoutTests.mm | 50 +++++++++++++++++++ 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/AsyncDisplayKit/Layout/ASLayout.mm b/AsyncDisplayKit/Layout/ASLayout.mm index a64d81be2e..0df753b7fa 100644 --- a/AsyncDisplayKit/Layout/ASLayout.mm +++ b/AsyncDisplayKit/Layout/ASLayout.mm @@ -47,6 +47,16 @@ static inline NSString * descriptionIndents(NSUInteger indents) */ @property (nonatomic, getter=isFlattened) BOOL flattened; +/* + * Caches all sublayouts if set to YES or destroys the sublayout cache if set to NO. Defaults to YES + */ +@property (nonatomic, assign) BOOL retainSublayoutLayoutElements; + +/** + * Array for explicitly retain sublayout layout elements in case they are created and references in layoutSpecThatFits: and no one else will hold a strong reference on it + */ +@property (nonatomic, strong) NSMutableArray> *sublayoutLayoutElements; + @end @implementation ASLayout @@ -69,6 +79,7 @@ static inline NSString * descriptionIndents(NSUInteger indents) #endif _layoutElement = layoutElement; + // Read this now to avoid @c weak overhead later. _layoutElementType = layoutElement.layoutElementType; @@ -88,7 +99,9 @@ static inline NSString * descriptionIndents(NSUInteger indents) _sublayouts = sublayouts != nil ? [sublayouts copy] : @[]; _flattened = NO; + _retainSublayoutLayoutElements = NO; } + return self; } @@ -137,6 +150,28 @@ static inline NSString * descriptionIndents(NSUInteger indents) sublayouts:layout.sublayouts]; } +#pragma mark - Sublayout Elements Caching + +- (void)setRetainSublayoutLayoutElements:(BOOL)retainSublayoutLayoutElements +{ + if (_retainSublayoutLayoutElements != retainSublayoutLayoutElements) { + _retainSublayoutLayoutElements = retainSublayoutLayoutElements; + + if (retainSublayoutLayoutElements == NO) { + _sublayoutLayoutElements = nil; + } else { + // Add sublayouts layout elements to an internal array to retain it while the layout lives + NSUInteger sublayoutCount = _sublayouts.count; + if (sublayoutCount > 0) { + _sublayoutLayoutElements = [NSMutableArray arrayWithCapacity:sublayoutCount]; + for (ASLayout *sublayout in _sublayouts) { + [_sublayoutLayoutElements addObject:sublayout.layoutElement]; + } + } + } + } +} + #pragma mark - Layout Flattening - (ASLayout *)filteredNodeLayoutTree @@ -170,8 +205,10 @@ static inline NSString * descriptionIndents(NSUInteger indents) } queue.insert(queue.cbegin(), sublayoutContexts.begin(), sublayoutContexts.end()); } - - return [ASLayout layoutWithLayoutElement:_layoutElement size:_size sublayouts:flattenedSublayouts]; + + ASLayout *layout = [ASLayout layoutWithLayoutElement:_layoutElement size:_size position:CGPointZero sublayouts:flattenedSublayouts]; + layout.retainSublayoutLayoutElements = YES; + return layout; } #pragma mark - Accessors diff --git a/AsyncDisplayKitTests/ASDisplayNodeLayoutTests.mm b/AsyncDisplayKitTests/ASDisplayNodeLayoutTests.mm index e608989bba..394d570f86 100644 --- a/AsyncDisplayKitTests/ASDisplayNodeLayoutTests.mm +++ b/AsyncDisplayKitTests/ASDisplayNodeLayoutTests.mm @@ -121,4 +121,54 @@ XCTAssertThrows([ASLayout layoutWithLayoutElement:displayNode size:CGSizeMake(INFINITY, INFINITY)]); } +- (void)testThatLayoutElementCreatedInLayoutSpecThatFitsDoNotGetDeallocated +{ + const CGSize kSize = CGSizeMake(300, 300); + + ASDisplayNode *subNode = [[ASDisplayNode alloc] init]; + subNode.automaticallyManagesSubnodes = YES; + subNode.layoutSpecBlock = ^(ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + ASTextNode *textNode = [ASTextNode new]; + textNode.attributedText = [[NSAttributedString alloc] initWithString:@"Test Test Test Test Test Test Test Test"]; + ASInsetLayoutSpec *insetSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:textNode]; + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:insetSpec]; + }; + + ASDisplayNode *rootNode = [[ASDisplayNode alloc] init]; + rootNode.automaticallyManagesSubnodes = YES; + rootNode.layoutSpecBlock = ^(ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + ASTextNode *textNode = [ASTextNode new]; + textNode.attributedText = [[NSAttributedString alloc] initWithString:@"Test Test Test Test Test"]; + ASInsetLayoutSpec *insetSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:textNode]; + + return [ASStackLayoutSpec + stackLayoutSpecWithDirection:ASStackLayoutDirectionVertical + spacing:0.0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStretch + children:@[insetSpec, subNode]]; + }; + + rootNode.frame = CGRectMake(0, 0, kSize.width, kSize.height); + [rootNode view]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Execute measure and layout pass"]; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + + [rootNode layoutThatFits:ASSizeRangeMake(kSize)]; + + dispatch_async(dispatch_get_main_queue(), ^{ + XCTAssertNoThrow([rootNode.view layoutIfNeeded]); + [expectation fulfill]; + }); + }); + + [self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) { + if (error) { + XCTFail(@"Expectation failed: %@", error); + } + }]; +} + @end