From 3a007fc510758c18f1dbec6b02af5d733bcdfe6e Mon Sep 17 00:00:00 2001 From: Heberti Almeida Date: Thu, 5 Jul 2018 16:25:55 -0400 Subject: [PATCH 001/122] Extended IGListKit support for node deselect, highlight and unhighlight --- Source/Private/ASIGListAdapterBasedDataSource.m | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Source/Private/ASIGListAdapterBasedDataSource.m b/Source/Private/ASIGListAdapterBasedDataSource.m index ab1863622..5c5f38279 100644 --- a/Source/Private/ASIGListAdapterBasedDataSource.m +++ b/Source/Private/ASIGListAdapterBasedDataSource.m @@ -92,6 +92,21 @@ - (void)collectionNode:(ASCollectionNode *)collectionNode didSelectItemAtIndexPa [self.delegate collectionView:collectionNode.view didSelectItemAtIndexPath:indexPath]; } +- (void)collectionNode:(ASCollectionNode *)collectionNode didDeselectItemAtIndexPath:(NSIndexPath *)indexPath +{ + [self.delegate collectionView:collectionNode.view didDeselectItemAtIndexPath:indexPath]; +} + +- (void)collectionNode:(ASCollectionNode *)collectionNode didHighlightItemAtIndexPath:(NSIndexPath *)indexPath +{ + [self.delegate collectionView:collectionNode.view didHighlightItemAtIndexPath:indexPath]; +} + +- (void)collectionNode:(ASCollectionNode *)collectionNode didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath +{ + [self.delegate collectionView:collectionNode.view didUnhighlightItemAtIndexPath:indexPath]; +} + - (void)scrollViewDidScroll:(UIScrollView *)scrollView { [self.delegate scrollViewDidScroll:scrollView]; From 755a1eb18240102556b6939179543d1fa7732724 Mon Sep 17 00:00:00 2001 From: Heberti Almeida Date: Thu, 5 Jul 2018 16:33:58 -0400 Subject: [PATCH 002/122] Proposal of renaming willDisplayItemWithNode and didEndDisplayingItemWithNode to be similiar to UICollectionView --- Source/ASCollectionNode.h | 6 ++++-- Source/ASCollectionView.mm | 23 ++++++++++++++++++----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Source/ASCollectionNode.h b/Source/ASCollectionNode.h index 194f31f01..b98ed007f 100644 --- a/Source/ASCollectionNode.h +++ b/Source/ASCollectionNode.h @@ -756,9 +756,11 @@ NS_ASSUME_NONNULL_BEGIN */ - (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode constrainedSizeForItemAtIndexPath:(NSIndexPath *)indexPath; -- (void)collectionNode:(ASCollectionNode *)collectionNode willDisplayItemWithNode:(ASCellNode *)node; +- (void)collectionNode:(ASCollectionNode *)collectionNode willDisplayNode:(ASCellNode *)node forItemAtIndexPath:(NSIndexPath *)indexPath; +- (void)collectionNode:(ASCollectionNode *)collectionNode willDisplayItemWithNode:(ASCellNode *)node ASDISPLAYNODE_DEPRECATED_MSG("This method has been renamed to -collectionNode:willDisplayNode:forItemAtIndexPath:"); -- (void)collectionNode:(ASCollectionNode *)collectionNode didEndDisplayingItemWithNode:(ASCellNode *)node; +- (void)collectionNode:(ASCollectionNode *)collectionNode didEndDisplayingNode:(ASCellNode *)node forItemAtIndexPath:(NSIndexPath *)indexPath; +- (void)collectionNode:(ASCollectionNode *)collectionNode didEndDisplayingItemWithNode:(ASCellNode *)node ASDISPLAYNODE_DEPRECATED_MSG("This method has been renamed to -collectionNode:didEndDisplayingNode:forItemAtIndexPath:"); - (void)collectionNode:(ASCollectionNode *)collectionNode willDisplaySupplementaryElementWithNode:(ASCellNode *)node NS_AVAILABLE_IOS(8_0); - (void)collectionNode:(ASCollectionNode *)collectionNode didEndDisplayingSupplementaryElementWithNode:(ASCellNode *)node; diff --git a/Source/ASCollectionView.mm b/Source/ASCollectionView.mm index d19e59898..4d0953784 100644 --- a/Source/ASCollectionView.mm +++ b/Source/ASCollectionView.mm @@ -179,7 +179,9 @@ @interface ASCollectionView () )asyncDelegate _asyncDelegateFlags.collectionViewShouldShowMenuForItem = [_asyncDelegate respondsToSelector:@selector(collectionView:shouldShowMenuForItemAtIndexPath:)]; _asyncDelegateFlags.collectionViewCanPerformActionForItem = [_asyncDelegate respondsToSelector:@selector(collectionView:canPerformAction:forItemAtIndexPath:withSender:)]; _asyncDelegateFlags.collectionViewPerformActionForItem = [_asyncDelegate respondsToSelector:@selector(collectionView:performAction:forItemAtIndexPath:withSender:)]; - _asyncDelegateFlags.collectionNodeWillDisplayItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:willDisplayItemWithNode:)]; - _asyncDelegateFlags.collectionNodeDidEndDisplayingItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:didEndDisplayingItemWithNode:)]; + _asyncDelegateFlags.collectionNodeWillDisplayItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:willDisplayNode:forItemAtIndexPath:)]; + _asyncDelegateFlags.collectionNodeWillDisplayItemDeprecated = [_asyncDelegate respondsToSelector:@selector(collectionNode:willDisplayItemWithNode:)]; + _asyncDelegateFlags.collectionNodeDidEndDisplayingItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:didEndDisplayingNode:forItemAtIndexPath:)]; + _asyncDelegateFlags.collectionNodeDidEndDisplayingItemDeprecated = [_asyncDelegate respondsToSelector:@selector(collectionNode:didEndDisplayingItemWithNode:)]; _asyncDelegateFlags.collectionNodeWillBeginBatchFetch = [_asyncDelegate respondsToSelector:@selector(collectionNode:willBeginBatchFetchWithContext:)]; _asyncDelegateFlags.shouldBatchFetchForCollectionNode = [_asyncDelegate respondsToSelector:@selector(shouldBatchFetchForCollectionNode:)]; _asyncDelegateFlags.collectionNodeShouldSelectItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:shouldSelectItemAtIndexPath:)]; @@ -1230,10 +1234,12 @@ - (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICol ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with cell that will be displayed not to be nil. indexPath: %@", indexPath); if (_asyncDelegateFlags.collectionNodeWillDisplayItem && self.collectionNode != nil) { - [_asyncDelegate collectionNode:self.collectionNode willDisplayItemWithNode:cellNode]; - } else if (_asyncDelegateFlags.collectionViewWillDisplayNodeForItem) { + [_asyncDelegate collectionNode:self.collectionNode willDisplayNode:cellNode forItemAtIndexPath:indexPath]; + } else if (_asyncDelegateFlags.collectionNodeWillDisplayItem && self.collectionNode != nil) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_asyncDelegate collectionNode:self.collectionNode willDisplayItemWithNode:cellNode]; + } else if (_asyncDelegateFlags.collectionViewWillDisplayNodeForItem) { [_asyncDelegate collectionView:self willDisplayNode:cellNode forItemAtIndexPath:indexPath]; } else if (_asyncDelegateFlags.collectionViewWillDisplayNodeForItemDeprecated) { [_asyncDelegate collectionView:self willDisplayNodeForItemAtIndexPath:indexPath]; @@ -1275,7 +1281,14 @@ - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:( if (_asyncDelegateFlags.collectionNodeDidEndDisplayingItem) { if (ASCollectionNode *collectionNode = self.collectionNode) { - [_asyncDelegate collectionNode:collectionNode didEndDisplayingItemWithNode:cellNode]; + [_asyncDelegate collectionNode:collectionNode didEndDisplayingNode:cellNode forItemAtIndexPath:indexPath]; + } + } else if (_asyncDelegateFlags.collectionNodeDidEndDisplayingItemDeprecated) { + if (ASCollectionNode *collectionNode = self.collectionNode) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [_asyncDelegate collectionNode:collectionNode didEndDisplayingItemWithNode:cellNode]; +#pragma clang diagnostic pop } } else if (_asyncDelegateFlags.collectionViewDidEndDisplayingNodeForItem) { #pragma clang diagnostic push From c1699c491b4721d9056049226c0be8eccefb89aa Mon Sep 17 00:00:00 2001 From: Heberti Almeida Date: Thu, 5 Jul 2018 16:35:09 -0400 Subject: [PATCH 003/122] Extended support for IGListDisplayDelegate --- Source/Private/ASIGListAdapterBasedDataSource.m | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Source/Private/ASIGListAdapterBasedDataSource.m b/Source/Private/ASIGListAdapterBasedDataSource.m index 5c5f38279..d1342698c 100644 --- a/Source/Private/ASIGListAdapterBasedDataSource.m +++ b/Source/Private/ASIGListAdapterBasedDataSource.m @@ -154,6 +154,16 @@ - (void)collectionNode:(ASCollectionNode *)collectionNode willBeginBatchFetchWit [ctrl beginBatchFetchWithContext:context]; } +- (void)collectionNode:(ASCollectionNode *)collectionNode willDisplayNode:(ASCellNode *)node forItemAtIndexPath:(NSIndexPath *)indexPath +{ + [self.delegate collectionView:collectionNode.view willDisplayCell:node.view forItemAtIndexPath:indexPath]; +} + +- (void)collectionNode:(ASCollectionNode *)collectionNode didEndDisplayingNode:(ASCellNode *)node forItemAtIndexPath:(NSIndexPath *)indexPath +{ + [self.delegate collectionView:collectionNode.view didEndDisplayingCell:node.view forItemAtIndexPath:indexPath]; +} + /** * Note: It is not documented that ASCollectionNode will forward these UIKit delegate calls if they are implemented. * It is not considered harmful to do so, and adding them to documentation will confuse most users, who should From fc94d8e72d49d380b029137cb677b1453cc4c5bf Mon Sep 17 00:00:00 2001 From: Heberti Almeida Date: Fri, 6 Jul 2018 09:31:25 -0400 Subject: [PATCH 004/122] Updated changelog with displaying delegates change --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 605ac24a0..3b93a46cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## master * Add your own contributions to the next release on the line below this with your name. +- [IGListKit] Extended IGListKit support for displaying delegates. [Heberti Almeida](https://github.com/hebertialmeida) [#1011](https://github.com/TextureGroup/Texture/pull/1011) - [ASDisplayNode] Adds support for multiple interface state delegates. [Garrett Moon](https://github.com/garrettmoon) [#979](https://github.com/TextureGroup/Texture/pull/979) - [ASDataController] Add capability to renew supplementary views (update map) when size change from zero to non-zero.[Max Wang](https://github.com/wsdwsd0829) [#842](https://github.com/TextureGroup/Texture/pull/842) - Make `ASPerformMainThreadDeallocation` visible in C. [Adlai Holler](https://github.com/Adlai-Holler) From 822f16dd1e75c0037f1418d437257a4695a1fb5b Mon Sep 17 00:00:00 2001 From: Heberti Almeida Date: Fri, 6 Jul 2018 10:01:50 -0400 Subject: [PATCH 005/122] Removed extra deprecation supression flag --- Source/ASCollectionView.mm | 3 --- 1 file changed, 3 deletions(-) diff --git a/Source/ASCollectionView.mm b/Source/ASCollectionView.mm index 4d0953784..d60bb7d9b 100644 --- a/Source/ASCollectionView.mm +++ b/Source/ASCollectionView.mm @@ -1288,11 +1288,8 @@ - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:( #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" [_asyncDelegate collectionNode:collectionNode didEndDisplayingItemWithNode:cellNode]; -#pragma clang diagnostic pop } } else if (_asyncDelegateFlags.collectionViewDidEndDisplayingNodeForItem) { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_asyncDelegate collectionView:self didEndDisplayingNode:cellNode forItemAtIndexPath:indexPath]; #pragma clang diagnostic pop } From 37398c038590d1d5b5c6e55f3f76bb3a5ae46e50 Mon Sep 17 00:00:00 2001 From: Heberti Almeida Date: Wed, 11 Jul 2018 10:20:56 -0400 Subject: [PATCH 006/122] Reverted ASCollectionNode changes and implemented displaing delegate with indexPathForNode: --- Source/ASCollectionNode.h | 6 ++--- Source/ASCollectionView.mm | 22 +++++-------------- .../Private/ASIGListAdapterBasedDataSource.m | 6 +++-- 3 files changed, 12 insertions(+), 22 deletions(-) diff --git a/Source/ASCollectionNode.h b/Source/ASCollectionNode.h index b98ed007f..194f31f01 100644 --- a/Source/ASCollectionNode.h +++ b/Source/ASCollectionNode.h @@ -756,11 +756,9 @@ NS_ASSUME_NONNULL_BEGIN */ - (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode constrainedSizeForItemAtIndexPath:(NSIndexPath *)indexPath; -- (void)collectionNode:(ASCollectionNode *)collectionNode willDisplayNode:(ASCellNode *)node forItemAtIndexPath:(NSIndexPath *)indexPath; -- (void)collectionNode:(ASCollectionNode *)collectionNode willDisplayItemWithNode:(ASCellNode *)node ASDISPLAYNODE_DEPRECATED_MSG("This method has been renamed to -collectionNode:willDisplayNode:forItemAtIndexPath:"); +- (void)collectionNode:(ASCollectionNode *)collectionNode willDisplayItemWithNode:(ASCellNode *)node; -- (void)collectionNode:(ASCollectionNode *)collectionNode didEndDisplayingNode:(ASCellNode *)node forItemAtIndexPath:(NSIndexPath *)indexPath; -- (void)collectionNode:(ASCollectionNode *)collectionNode didEndDisplayingItemWithNode:(ASCellNode *)node ASDISPLAYNODE_DEPRECATED_MSG("This method has been renamed to -collectionNode:didEndDisplayingNode:forItemAtIndexPath:"); +- (void)collectionNode:(ASCollectionNode *)collectionNode didEndDisplayingItemWithNode:(ASCellNode *)node; - (void)collectionNode:(ASCollectionNode *)collectionNode willDisplaySupplementaryElementWithNode:(ASCellNode *)node NS_AVAILABLE_IOS(8_0); - (void)collectionNode:(ASCollectionNode *)collectionNode didEndDisplayingSupplementaryElementWithNode:(ASCellNode *)node; diff --git a/Source/ASCollectionView.mm b/Source/ASCollectionView.mm index d60bb7d9b..d19e59898 100644 --- a/Source/ASCollectionView.mm +++ b/Source/ASCollectionView.mm @@ -179,9 +179,7 @@ @interface ASCollectionView () )asyncDelegate _asyncDelegateFlags.collectionViewShouldShowMenuForItem = [_asyncDelegate respondsToSelector:@selector(collectionView:shouldShowMenuForItemAtIndexPath:)]; _asyncDelegateFlags.collectionViewCanPerformActionForItem = [_asyncDelegate respondsToSelector:@selector(collectionView:canPerformAction:forItemAtIndexPath:withSender:)]; _asyncDelegateFlags.collectionViewPerformActionForItem = [_asyncDelegate respondsToSelector:@selector(collectionView:performAction:forItemAtIndexPath:withSender:)]; - _asyncDelegateFlags.collectionNodeWillDisplayItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:willDisplayNode:forItemAtIndexPath:)]; - _asyncDelegateFlags.collectionNodeWillDisplayItemDeprecated = [_asyncDelegate respondsToSelector:@selector(collectionNode:willDisplayItemWithNode:)]; - _asyncDelegateFlags.collectionNodeDidEndDisplayingItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:didEndDisplayingNode:forItemAtIndexPath:)]; - _asyncDelegateFlags.collectionNodeDidEndDisplayingItemDeprecated = [_asyncDelegate respondsToSelector:@selector(collectionNode:didEndDisplayingItemWithNode:)]; + _asyncDelegateFlags.collectionNodeWillDisplayItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:willDisplayItemWithNode:)]; + _asyncDelegateFlags.collectionNodeDidEndDisplayingItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:didEndDisplayingItemWithNode:)]; _asyncDelegateFlags.collectionNodeWillBeginBatchFetch = [_asyncDelegate respondsToSelector:@selector(collectionNode:willBeginBatchFetchWithContext:)]; _asyncDelegateFlags.shouldBatchFetchForCollectionNode = [_asyncDelegate respondsToSelector:@selector(shouldBatchFetchForCollectionNode:)]; _asyncDelegateFlags.collectionNodeShouldSelectItem = [_asyncDelegate respondsToSelector:@selector(collectionNode:shouldSelectItemAtIndexPath:)]; @@ -1234,12 +1230,10 @@ - (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICol ASDisplayNodeAssertNotNil(cellNode, @"Expected node associated with cell that will be displayed not to be nil. indexPath: %@", indexPath); if (_asyncDelegateFlags.collectionNodeWillDisplayItem && self.collectionNode != nil) { - [_asyncDelegate collectionNode:self.collectionNode willDisplayNode:cellNode forItemAtIndexPath:indexPath]; - } else if (_asyncDelegateFlags.collectionNodeWillDisplayItem && self.collectionNode != nil) { -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_asyncDelegate collectionNode:self.collectionNode willDisplayItemWithNode:cellNode]; } else if (_asyncDelegateFlags.collectionViewWillDisplayNodeForItem) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" [_asyncDelegate collectionView:self willDisplayNode:cellNode forItemAtIndexPath:indexPath]; } else if (_asyncDelegateFlags.collectionViewWillDisplayNodeForItemDeprecated) { [_asyncDelegate collectionView:self willDisplayNodeForItemAtIndexPath:indexPath]; @@ -1281,15 +1275,11 @@ - (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:( if (_asyncDelegateFlags.collectionNodeDidEndDisplayingItem) { if (ASCollectionNode *collectionNode = self.collectionNode) { - [_asyncDelegate collectionNode:collectionNode didEndDisplayingNode:cellNode forItemAtIndexPath:indexPath]; + [_asyncDelegate collectionNode:collectionNode didEndDisplayingItemWithNode:cellNode]; } - } else if (_asyncDelegateFlags.collectionNodeDidEndDisplayingItemDeprecated) { - if (ASCollectionNode *collectionNode = self.collectionNode) { + } else if (_asyncDelegateFlags.collectionViewDidEndDisplayingNodeForItem) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - [_asyncDelegate collectionNode:collectionNode didEndDisplayingItemWithNode:cellNode]; - } - } else if (_asyncDelegateFlags.collectionViewDidEndDisplayingNodeForItem) { [_asyncDelegate collectionView:self didEndDisplayingNode:cellNode forItemAtIndexPath:indexPath]; #pragma clang diagnostic pop } diff --git a/Source/Private/ASIGListAdapterBasedDataSource.m b/Source/Private/ASIGListAdapterBasedDataSource.m index d1342698c..d3b846ed6 100644 --- a/Source/Private/ASIGListAdapterBasedDataSource.m +++ b/Source/Private/ASIGListAdapterBasedDataSource.m @@ -154,13 +154,15 @@ - (void)collectionNode:(ASCollectionNode *)collectionNode willBeginBatchFetchWit [ctrl beginBatchFetchWithContext:context]; } -- (void)collectionNode:(ASCollectionNode *)collectionNode willDisplayNode:(ASCellNode *)node forItemAtIndexPath:(NSIndexPath *)indexPath +- (void)collectionNode:(ASCollectionNode *)collectionNode willDisplayItemWithNode:(ASCellNode *)node { + NSIndexPath *indexPath = [collectionNode.view indexPathForNode:node]; [self.delegate collectionView:collectionNode.view willDisplayCell:node.view forItemAtIndexPath:indexPath]; } -- (void)collectionNode:(ASCollectionNode *)collectionNode didEndDisplayingNode:(ASCellNode *)node forItemAtIndexPath:(NSIndexPath *)indexPath +- (void)collectionNode:(ASCollectionNode *)collectionNode didEndDisplayingItemWithNode:(ASCellNode *)node { + NSIndexPath *indexPath = [collectionNode.view indexPathForNode:node]; [self.delegate collectionView:collectionNode.view didEndDisplayingCell:node.view forItemAtIndexPath:indexPath]; } From 7905d1edf3df8332bc62126d0868c445b0ffe501 Mon Sep 17 00:00:00 2001 From: Heberti Almeida Date: Wed, 11 Jul 2018 14:39:46 -0400 Subject: [PATCH 007/122] Sending a real UICollectionViewCell to the delegate if not nil --- .../Private/ASIGListAdapterBasedDataSource.m | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Source/Private/ASIGListAdapterBasedDataSource.m b/Source/Private/ASIGListAdapterBasedDataSource.m index d3b846ed6..54abd1211 100644 --- a/Source/Private/ASIGListAdapterBasedDataSource.m +++ b/Source/Private/ASIGListAdapterBasedDataSource.m @@ -157,13 +157,27 @@ - (void)collectionNode:(ASCollectionNode *)collectionNode willBeginBatchFetchWit - (void)collectionNode:(ASCollectionNode *)collectionNode willDisplayItemWithNode:(ASCellNode *)node { NSIndexPath *indexPath = [collectionNode.view indexPathForNode:node]; - [self.delegate collectionView:collectionNode.view willDisplayCell:node.view forItemAtIndexPath:indexPath]; + UIView *contentView = node.view.superview; + UICollectionViewCell *cell = contentView.superview; + + if (cell == nil) { + return; + } + + [self.delegate collectionView:collectionNode.view willDisplayCell:cell forItemAtIndexPath:indexPath]; } - (void)collectionNode:(ASCollectionNode *)collectionNode didEndDisplayingItemWithNode:(ASCellNode *)node { NSIndexPath *indexPath = [collectionNode.view indexPathForNode:node]; - [self.delegate collectionView:collectionNode.view didEndDisplayingCell:node.view forItemAtIndexPath:indexPath]; + UIView *contentView = node.view.superview; + UICollectionViewCell *cell = contentView.superview; + + if (cell == nil) { + return; + } + + [self.delegate collectionView:collectionNode.view didEndDisplayingCell:cell forItemAtIndexPath:indexPath]; } /** From d941a03653c51b114c88566957009b9084d3b12d Mon Sep 17 00:00:00 2001 From: Heberti Almeida Date: Wed, 11 Jul 2018 15:02:02 -0400 Subject: [PATCH 008/122] Added safety check for nil indexPaths --- Source/Private/ASIGListAdapterBasedDataSource.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Private/ASIGListAdapterBasedDataSource.m b/Source/Private/ASIGListAdapterBasedDataSource.m index 54abd1211..9393e8a1f 100644 --- a/Source/Private/ASIGListAdapterBasedDataSource.m +++ b/Source/Private/ASIGListAdapterBasedDataSource.m @@ -160,7 +160,7 @@ - (void)collectionNode:(ASCollectionNode *)collectionNode willDisplayItemWithNod UIView *contentView = node.view.superview; UICollectionViewCell *cell = contentView.superview; - if (cell == nil) { + if (cell == nil || indexPath == nil) { return; } @@ -173,7 +173,7 @@ - (void)collectionNode:(ASCollectionNode *)collectionNode didEndDisplayingItemWi UIView *contentView = node.view.superview; UICollectionViewCell *cell = contentView.superview; - if (cell == nil) { + if (cell == nil || indexPath == nil) { return; } From 05e2ce24aab409f042eec85ef149c39ff1ab4f8d Mon Sep 17 00:00:00 2001 From: Heberti Almeida Date: Tue, 19 Feb 2019 13:11:09 -0500 Subject: [PATCH 009/122] Discard any CHANGELOG changes for fix Danger error --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e90c1c536..0cddbfab4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -682,4 +682,4 @@ -\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* \ No newline at end of file +\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* From ac08bb8bb0cc8ebfff9d65fb614783624cd91a9f Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Thu, 21 Feb 2019 12:33:40 -0800 Subject: [PATCH 010/122] Add an experimental flag to use native dispatch_apply (#1345) * Add an experimental flag to use native dispatch_apply instead of our core count * 2 approach. This has shown performance wins in some profiling. * Add in other places --- Schemas/configuration.json | 3 ++- Source/ASExperimentalFeatures.h | 1 + Source/ASExperimentalFeatures.mm | 3 ++- Source/Private/ASDispatch.mm | 6 ++++++ Tests/ASConfigurationTests.mm | 6 ++++-- 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Schemas/configuration.json b/Schemas/configuration.json index 2126fa876..5fb3993b7 100644 --- a/Schemas/configuration.json +++ b/Schemas/configuration.json @@ -25,7 +25,8 @@ "exp_did_enter_preload_skip_asm_layout", "exp_disable_a11y_cache", "exp_skip_a11y_wait", - "exp_new_default_cell_layout_mode" + "exp_new_default_cell_layout_mode", + "exp_dispatch_apply" ] } } diff --git a/Source/ASExperimentalFeatures.h b/Source/ASExperimentalFeatures.h index 8061785a8..96e5127c4 100644 --- a/Source/ASExperimentalFeatures.h +++ b/Source/ASExperimentalFeatures.h @@ -31,6 +31,7 @@ typedef NS_OPTIONS(NSUInteger, ASExperimentalFeatures) { ASExperimentalDisableAccessibilityCache = 1 << 10, // exp_disable_a11y_cache ASExperimentalSkipAccessibilityWait = 1 << 11, // exp_skip_a11y_wait ASExperimentalNewDefaultCellLayoutMode = 1 << 12, // exp_new_default_cell_layout_mode + ASExperimentalDispatchApply = 1 << 13, // exp_dispatch_apply ASExperimentalFeatureAll = 0xFFFFFFFF }; diff --git a/Source/ASExperimentalFeatures.mm b/Source/ASExperimentalFeatures.mm index 3f005beed..bba80d5e8 100644 --- a/Source/ASExperimentalFeatures.mm +++ b/Source/ASExperimentalFeatures.mm @@ -24,7 +24,8 @@ @"exp_did_enter_preload_skip_asm_layout", @"exp_disable_a11y_cache", @"exp_skip_a11y_wait", - @"exp_new_default_cell_layout_mode"])); + @"exp_new_default_cell_layout_mode", + @"exp_dispatch_apply"])); if (flags == ASExperimentalFeatureAll) { return allNames; diff --git a/Source/Private/ASDispatch.mm b/Source/Private/ASDispatch.mm index 5c713db80..769a9185d 100644 --- a/Source/Private/ASDispatch.mm +++ b/Source/Private/ASDispatch.mm @@ -7,6 +7,8 @@ // #import +#import + // Prefer C atomics in this file because ObjC blocks can't capture C++ atomics well. #import @@ -19,6 +21,10 @@ */ void ASDispatchApply(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i)) { if (threadCount == 0) { + if (ASActivateExperimentalFeature(ASExperimentalDispatchApply)) { + dispatch_apply(iterationCount, queue, work); + return; + } threadCount = NSProcessInfo.processInfo.activeProcessorCount * 2; } dispatch_group_t group = dispatch_group_create(); diff --git a/Tests/ASConfigurationTests.mm b/Tests/ASConfigurationTests.mm index b5aeeb656..4dd8de2ab 100644 --- a/Tests/ASConfigurationTests.mm +++ b/Tests/ASConfigurationTests.mm @@ -30,7 +30,8 @@ ASExperimentalDidEnterPreloadSkipASMLayout, ASExperimentalDisableAccessibilityCache, ASExperimentalSkipAccessibilityWait, - ASExperimentalNewDefaultCellLayoutMode + ASExperimentalNewDefaultCellLayoutMode, + ASExperimentalDispatchApply }; @interface ASConfigurationTests : ASTestCase @@ -55,7 +56,8 @@ + (NSArray *)names { @"exp_did_enter_preload_skip_asm_layout", @"exp_disable_a11y_cache", @"exp_skip_a11y_wait", - @"exp_new_default_cell_layout_mode" + @"exp_new_default_cell_layout_mode", + @"exp_dispatch_apply" ]; } From 2fbc452788eeb2e7378333d0a780bd4c38f10e7e Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Thu, 21 Feb 2019 15:21:19 -0800 Subject: [PATCH 011/122] Don't return non-animated GIFs for animation (update to latest PINRemoteImage beta) (#940) * Don't return non-animated GIFs for animation * Update to latest PINRemoteImage beta --- Cartfile | 4 ++-- Podfile | 2 +- Source/Details/ASPINRemoteImageDownloader.mm | 2 +- Texture.podspec | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cartfile b/Cartfile index f1a449b01..ea145be2c 100644 --- a/Cartfile +++ b/Cartfile @@ -1,2 +1,2 @@ -github "pinterest/PINRemoteImage" "3.0.0-beta.13" -github "pinterest/PINCache" "3.0.1-beta.6" +github "pinterest/PINRemoteImage" "3.0.0-beta.14" +github "pinterest/PINCache" "3.0.1-beta.7" diff --git a/Podfile b/Podfile index dfb2350e8..98479737e 100644 --- a/Podfile +++ b/Podfile @@ -8,7 +8,7 @@ target :'AsyncDisplayKitTests' do pod 'JGMethodSwizzler', :git => 'https://github.com/JonasGessner/JGMethodSwizzler', :branch => 'master' # Only for buck build - pod 'PINRemoteImage', '3.0.0-beta.13' + pod 'PINRemoteImage', '3.0.0-beta.14' end #TODO CocoaPods plugin instead? diff --git a/Source/Details/ASPINRemoteImageDownloader.mm b/Source/Details/ASPINRemoteImageDownloader.mm index 35b1c919b..0276946f9 100644 --- a/Source/Details/ASPINRemoteImageDownloader.mm +++ b/Source/Details/ASPINRemoteImageDownloader.mm @@ -332,7 +332,7 @@ - (void)setPriority:(ASImageDownloaderPriority)priority withDownloadIdentifier:( - (id)alternateRepresentationWithData:(NSData *)data options:(PINRemoteImageManagerDownloadOptions)options { #if PIN_ANIMATED_AVAILABLE - if ([data pin_isGIF]) { + if ([data pin_isAnimatedGIF]) { return data; } #if PIN_WEBP_AVAILABLE diff --git a/Texture.podspec b/Texture.podspec index dbf43e04d..ef915ac59 100644 --- a/Texture.podspec +++ b/Texture.podspec @@ -39,7 +39,7 @@ Pod::Spec.new do |spec| end spec.subspec 'PINRemoteImage' do |pin| - pin.dependency 'PINRemoteImage/iOS', '= 3.0.0-beta.13' + pin.dependency 'PINRemoteImage/iOS', '= 3.0.0-beta.14' pin.dependency 'PINRemoteImage/PINCache' pin.dependency 'Texture/Core' end From 37e59558e4c86dc10ac761a1b4619b4ed94b7fb2 Mon Sep 17 00:00:00 2001 From: dirtmelon <272485887@qq.com> Date: Sat, 23 Feb 2019 03:28:26 +0800 Subject: [PATCH 012/122] Fix typos (#1348) * Update containers-overview.md * Update subclassing.md * Update node-overview.md ASImageNode, ASNetworkImageNode and ASMultiplexImageNode are in place of UIImageView, not UIImage. --- docs/_docs/containers-overview.md | 2 +- docs/_docs/node-overview.md | 2 +- docs/_docs/subclassing.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/_docs/containers-overview.md b/docs/_docs/containers-overview.md index ac8622ac9..cde2fb7f8 100755 --- a/docs/_docs/containers-overview.md +++ b/docs/_docs/containers-overview.md @@ -49,4 +49,4 @@ Example code and specific sample projects are highlighted in the documentation f A node container automatically manages the intelligent preloading of its nodes. This means that all of the node's layout measurement, data fetching, decoding and rendering will be done asynchronously. Among other conveniences, this is why it is recommended to use nodes within a container node. -Note that while it _is_ possible to use nodes directly (without an Texture node container), unless you add additional calls, they will only start displaying once they come onscreen (as UIKit does). This can lead to performance degredation and flashing of content. +Note that while it _is_ possible to use nodes directly (without a Texture node container), unless you add additional calls, they will only start displaying once they come onscreen (as UIKit does). This can lead to performance degradation and flashing of content. diff --git a/docs/_docs/node-overview.md b/docs/_docs/node-overview.md index c4f6d413c..0b699e2da 100755 --- a/docs/_docs/node-overview.md +++ b/docs/_docs/node-overview.md @@ -40,7 +40,7 @@ A key advantage of using nodes over UIKit components is that **all nodes perform ASImageNode
ASNetworkImageNode
ASMultiplexImageNode - in place of UIKit's UIImage + in place of UIKit's UIImageView ASVideoNode
diff --git a/docs/_docs/subclassing.md b/docs/_docs/subclassing.md index ea4cd24b9..8052e2093 100755 --- a/docs/_docs/subclassing.md +++ b/docs/_docs/subclassing.md @@ -27,7 +27,7 @@ This method defines the layout and does the heavy calculation on a **background The layout spec object that you create is malleable up until the point that it is return in this method. After this point, it will be immutable. It's important to remember not to cache layout specs for use later but instead to recreate them when necessary. -Because it is run on a background thread, you should not set any `node.view` or `node.layer` properties here. Also, unless you know what you are doing, do not create any nodes in this method. Additionally, it is not neccessary to begin this method with a call to super, unlike other method overrides. +Because it is run on a background thread, you should not set any `node.view` or `node.layer` properties here. Also, unless you know what you are doing, do not create any nodes in this method. Additionally, it is not necessary to begin this method with a call to super, unlike other method overrides. ### `-layout` From c284847da0fc5aa19a07de38281f80f02cb2ec71 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Fri, 22 Feb 2019 11:28:36 -0800 Subject: [PATCH 013/122] Revert "Have node and controller share lock (#1227)" (#1347) This reverts commit 2baa9438d798b9cf5689b46038383d0a9b7d007c. --- Source/ASNodeController+Beta.h | 13 ++++------- Source/ASNodeController+Beta.mm | 38 ++++++++++++++------------------- 2 files changed, 20 insertions(+), 31 deletions(-) diff --git a/Source/ASNodeController+Beta.h b/Source/ASNodeController+Beta.h index 56b7d936f..d7f4e116e 100644 --- a/Source/ASNodeController+Beta.h +++ b/Source/ASNodeController+Beta.h @@ -10,21 +10,18 @@ #import #import // for ASInterfaceState protocol -NS_ASSUME_NONNULL_BEGIN - /* ASNodeController is currently beta and open to change in the future */ @interface ASNodeController<__covariant DisplayNodeType : ASDisplayNode *> - : NSObject + : NSObject -@property (strong, readonly /* may be weak! */) DisplayNodeType node; +@property (nonatomic, strong /* may be weak! */) DisplayNodeType node; // Until an ASNodeController can be provided in place of an ASCellNode, some apps may prefer to have // nodes keep their controllers alive (and a weak reference from controller to node) @property (nonatomic) BOOL shouldInvertStrongReference; -// called on an arbitrary thread by the framework. You do not call this. Return a new node instance. -- (DisplayNodeType)createNode; +- (void)loadNode; // for descriptions see definition - (void)nodeDidLoad ASDISPLAYNODE_REQUIRES_SUPER; @@ -51,8 +48,6 @@ NS_ASSUME_NONNULL_BEGIN @interface ASDisplayNode (ASNodeController) -@property(nullable, readonly) ASNodeController *nodeController; +@property(nonatomic, readonly) ASNodeController *nodeController; @end - -NS_ASSUME_NONNULL_END diff --git a/Source/ASNodeController+Beta.mm b/Source/ASNodeController+Beta.mm index 56034c55c..767f23942 100644 --- a/Source/ASNodeController+Beta.mm +++ b/Source/ASNodeController+Beta.mm @@ -18,27 +18,22 @@ @implementation ASNodeController { ASDisplayNode *_strongNode; __weak ASDisplayNode *_weakNode; - ASDN::Mutex _nodeLock; + ASDN::RecursiveMutex __instanceLock__; } -- (ASDisplayNode *)createNode +- (void)loadNode { - return [[ASDisplayNode alloc] init]; + ASLockScopeSelf(); + self.node = [[ASDisplayNode alloc] init]; } - (ASDisplayNode *)node { - ASDN::MutexLocker l(_nodeLock); - ASDisplayNode *node = _node; - if (!node) { - node = [self createNode]; - if (!node) { - ASDisplayNodeCFailAssert(@"Returned nil from -createNode."); - node = [[ASDisplayNode alloc] init]; - } - [self setupReferencesWithNode:node]; + ASLockScopeSelf(); + if (_node == nil) { + [self loadNode]; } - return node; + return _node; } - (void)setupReferencesWithNode:(ASDisplayNode *)node @@ -58,6 +53,12 @@ - (void)setupReferencesWithNode:(ASDisplayNode *)node [node addInterfaceStateDelegate:self]; } +- (void)setNode:(ASDisplayNode *)node +{ + ASLockScopeSelf(); + [self setupReferencesWithNode:node]; +} + - (void)setShouldInvertStrongReference:(BOOL)shouldInvertStrongReference { ASLockScopeSelf(); @@ -92,19 +93,12 @@ - (void)hierarchyDisplayDidFinish {} - (void)lock { - [self.node lock]; + __instanceLock__.lock(); } - (void)unlock { - // Since the node was already locked on this thread, we don't need to call our accessor or take our lock. - ASDisplayNodeAssertNotNil(_node, @"Node deallocated while locked."); - [_node unlock]; -} - -- (BOOL)tryLock -{ - return [self.node tryLock]; + __instanceLock__.unlock(); } @end From 60ee8d4fa4aecb9257099d96f33e61ef8a048662 Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Sun, 3 Mar 2019 07:56:09 -0800 Subject: [PATCH 014/122] Don't compile out ASExperimentalTextNode if ASTextNode is compiled out (#1353) --- Source/ASExperimentalFeatures.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/ASExperimentalFeatures.h b/Source/ASExperimentalFeatures.h index 96e5127c4..87aee06aa 100644 --- a/Source/ASExperimentalFeatures.h +++ b/Source/ASExperimentalFeatures.h @@ -17,9 +17,9 @@ NS_ASSUME_NONNULL_BEGIN */ typedef NS_OPTIONS(NSUInteger, ASExperimentalFeatures) { ASExperimentalGraphicsContexts = 1 << 0, // exp_graphics_contexts -#if AS_ENABLE_TEXTNODE + // If AS_ENABLE_TEXTNODE=0 or TextNode2 subspec is used this setting is a no op and ASTextNode2 + // will be used in all cases ASExperimentalTextNode = 1 << 1, // exp_text_node -#endif ASExperimentalInterfaceStateCoalescing = 1 << 2, // exp_interface_state_coalesce ASExperimentalUnfairLock = 1 << 3, // exp_unfair_lock ASExperimentalLayerDefaults = 1 << 4, // exp_infer_layer_defaults From 71eb5a3ec99895e2f4395083ea6a138b8fc671c1 Mon Sep 17 00:00:00 2001 From: Kevin Date: Sun, 3 Mar 2019 08:05:01 -0800 Subject: [PATCH 015/122] Lock up to yogaRoot during layout to avoid deadlocks. (#1356) * Lock up to yogaRoot during layout to avoid dead lock. 1) lock to root for tree 2) lock self to change parent (& consequently root) 3) Implement ASLocking (tryLock) on ASNodeController 4) add lockPair to try-lock node & controller together 5) lock controllers if they exist in lockToRoot... Disable some asserts due to lock to root. :( LL# No commands remaining. * Add macro so non-Yoga still builds :) * wut --- Source/ASDisplayNode+Layout.mm | 90 +++++++++++++++++++++++++++++---- Source/ASDisplayNode+Yoga.h | 16 ++++++ Source/ASDisplayNode+Yoga.mm | 56 +++++++++----------- Source/ASDisplayNode.mm | 8 ++- Source/ASNodeController+Beta.h | 8 ++- Source/ASNodeController+Beta.mm | 19 +++++++ Source/ASScrollNode.mm | 4 +- 7 files changed, 153 insertions(+), 48 deletions(-) diff --git a/Source/ASDisplayNode+Layout.mm b/Source/ASDisplayNode+Layout.mm index 8301ebfe0..9fbecf39f 100644 --- a/Source/ASDisplayNode+Layout.mm +++ b/Source/ASDisplayNode+Layout.mm @@ -16,6 +16,8 @@ #import #import #import +#import +#import #pragma mark - ASDisplayNode (ASLayoutElement) @@ -65,7 +67,7 @@ - (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize - (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize parentSize:(CGSize)parentSize { - ASDN::MutexLocker l(__instanceLock__); + ASScopedLockSelfOrToRoot(); // If one or multiple layout transitions are in flight it still can happen that layout information is requested // on other threads. As the pending and calculated layout to be updated in the layout transition in here just a @@ -240,11 +242,11 @@ - (void)_u_setNeedsLayoutFromAbove } } +// TODO It would be easier to work with if we could `ASAssertUnlocked` here, but we +// cannot due to locking to root in `_u_measureNodeWithBoundsIfNecessary`. - (void)_rootNodeDidInvalidateSize { ASDisplayNodeAssertThreadAffinity(self); - ASAssertUnlocked(__instanceLock__); - __instanceLock__.lock(); // We are the root node and need to re-flow the layout; at least one child needs a new size. @@ -270,11 +272,21 @@ - (void)_rootNodeDidInvalidateSize } } +// TODO +// We should remove this logic, which is relatively new, and instead +// rely on the parent / host of the root node to do this size change. That's always been the +// expectation with other node containers like ASTableView, ASCollectionView, ASViewController, etc. +// E.g. in ASCellNode the _interactionDelegate is a Table or Collection that will resize in this +// case. By resizing without participating with the parent, we could get cases where our parent size +// does not match, especially if there is a size constraint that is applied at that level. +// +// In general a node should never need to set its own size, instead allowing its parent to do so - +// even in the root case. Anyhow this is a separate / pre-existing issue, but I think it could be +// causing real issues in cases of resizing nodes. - (void)displayNodeDidInvalidateSizeNewSize:(CGSize)size { ASDisplayNodeAssertThreadAffinity(self); - ASAssertUnlocked(__instanceLock__); - + // The default implementation of display node changes the size of itself to the new size CGRect oldBounds = self.bounds; CGSize oldSize = oldBounds.size; @@ -295,9 +307,9 @@ - (void)displayNodeDidInvalidateSizeNewSize:(CGSize)size - (void)_u_measureNodeWithBoundsIfNecessary:(CGRect)bounds { - ASAssertUnlocked(__instanceLock__); - - ASDN::MutexLocker l(__instanceLock__); + // ASAssertUnlocked(__instanceLock__); + ASScopedLockSelfOrToRoot(); + // Check if we are a subnode in a layout transition. // In this case no measurement is needed as it's part of the layout transition if ([self _locked_isLayoutTransitionInvalid]) { @@ -455,7 +467,7 @@ - (ASSizeRange)_locked_constrainedSizeForLayoutPass - (void)_layoutSublayouts { ASDisplayNodeAssertThreadAffinity(self); - ASAssertUnlocked(__instanceLock__); + // ASAssertUnlocked(__instanceLock__); ASLayout *layout; { @@ -620,7 +632,7 @@ - (void)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize NSUInteger newLayoutVersion = _layoutVersion; ASLayout *newLayout; { - ASDN::MutexLocker l(__instanceLock__); + ASScopedLockSelfOrToRoot(); ASLayoutElementContext *ctx = [[ASLayoutElementContext alloc] init]; ctx.transitionID = transitionID; @@ -1009,3 +1021,61 @@ - (void)_locked_setCalculatedDisplayNodeLayout:(const ASDisplayNodeLayout &)disp } @end + +#pragma mark - +#pragma mark - ASDisplayNode (YogaLayout) + +@implementation ASDisplayNode (YogaInternal) + +#pragma mark - +#pragma mark - ASDisplayNode (Yoga) + +- (BOOL)locked_shouldLayoutFromYogaRoot { +#if YOGA + YGNodeRef yogaNode = _style.yogaNode; + BOOL hasYogaParent = (_yogaParent != nil); + BOOL hasYogaChildren = (_yogaChildren.count > 0); + BOOL usesYoga = (yogaNode != NULL && (hasYogaParent || hasYogaChildren)); + if (usesYoga) { + if ([self shouldHaveYogaMeasureFunc] == NO) { + return YES; + } else { + return NO; + } + } else { + return NO; + } +#else + return NO; +#endif +} + +- (ASLockSet)lockToRootIfNeededForLayout { + ASLockSet lockSet = ASLockSequence(^BOOL(ASAddLockBlock addLock) { + if (!addLock(self)) { + return NO; + } + if (self.nodeController && !addLock(self.nodeController)) { + return NO; + } +#if YOGA + if (![self locked_shouldLayoutFromYogaRoot]) { + return YES; + } + ASDisplayNode *parent = _supernode; + while (parent) { + if (!addLock(parent)) { + return NO; + } + if (parent.nodeController && !addLock(parent.nodeController)) { + return NO; + } + parent = parent->_supernode; + } +#endif + return true; + }); + return lockSet; +} + +@end diff --git a/Source/ASDisplayNode+Yoga.h b/Source/ASDisplayNode+Yoga.h index 14b77697a..fc8d88464 100644 --- a/Source/ASDisplayNode+Yoga.h +++ b/Source/ASDisplayNode+Yoga.h @@ -33,6 +33,12 @@ AS_EXTERN void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode * _Nullab // Will walk up the Yoga tree and returns the root node - (ASDisplayNode *)yogaRoot; +/** + * @discussion Attempts(spinning) to lock all node up to root node when yoga is enabled. + * This will lock self when yoga is not enabled; + */ +- (ASLockSet)lockToRootIfNeededForLayout; + @end @@ -47,6 +53,11 @@ AS_EXTERN void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode * _Nullab - (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize; /// For internal usage only - (void)invalidateCalculatedYogaLayout; +/** + * @discussion return true only when yoga enabled and the node is in yoga tree and the node is + * not leaf that implemented measure function. + */ +- (BOOL)locked_shouldLayoutFromYogaRoot; @end @@ -79,4 +90,9 @@ AS_EXTERN void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode * _Nullab NS_ASSUME_NONNULL_END +// When Yoga is enabled, there are several points where we want to lock the tree to the root but otherwise (without Yoga) +// will want to simply lock self. +#define ASScopedLockSelfOrToRoot() ASScopedLockSet lockSet = [self lockToRootIfNeededForLayout] +#else +#define ASScopedLockSelfOrToRoot() ASLockScopeSelf() #endif diff --git a/Source/ASDisplayNode+Yoga.mm b/Source/ASDisplayNode+Yoga.mm index 27cedde8c..ecd44805d 100644 --- a/Source/ASDisplayNode+Yoga.mm +++ b/Source/ASDisplayNode+Yoga.mm @@ -46,7 +46,7 @@ - (ASDisplayNode *)yogaRoot - (void)setYogaChildren:(NSArray *)yogaChildren { - ASLockScope(self.yogaRoot); + ASScopedLockSelfOrToRoot(); for (ASDisplayNode *child in [_yogaChildren copy]) { // Make sure to un-associate the YGNodeRef tree before replacing _yogaChildren // If this becomes a performance bottleneck, it can be optimized by not doing the NSArray removals here. @@ -66,7 +66,7 @@ - (NSArray *)yogaChildren - (void)addYogaChild:(ASDisplayNode *)child { - ASLockScope(self.yogaRoot); + ASScopedLockSelfOrToRoot(); [self _locked_addYogaChild:child]; } @@ -77,7 +77,7 @@ - (void)_locked_addYogaChild:(ASDisplayNode *)child - (void)removeYogaChild:(ASDisplayNode *)child { - ASLockScope(self.yogaRoot); + ASScopedLockSelfOrToRoot(); [self _locked_removeYogaChild:child]; } @@ -95,7 +95,7 @@ - (void)_locked_removeYogaChild:(ASDisplayNode *)child - (void)insertYogaChild:(ASDisplayNode *)child atIndex:(NSUInteger)index { - ASLockScope(self.yogaRoot); + ASScopedLockSelfOrToRoot(); [self _locked_insertYogaChild:child atIndex:index]; } @@ -129,6 +129,7 @@ - (void)semanticContentAttributeDidChange:(UISemanticContentAttribute)attribute - (void)setYogaParent:(ASDisplayNode *)yogaParent { + ASLockScopeSelf(); if (_yogaParent == yogaParent) { return; } @@ -184,7 +185,7 @@ - (ASLayout *)layoutForYogaNode - (void)setupYogaCalculatedLayout { - ASLockScopeSelf(); + ASScopedLockSelfOrToRoot(); YGNodeRef yogaNode = self.style.yogaNode; uint32_t childCount = YGNodeGetChildCount(yogaNode); @@ -194,7 +195,7 @@ - (void)setupYogaCalculatedLayout ASLayout *rawSublayouts[childCount]; int i = 0; - for (ASDisplayNode *subnode in self.yogaChildren) { + for (ASDisplayNode *subnode in _yogaChildren) { rawSublayouts[i++] = [subnode layoutForYogaNode]; } const auto sublayouts = [NSArray arrayByTransferring:rawSublayouts count:childCount]; @@ -251,10 +252,11 @@ - (void)setupYogaCalculatedLayout - (BOOL)shouldHaveYogaMeasureFunc { + ASLockScopeSelf(); // Size calculation via calculateSizeThatFits: or layoutSpecThatFits: // For these nodes, we assume they may need custom Baseline calculation too. // This will be used for ASTextNode, as well as any other node that has no Yoga children - BOOL isLeafNode = (self.yogaChildren.count == 0); + BOOL isLeafNode = (_yogaChildren.count == 0); BOOL definesCustomLayout = [self implementsLayoutMethod]; return (isLeafNode && definesCustomLayout); } @@ -296,31 +298,24 @@ - (ASLayout *)calculateLayoutYoga:(ASSizeRange)constrainedSize // - This node is a Yoga tree root: it has no yogaParent, but has yogaChildren. // - This node is a Yoga tree node: it has both a yogaParent and yogaChildren. // - This node is a Yoga tree leaf: it has a yogaParent, but no yogaChidlren. - YGNodeRef yogaNode = _style.yogaNode; - BOOL hasYogaParent = (_yogaParent != nil); - BOOL hasYogaChildren = (_yogaChildren.count > 0); - BOOL usesYoga = (yogaNode != NULL && (hasYogaParent || hasYogaChildren)); - if (usesYoga) { - // This node has some connection to a Yoga tree. - if ([self shouldHaveYogaMeasureFunc] == NO) { - // If we're a yoga root, tree node, or leaf with no measure func (e.g. spacer), then - // initiate a new Yoga calculation pass from root. - - as_activity_create_for_scope("Yoga layout calculation"); - if (self.yogaLayoutInProgress == NO) { - ASYogaLog("Calculating yoga layout from root %@, %@", self, NSStringFromASSizeRange(constrainedSize)); - l.unlock(); - [self calculateLayoutFromYogaRoot:constrainedSize]; - l.lock(); - } else { - ASYogaLog("Reusing existing yoga layout %@", _yogaCalculatedLayout); - } - ASDisplayNodeAssert(_yogaCalculatedLayout, @"Yoga node should have a non-nil layout at this stage: %@", self); - return _yogaCalculatedLayout; + if ([self locked_shouldLayoutFromYogaRoot]) { + // If we're a yoga root, tree node, or leaf with no measure func (e.g. spacer), then + // initiate a new Yoga calculation pass from root. + as_activity_create_for_scope("Yoga layout calculation"); + if (self.yogaLayoutInProgress == NO) { + ASYogaLog("Calculating yoga layout from root %@, %@", self, + NSStringFromASSizeRange(constrainedSize)); + [self calculateLayoutFromYogaRoot:constrainedSize]; } else { - // If we're a yoga leaf node with custom measurement function, proceed with normal layout so layoutSpecs can run (e.g. ASButtonNode). - ASYogaLog("PROCEEDING past Yoga check to calculate ASLayout for: %@", self); + ASYogaLog("Reusing existing yoga layout %@", _yogaCalculatedLayout); } + ASDisplayNodeAssert(_yogaCalculatedLayout, + @"Yoga node should have a non-nil layout at this stage: %@", self); + return _yogaCalculatedLayout; + } else { + // If we're a yoga leaf node with custom measurement function, proceed with normal layout so + // layoutSpecs can run (e.g. ASButtonNode). + ASYogaLog("PROCEEDING past Yoga check to calculate ASLayout for: %@", self); } // Delegate to layout spec layout for nodes that do not support Yoga @@ -345,7 +340,6 @@ - (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize } }]; - ASLockScopeSelf(); // Prepare all children for the layout pass with the current Yoga tree configuration. ASDisplayNodePerformBlockOnEveryYogaChild(self, ^(ASDisplayNode *_Nonnull node) { diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index 2ca35422e..10f13c5f8 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -1046,7 +1046,7 @@ - (void)invalidateCalculatedLayout - (void)__layout { ASDisplayNodeAssertThreadAffinity(self); - ASAssertUnlocked(__instanceLock__); + // ASAssertUnlocked(__instanceLock__); BOOL loaded = NO; { @@ -1096,7 +1096,7 @@ - (void)__layout - (void)_layoutDidFinish { ASDisplayNodeAssertMainThread(); - ASAssertUnlocked(__instanceLock__); + // ASAssertUnlocked(__instanceLock__); ASDisplayNodeAssertTrue(self.isNodeLoaded); [self layoutDidFinish]; } @@ -1169,7 +1169,7 @@ - (void)layout { // Hook for subclasses ASDisplayNodeAssertMainThread(); - ASAssertUnlocked(__instanceLock__); + // ASAssertUnlocked(__instanceLock__); ASDisplayNodeAssertTrue(self.isNodeLoaded); [self enumerateInterfaceStateDelegates:^(id del) { [del nodeDidLayout]; @@ -2684,7 +2684,6 @@ - (void)__enterHierarchy { ASDisplayNodeAssertMainThread(); ASDisplayNodeAssert(!_flags.isEnteringHierarchy, @"Should not cause recursive __enterHierarchy"); - ASAssertUnlocked(__instanceLock__); ASDisplayNodeLogEvent(self, @"enterHierarchy"); // Profiling has shown that locking this method is beneficial, so each of the property accesses don't have to lock and unlock. @@ -2733,7 +2732,6 @@ - (void)__exitHierarchy { ASDisplayNodeAssertMainThread(); ASDisplayNodeAssert(!_flags.isExitingHierarchy, @"Should not cause recursive __exitHierarchy"); - ASAssertUnlocked(__instanceLock__); ASDisplayNodeLogEvent(self, @"exitHierarchy"); // Profiling has shown that locking this method is beneficial, so each of the property accesses don't have to lock and unlock. diff --git a/Source/ASNodeController+Beta.h b/Source/ASNodeController+Beta.h index d7f4e116e..e9028f198 100644 --- a/Source/ASNodeController+Beta.h +++ b/Source/ASNodeController+Beta.h @@ -12,7 +12,7 @@ /* ASNodeController is currently beta and open to change in the future */ @interface ASNodeController<__covariant DisplayNodeType : ASDisplayNode *> - : NSObject + : NSObject @property (nonatomic, strong /* may be weak! */) DisplayNodeType node; @@ -44,6 +44,12 @@ - (void)hierarchyDisplayDidFinish ASDISPLAYNODE_REQUIRES_SUPER; +/** + * @discussion Attempts (via ASLockSequence, a backing-off spinlock similar to + * std::lock()) to lock both the node and its ASNodeController, if one exists. + */ +- (ASLockSet)lockPair; + @end @interface ASDisplayNode (ASNodeController) diff --git a/Source/ASNodeController+Beta.mm b/Source/ASNodeController+Beta.mm index 767f23942..83f438239 100644 --- a/Source/ASNodeController+Beta.mm +++ b/Source/ASNodeController+Beta.mm @@ -89,6 +89,20 @@ - (void)interfaceStateDidChange:(ASInterfaceState)newState - (void)hierarchyDisplayDidFinish {} +- (ASLockSet)lockPair { + ASLockSet lockSet = ASLockSequence(^BOOL(ASAddLockBlock addLock) { + if (!addLock(_node)) { + return NO; + } + if (!addLock(self)) { + return NO; + } + return YES; + }); + + return lockSet; +} + #pragma mark NSLocking - (void)lock @@ -101,6 +115,11 @@ - (void)unlock __instanceLock__.unlock(); } +- (BOOL)tryLock +{ + return __instanceLock__.try_lock(); +} + @end @implementation ASDisplayNode (ASNodeController) diff --git a/Source/ASScrollNode.mm b/Source/ASScrollNode.mm index 497ef1429..c6e60e619 100644 --- a/Source/ASScrollNode.mm +++ b/Source/ASScrollNode.mm @@ -10,10 +10,12 @@ #import #import #import +#import #import #import #import #import +#import @interface ASScrollView : UIScrollView @end @@ -79,7 +81,7 @@ - (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize restrictedToSize:(ASLayoutElementSize)size relativeToParentSize:(CGSize)parentSize { - ASLockScopeSelf(); // Lock for using our instance variables. + ASScopedLockSelfOrToRoot(); ASSizeRange contentConstrainedSize = constrainedSize; if (ASScrollDirectionContainsVerticalDirection(_scrollableDirections)) { From 72d4d79f5de3815409cc7fcb86c69d2699913e65 Mon Sep 17 00:00:00 2001 From: dirtmelon <0xffdirtmelon@gmail.com> Date: Mon, 4 Mar 2019 00:05:44 +0800 Subject: [PATCH 016/122] Update layout-transition-api.md (#1357) --- docs/_docs/layout-transition-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_docs/layout-transition-api.md b/docs/_docs/layout-transition-api.md index e62eaadb3..e1f7afc70 100755 --- a/docs/_docs/layout-transition-api.md +++ b/docs/_docs/layout-transition-api.md @@ -8,7 +8,7 @@ nextPage: hit-test-slop.html The Layout Transition API was designed to make all animations with Texture easy - even transforming an entire set of views into a completely different set of views! -With this system, you simply specify the desired layout and Texture will do the work to figure out differences from the current layout. It will automatically add new elements, remove unneeded elements after the transiton, and update the position of any existing elements. +With this system, you simply specify the desired layout and Texture will do the work to figure out differences from the current layout. It will automatically add new elements, remove unneeded elements after the transition, and update the position of any existing elements. There are also easy to use APIs that allow you to fully customize the starting position of newly introduced elements, as well as the ending position of removed elements. From 877b6cd30849a4bc0724929e5dec902e915dd136 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Sun, 3 Mar 2019 12:49:36 -0600 Subject: [PATCH 017/122] Optimize ASTwoDimensionalArrayUtils (#1351) * Optimize ASTwoDimensionalArrayUtils These methods are called on the main thread during range controller updates (i.e. every frame) and so they should be as fast as possible. * Rename * Use vector instead of stack array to handle really big cases (e.g. photos) --- Source/Private/ASTwoDimensionalArrayUtils.mm | 42 +++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/Source/Private/ASTwoDimensionalArrayUtils.mm b/Source/Private/ASTwoDimensionalArrayUtils.mm index 8b4836fe8..67267b7a9 100644 --- a/Source/Private/ASTwoDimensionalArrayUtils.mm +++ b/Source/Private/ASTwoDimensionalArrayUtils.mm @@ -8,9 +8,12 @@ // #import +#import #import #import +#import + // Import UIKit to get [NSIndexPath indexPathForItem:inSection:] which uses // tagged pointers. #import @@ -63,31 +66,42 @@ void ASDeleteElementsInTwoDimensionalArrayAtIndexPaths(NSMutableArray *mutableAr NSArray *ASIndexPathsForTwoDimensionalArray(NSArray * twoDimensionalArray) { - NSMutableArray *result = [[NSMutableArray alloc] init]; - NSInteger section = 0; + NSInteger sectionCount = twoDimensionalArray.count; + NSInteger counts[sectionCount]; + NSInteger totalCount = 0; NSInteger i = 0; for (NSArray *subarray in twoDimensionalArray) { - ASDisplayNodeCAssert([subarray isKindOfClass:[NSArray class]], @"This function expects NSArray *"); - NSInteger itemCount = subarray.count; - for (NSInteger item = 0; item < itemCount; item++) { - result[i++] = [NSIndexPath indexPathForItem:item inSection:section]; + NSInteger count = subarray.count; + counts[i++] = count; + totalCount += count; + } + + // Count could be huge. Use a reserved vector rather than VLA (stack.) + std::vector indexPaths; + indexPaths.reserve(totalCount); + for (NSInteger i = 0; i < sectionCount; i++) { + for (NSInteger j = 0; j < counts[i]; j++) { + indexPaths.push_back([NSIndexPath indexPathForItem:j inSection:i]); } - section++; } - return result; + return [NSArray arrayByTransferring:indexPaths.data() count:totalCount]; } NSArray *ASElementsInTwoDimensionalArray(NSArray * twoDimensionalArray) { - NSMutableArray *result = [[NSMutableArray alloc] init]; - NSInteger i = 0; + NSInteger totalCount = 0; for (NSArray *subarray in twoDimensionalArray) { - ASDisplayNodeCAssert([subarray isKindOfClass:[NSArray class]], @"This function expects NSArray *"); - for (id element in subarray) { - result[i++] = element; + totalCount += subarray.count; + } + + std::vector elements; + elements.reserve(totalCount); + for (NSArray *subarray in twoDimensionalArray) { + for (id object in subarray) { + elements.push_back(object); } } - return result; + return [NSArray arrayByTransferring:elements.data() count:totalCount]; } id ASGetElementInTwoDimensionalArray(NSArray *array, NSIndexPath *indexPath) From 378e0d2feff49863eaaecc05f19a042922bf0b1b Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Sun, 3 Mar 2019 17:33:32 -0800 Subject: [PATCH 018/122] If we check for batching before content size is available we'll always fetch (#1355) --- Source/ASCollectionView.mm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/ASCollectionView.mm b/Source/ASCollectionView.mm index 395b6938a..21ff2a4a9 100644 --- a/Source/ASCollectionView.mm +++ b/Source/ASCollectionView.mm @@ -1685,7 +1685,8 @@ - (void)setLeadingScreensForBatching:(CGFloat)leadingScreensForBatching { if (_leadingScreensForBatching != leadingScreensForBatching) { _leadingScreensForBatching = leadingScreensForBatching; - ASPerformBlockOnMainThread(^{ + // Push this to the next runloop to be sure the scroll view has the right content size + dispatch_async(dispatch_get_main_queue(), ^{ [self _checkForBatchFetching]; }); } From 49dd925ac0882565c1244b37a37079b65cbcfc4f Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 4 Mar 2019 17:20:47 -0800 Subject: [PATCH 019/122] Do not lock the nodeController if we are not locking to root. (#1360) Doing so leaves it locked when we elsewhere explicitly unlock the node to ascend. --- Source/ASDisplayNode+Layout.mm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/ASDisplayNode+Layout.mm b/Source/ASDisplayNode+Layout.mm index 9fbecf39f..d17136efe 100644 --- a/Source/ASDisplayNode+Layout.mm +++ b/Source/ASDisplayNode+Layout.mm @@ -1055,13 +1055,13 @@ - (ASLockSet)lockToRootIfNeededForLayout { if (!addLock(self)) { return NO; } - if (self.nodeController && !addLock(self.nodeController)) { - return NO; - } #if YOGA if (![self locked_shouldLayoutFromYogaRoot]) { return YES; } + if (self.nodeController && !addLock(self.nodeController)) { + return NO; + } ASDisplayNode *parent = _supernode; while (parent) { if (!addLock(parent)) { From fd223d0b2ea0b831fc9e2a36d2cd604a4154a06e Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 4 Mar 2019 17:21:01 -0800 Subject: [PATCH 020/122] Resolve root constrained size before informing delegate to resolve infinite layout loop. (#1359) --- Source/ASDisplayNode+Yoga.mm | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/ASDisplayNode+Yoga.mm b/Source/ASDisplayNode+Yoga.mm index ecd44805d..b8005e645 100644 --- a/Source/ASDisplayNode+Yoga.mm +++ b/Source/ASDisplayNode+Yoga.mm @@ -333,6 +333,10 @@ - (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize return; } + if (ASSizeRangeEqualToSizeRange(rootConstrainedSize, ASSizeRangeUnconstrained)) { + rootConstrainedSize = [self _locked_constrainedSizeForLayoutPass]; + } + [self willCalculateLayout:rootConstrainedSize]; [self enumerateInterfaceStateDelegates:^(id _Nonnull delegate) { if ([delegate respondsToSelector:@selector(nodeWillCalculateLayout:)]) { @@ -352,10 +356,6 @@ - (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize }; }); - if (ASSizeRangeEqualToSizeRange(rootConstrainedSize, ASSizeRangeUnconstrained)) { - rootConstrainedSize = [self _locked_constrainedSizeForLayoutPass]; - } - ASYogaLog("CALCULATING at Yoga root with constraint = {%@, %@}: %@", NSStringFromCGSize(rootConstrainedSize.min), NSStringFromCGSize(rootConstrainedSize.max), self); From f3c2ad7a2290bdf3088a84020d7554a0b583e559 Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 4 Mar 2019 17:33:28 -0800 Subject: [PATCH 021/122] Add `setNeedsLayout` to yoga tree changes. (#1361) --- Source/ASDisplayNode+Layout.mm | 16 ++- Source/ASDisplayNode+Yoga.mm | 2 + Source/Layout/ASLayoutElement.mm | 226 +++++++++++++++++++------------ 3 files changed, 154 insertions(+), 90 deletions(-) diff --git a/Source/ASDisplayNode+Layout.mm b/Source/ASDisplayNode+Layout.mm index d17136efe..7d5e00abc 100644 --- a/Source/ASDisplayNode+Layout.mm +++ b/Source/ASDisplayNode+Layout.mm @@ -19,6 +19,19 @@ #import #import +@interface ASDisplayNode (ASLayoutElementStyleDelegate) +@end + +@implementation ASDisplayNode (ASLayoutElementStyleDelegate) + +#pragma mark + +- (void)style:(ASLayoutElementStyle *)style propertyDidChange:(NSString *)propertyName { + [self setNeedsLayout]; +} + +@end + #pragma mark - ASDisplayNode (ASLayoutElement) @implementation ASDisplayNode (ASLayoutElement) @@ -42,8 +55,9 @@ - (ASLayoutElementStyle *)style - (ASLayoutElementStyle *)_locked_style { + ASAssertLocked(__instanceLock__); if (_style == nil) { - _style = [[ASLayoutElementStyle alloc] init]; + _style = [[ASLayoutElementStyle alloc] initWithDelegate:self]; } return _style; } diff --git a/Source/ASDisplayNode+Yoga.mm b/Source/ASDisplayNode+Yoga.mm index b8005e645..0f975c885 100644 --- a/Source/ASDisplayNode+Yoga.mm +++ b/Source/ASDisplayNode+Yoga.mm @@ -91,6 +91,7 @@ - (void)_locked_removeYogaChild:(ASDisplayNode *)child // YGNodeRef removal is done in setParent: child.yogaParent = nil; + [self setNeedsLayout]; } - (void)insertYogaChild:(ASDisplayNode *)child atIndex:(NSUInteger)index @@ -115,6 +116,7 @@ - (void)_locked_insertYogaChild:(ASDisplayNode *)child atIndex:(NSUInteger)index // YGNodeRef insertion is done in setParent: child.yogaParent = self; + [self setNeedsLayout]; } #pragma mark - Subclass Hooks diff --git a/Source/Layout/ASLayoutElement.mm b/Source/Layout/ASLayoutElement.mm index 5a33a11f4..7cc168083 100644 --- a/Source/Layout/ASLayoutElement.mm +++ b/Source/Layout/ASLayoutElement.mm @@ -139,12 +139,19 @@ void ASLayoutElementPopContext() NSString * const ASYogaAspectRatioProperty = @"ASYogaAspectRatioProperty"; #endif -#define ASLayoutElementStyleSetSizeWithScope(x) \ - __instanceLock__.lock(); \ - ASLayoutElementSize newSize = _size.load(); \ - { x } \ - _size.store(newSize); \ - __instanceLock__.unlock(); +#define ASLayoutElementStyleSetSizeWithScope(x) \ + ({ \ + __instanceLock__.lock(); \ + const ASLayoutElementSize oldSize = _size.load(); \ + ASLayoutElementSize newSize = oldSize; \ + {x}; \ + BOOL changed = !ASLayoutElementSizeEqualToLayoutElementSize(oldSize, newSize); \ + if (changed) { \ + _size.store(newSize); \ + } \ + __instanceLock__.unlock(); \ + changed; \ + }) #define ASLayoutElementStyleCallDelegate(propertyName)\ do {\ @@ -202,9 +209,13 @@ - (instancetype)init { self = [super init]; if (self) { - _size = ASLayoutElementSizeMake(); + std::atomic_init(&_size, ASLayoutElementSizeMake()); + std::atomic_init(&_flexBasis, ASDimensionAuto); #if YOGA _parentAlignStyle = ASStackLayoutAlignItemsNotSet; + std::atomic_init(&_flexDirection, ASStackLayoutDirectionVertical); + std::atomic_init(&_alignItems, ASStackLayoutAlignItemsStretch); + std::atomic_init(&_aspectRatio, static_cast(YGUndefined)); #endif } return self; @@ -236,10 +247,10 @@ - (ASDimension)width - (void)setWidth:(ASDimension)width { - ASLayoutElementStyleSetSizeWithScope({ - newSize.width = width; - }); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleWidthProperty); + BOOL changed = ASLayoutElementStyleSetSizeWithScope({ newSize.width = width; }); + if (changed) { + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleWidthProperty); + } } - (ASDimension)height @@ -249,10 +260,10 @@ - (ASDimension)height - (void)setHeight:(ASDimension)height { - ASLayoutElementStyleSetSizeWithScope({ - newSize.height = height; - }); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleHeightProperty); + BOOL changed = ASLayoutElementStyleSetSizeWithScope({ newSize.height = height; }); + if (changed) { + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleHeightProperty); + } } - (ASDimension)minWidth @@ -262,10 +273,10 @@ - (ASDimension)minWidth - (void)setMinWidth:(ASDimension)minWidth { - ASLayoutElementStyleSetSizeWithScope({ - newSize.minWidth = minWidth; - }); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinWidthProperty); + BOOL changed = ASLayoutElementStyleSetSizeWithScope({ newSize.minWidth = minWidth; }); + if (changed) { + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinWidthProperty); + } } - (ASDimension)maxWidth @@ -275,10 +286,10 @@ - (ASDimension)maxWidth - (void)setMaxWidth:(ASDimension)maxWidth { - ASLayoutElementStyleSetSizeWithScope({ - newSize.maxWidth = maxWidth; - }); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxWidthProperty); + BOOL changed = ASLayoutElementStyleSetSizeWithScope({ newSize.maxWidth = maxWidth; }); + if (changed) { + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxWidthProperty); + } } - (ASDimension)minHeight @@ -288,10 +299,10 @@ - (ASDimension)minHeight - (void)setMinHeight:(ASDimension)minHeight { - ASLayoutElementStyleSetSizeWithScope({ - newSize.minHeight = minHeight; - }); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinHeightProperty); + BOOL changed = ASLayoutElementStyleSetSizeWithScope({ newSize.minHeight = minHeight; }); + if (changed) { + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinHeightProperty); + } } - (ASDimension)maxHeight @@ -301,10 +312,10 @@ - (ASDimension)maxHeight - (void)setMaxHeight:(ASDimension)maxHeight { - ASLayoutElementStyleSetSizeWithScope({ - newSize.maxHeight = maxHeight; - }); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxHeightProperty); + BOOL changed = ASLayoutElementStyleSetSizeWithScope({ newSize.maxHeight = maxHeight; }); + if (changed) { + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxHeightProperty); + } } @@ -312,12 +323,14 @@ - (void)setMaxHeight:(ASDimension)maxHeight - (void)setPreferredSize:(CGSize)preferredSize { - ASLayoutElementStyleSetSizeWithScope({ + BOOL changed = ASLayoutElementStyleSetSizeWithScope({ newSize.width = ASDimensionMakeWithPoints(preferredSize.width); newSize.height = ASDimensionMakeWithPoints(preferredSize.height); }); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleWidthProperty); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleHeightProperty); + if (changed) { + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleWidthProperty); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleHeightProperty); + } } - (CGSize)preferredSize @@ -338,22 +351,26 @@ - (CGSize)preferredSize - (void)setMinSize:(CGSize)minSize { - ASLayoutElementStyleSetSizeWithScope({ + BOOL changed = ASLayoutElementStyleSetSizeWithScope({ newSize.minWidth = ASDimensionMakeWithPoints(minSize.width); newSize.minHeight = ASDimensionMakeWithPoints(minSize.height); }); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinWidthProperty); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinHeightProperty); + if (changed) { + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinWidthProperty); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinHeightProperty); + } } - (void)setMaxSize:(CGSize)maxSize { - ASLayoutElementStyleSetSizeWithScope({ + BOOL changed = ASLayoutElementStyleSetSizeWithScope({ newSize.maxWidth = ASDimensionMakeWithPoints(maxSize.width); newSize.maxHeight = ASDimensionMakeWithPoints(maxSize.height); }); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxWidthProperty); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxHeightProperty); + if (changed) { + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxWidthProperty); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxHeightProperty); + } } - (ASLayoutSize)preferredLayoutSize @@ -364,12 +381,14 @@ - (ASLayoutSize)preferredLayoutSize - (void)setPreferredLayoutSize:(ASLayoutSize)preferredLayoutSize { - ASLayoutElementStyleSetSizeWithScope({ + BOOL changed = ASLayoutElementStyleSetSizeWithScope({ newSize.width = preferredLayoutSize.width; newSize.height = preferredLayoutSize.height; }); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleWidthProperty); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleHeightProperty); + if (changed) { + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleWidthProperty); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleHeightProperty); + } } - (ASLayoutSize)minLayoutSize @@ -380,12 +399,14 @@ - (ASLayoutSize)minLayoutSize - (void)setMinLayoutSize:(ASLayoutSize)minLayoutSize { - ASLayoutElementStyleSetSizeWithScope({ + BOOL changed = ASLayoutElementStyleSetSizeWithScope({ newSize.minWidth = minLayoutSize.width; newSize.minHeight = minLayoutSize.height; }); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinWidthProperty); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinHeightProperty); + if (changed) { + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinWidthProperty); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinHeightProperty); + } } - (ASLayoutSize)maxLayoutSize @@ -396,20 +417,23 @@ - (ASLayoutSize)maxLayoutSize - (void)setMaxLayoutSize:(ASLayoutSize)maxLayoutSize { - ASLayoutElementStyleSetSizeWithScope({ + BOOL changed = ASLayoutElementStyleSetSizeWithScope({ newSize.maxWidth = maxLayoutSize.width; newSize.maxHeight = maxLayoutSize.height; }); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxWidthProperty); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxHeightProperty); + if (changed) { + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxWidthProperty); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxHeightProperty); + } } #pragma mark - ASStackLayoutElement - (void)setSpacingBefore:(CGFloat)spacingBefore { - _spacingBefore.store(spacingBefore); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleSpacingBeforeProperty); + if (_spacingBefore.exchange(spacingBefore) != spacingBefore) { + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleSpacingBeforeProperty); + } } - (CGFloat)spacingBefore @@ -419,8 +443,9 @@ - (CGFloat)spacingBefore - (void)setSpacingAfter:(CGFloat)spacingAfter { - _spacingAfter.store(spacingAfter); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleSpacingAfterProperty); + if (_spacingAfter.exchange(spacingAfter) != spacingAfter) { + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleSpacingAfterProperty); + } } - (CGFloat)spacingAfter @@ -430,8 +455,9 @@ - (CGFloat)spacingAfter - (void)setFlexGrow:(CGFloat)flexGrow { - _flexGrow.store(flexGrow); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleFlexGrowProperty); + if (_flexGrow.exchange(flexGrow) != flexGrow) { + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleFlexGrowProperty); + } } - (CGFloat)flexGrow @@ -441,8 +467,9 @@ - (CGFloat)flexGrow - (void)setFlexShrink:(CGFloat)flexShrink { - _flexShrink.store(flexShrink); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleFlexShrinkProperty); + if (_flexShrink.exchange(flexShrink) != flexShrink) { + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleFlexShrinkProperty); + } } - (CGFloat)flexShrink @@ -452,8 +479,9 @@ - (CGFloat)flexShrink - (void)setFlexBasis:(ASDimension)flexBasis { - _flexBasis.store(flexBasis); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleFlexBasisProperty); + if (!ASDimensionEqualToDimension(_flexBasis.exchange(flexBasis), flexBasis)) { + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleFlexBasisProperty); + } } - (ASDimension)flexBasis @@ -463,8 +491,9 @@ - (ASDimension)flexBasis - (void)setAlignSelf:(ASStackLayoutAlignSelf)alignSelf { - _alignSelf.store(alignSelf); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleAlignSelfProperty); + if (_alignSelf.exchange(alignSelf) != alignSelf) { + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleAlignSelfProperty); + } } - (ASStackLayoutAlignSelf)alignSelf @@ -474,8 +503,9 @@ - (ASStackLayoutAlignSelf)alignSelf - (void)setAscender:(CGFloat)ascender { - _ascender.store(ascender); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleAscenderProperty); + if (_ascender.exchange(ascender) != ascender) { + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleAscenderProperty); + } } - (CGFloat)ascender @@ -485,8 +515,9 @@ - (CGFloat)ascender - (void)setDescender:(CGFloat)descender { - _descender.store(descender); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleDescenderProperty); + if (_descender.exchange(descender) != descender) { + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleDescenderProperty); + } } - (CGFloat)descender @@ -498,8 +529,9 @@ - (CGFloat)descender - (void)setLayoutPosition:(CGPoint)layoutPosition { - _layoutPosition.store(layoutPosition); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleLayoutPositionProperty); + if (!CGPointEqualToPoint(_layoutPosition.exchange(layoutPosition), layoutPosition)) { + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleLayoutPositionProperty); + } } - (CGPoint)layoutPosition @@ -787,48 +819,64 @@ - (ASStackLayoutAlignItems)parentAlignStyle { } - (void)setFlexWrap:(YGWrap)flexWrap { - _flexWrap.store(flexWrap); - ASLayoutElementStyleCallDelegate(ASYogaFlexWrapProperty); + if (_flexWrap.exchange(flexWrap) != flexWrap) { + ASLayoutElementStyleCallDelegate(ASYogaFlexWrapProperty); + } } - (void)setFlexDirection:(ASStackLayoutDirection)flexDirection { - _flexDirection.store(flexDirection); - ASLayoutElementStyleCallDelegate(ASYogaFlexDirectionProperty); + if (_flexDirection.exchange(flexDirection) != flexDirection) { + ASLayoutElementStyleCallDelegate(ASYogaFlexDirectionProperty); + } } - (void)setDirection:(YGDirection)direction { - _direction.store(direction); - ASLayoutElementStyleCallDelegate(ASYogaDirectionProperty); + if (_direction.exchange(direction) != direction) { + ASLayoutElementStyleCallDelegate(ASYogaDirectionProperty); + } } - (void)setJustifyContent:(ASStackLayoutJustifyContent)justify { - _justifyContent.store(justify); - ASLayoutElementStyleCallDelegate(ASYogaJustifyContentProperty); + if (_justifyContent.exchange(justify) != justify) { + ASLayoutElementStyleCallDelegate(ASYogaJustifyContentProperty); + } } - (void)setAlignItems:(ASStackLayoutAlignItems)alignItems { - _alignItems.store(alignItems); - ASLayoutElementStyleCallDelegate(ASYogaAlignItemsProperty); + if (_alignItems.exchange(alignItems) != alignItems) { + ASLayoutElementStyleCallDelegate(ASYogaAlignItemsProperty); + } } - (void)setPositionType:(YGPositionType)positionType { - _positionType.store(positionType); - ASLayoutElementStyleCallDelegate(ASYogaPositionTypeProperty); + if (_positionType.exchange(positionType) != positionType) { + ASLayoutElementStyleCallDelegate(ASYogaPositionTypeProperty); + } } +/// TODO: smart compare ASEdgeInsets instead of memory compare. - (void)setPosition:(ASEdgeInsets)position { - _position.store(position); - ASLayoutElementStyleCallDelegate(ASYogaPositionProperty); + ASEdgeInsets oldValue = _position.exchange(position); + if (0 != memcmp(&position, &oldValue, sizeof(ASEdgeInsets))) { + ASLayoutElementStyleCallDelegate(ASYogaPositionProperty); + } } - (void)setMargin:(ASEdgeInsets)margin { - _margin.store(margin); - ASLayoutElementStyleCallDelegate(ASYogaMarginProperty); + ASEdgeInsets oldValue = _margin.exchange(margin); + if (0 != memcmp(&margin, &oldValue, sizeof(ASEdgeInsets))) { + ASLayoutElementStyleCallDelegate(ASYogaMarginProperty); + } } - (void)setPadding:(ASEdgeInsets)padding { - _padding.store(padding); - ASLayoutElementStyleCallDelegate(ASYogaPaddingProperty); + ASEdgeInsets oldValue = _padding.exchange(padding); + if (0 != memcmp(&padding, &oldValue, sizeof(ASEdgeInsets))) { + ASLayoutElementStyleCallDelegate(ASYogaPaddingProperty); + } } - (void)setBorder:(ASEdgeInsets)border { - _border.store(border); - ASLayoutElementStyleCallDelegate(ASYogaBorderProperty); + ASEdgeInsets oldValue = _border.exchange(border); + if (0 != memcmp(&border, &oldValue, sizeof(ASEdgeInsets))) { + ASLayoutElementStyleCallDelegate(ASYogaBorderProperty); + } } - (void)setAspectRatio:(CGFloat)aspectRatio { - _aspectRatio.store(aspectRatio); - ASLayoutElementStyleCallDelegate(ASYogaAspectRatioProperty); + if (_aspectRatio.exchange(aspectRatio) != aspectRatio) { + ASLayoutElementStyleCallDelegate(ASYogaAspectRatioProperty); + } } // private (ASLayoutElementStylePrivate.h) - (void)setParentAlignStyle:(ASStackLayoutAlignItems)style { From d4a175416b9a95371ef982b1b588c41cf1decc73 Mon Sep 17 00:00:00 2001 From: Jonathan Downing Date: Tue, 5 Mar 2019 01:34:56 +0000 Subject: [PATCH 022/122] Make ASCollectionElement Public (#1303) * Update AsyncDisplayKit.h * Update project.pbxproj --- AsyncDisplayKit.xcodeproj/project.pbxproj | 2 +- Source/AsyncDisplayKit.h | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index df8a931ad..84d4290de 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -478,7 +478,7 @@ E55D86331CA8A14000A0C26F /* ASLayoutElement.mm in Sources */ = {isa = PBXBuildFile; fileRef = E55D86311CA8A14000A0C26F /* ASLayoutElement.mm */; }; E5667E8C1F33871300FA6FC0 /* _ASCollectionGalleryLayoutInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = E5667E8B1F33871300FA6FC0 /* _ASCollectionGalleryLayoutInfo.h */; settings = {ATTRIBUTES = (Private, ); }; }; E5667E8E1F33872700FA6FC0 /* _ASCollectionGalleryLayoutInfo.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5667E8D1F33872700FA6FC0 /* _ASCollectionGalleryLayoutInfo.mm */; }; - E5711A2C1C840C81009619D4 /* ASCollectionElement.h in Headers */ = {isa = PBXBuildFile; fileRef = E5711A2A1C840C81009619D4 /* ASCollectionElement.h */; settings = {ATTRIBUTES = (Private, ); }; }; + E5711A2C1C840C81009619D4 /* ASCollectionElement.h in Headers */ = {isa = PBXBuildFile; fileRef = E5711A2A1C840C81009619D4 /* ASCollectionElement.h */; settings = {ATTRIBUTES = (Public, ); }; }; E5711A301C840C96009619D4 /* ASCollectionElement.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5711A2D1C840C96009619D4 /* ASCollectionElement.mm */; }; E5775AFC1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h in Headers */ = {isa = PBXBuildFile; fileRef = E5775AFB1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h */; settings = {ATTRIBUTES = (Private, ); }; }; E5775AFE1F13CF7400CAC9BC /* _ASCollectionGalleryLayoutItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5775AFD1F13CF7400CAC9BC /* _ASCollectionGalleryLayoutItem.mm */; }; diff --git a/Source/AsyncDisplayKit.h b/Source/AsyncDisplayKit.h index eafc03f86..742cfca75 100644 --- a/Source/AsyncDisplayKit.h +++ b/Source/AsyncDisplayKit.h @@ -48,6 +48,7 @@ #import #import +#import #import #import #import From 03746f08696b7309aaf08d7ec1e03a5737823cde Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Mon, 4 Mar 2019 21:07:31 -0800 Subject: [PATCH 023/122] Optimize _assertSubnodeState (#1352) * Optimize _assertSubnodeState This method is actually pretty painful in today's world. In one iPad mini trace, the first page of nodes spent 6.6ms in this call, just in time profiler. * Clean it up, check count * Check the right value --- Source/ASDisplayNode+Layout.mm | 57 +++++++++++++++++++--------------- Source/Base/ASBaseDefines.h | 2 ++ 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/Source/ASDisplayNode+Layout.mm b/Source/ASDisplayNode+Layout.mm index 7d5e00abc..0eac1e3ab 100644 --- a/Source/ASDisplayNode+Layout.mm +++ b/Source/ASDisplayNode+Layout.mm @@ -8,6 +8,7 @@ // #import +#import #import #import #import @@ -18,6 +19,7 @@ #import #import #import +#import @interface ASDisplayNode (ASLayoutElementStyleDelegate) @end @@ -940,35 +942,40 @@ - (void)_assertSubnodeState return; } - NSArray *subnodes = [self subnodes]; - NSArray *sublayouts = _calculatedDisplayNodeLayout.layout.sublayouts; - - const auto currentSubnodes = [[NSHashTable alloc] initWithOptions:NSHashTableObjectPointerPersonality - capacity:subnodes.count]; - const auto layoutSubnodes = [[NSHashTable alloc] initWithOptions:NSHashTableObjectPointerPersonality - capacity:sublayouts.count];; - for (ASDisplayNode *subnode in subnodes) { - [currentSubnodes addObject:subnode]; - } - - for (ASLayout *sublayout in sublayouts) { - id layoutElement = sublayout.layoutElement; - ASDisplayNodeAssert([layoutElement isKindOfClass:[ASDisplayNode class]], - @"All calculatedLayouts should be flattened and only contain nodes!"); - [layoutSubnodes addObject:(ASDisplayNode *)layoutElement]; + ASDN::MutexLocker l(__instanceLock__); + NSArray *sublayouts = _calculatedDisplayNodeLayout.layout.sublayouts; + unowned ASLayout *cSublayouts[sublayouts.count]; + [sublayouts getObjects:cSublayouts range:NSMakeRange(0, AS_ARRAY_SIZE(cSublayouts))]; + + // Fast-path if we are in the correct state (likely). + if (_subnodes.count == AS_ARRAY_SIZE(cSublayouts)) { + NSUInteger i = 0; + BOOL matches = YES; + for (ASDisplayNode *subnode in _subnodes) { + if (subnode != cSublayouts[i].layoutElement) { + matches = NO; + } + i++; + } + if (matches) { + return; + } } - // Verify that all subnodes that occur in the current ASLayout tree are present in .subnodes array. - if ([layoutSubnodes isSubsetOfHashTable:currentSubnodes] == NO) { - // Note: This should be converted to an assertion after confirming it is rare. - NSLog(@"Warning: node's layout includes subnodes that have not been added: node = %@, subnodes = %@, subnodes in layout = %@", self, currentSubnodes, layoutSubnodes); + NSArray *layoutNodes = ASArrayByFlatMapping(sublayouts, ASLayout *layout, (ASDisplayNode *)layout.layoutElement); + NSIndexSet *insertions, *deletions; + [_subnodes asdk_diffWithArray:layoutNodes insertions:&insertions deletions:&deletions]; + if (insertions.count > 0) { + NSLog(@"Warning: node's layout includes subnode that has not been added: node = %@, subnodes = %@, subnodes in layout = %@", self, _subnodes, layoutNodes); } - // Verify that everything in the .subnodes array is present in the ASLayout tree (and correct it if not). - [currentSubnodes minusHashTable:layoutSubnodes]; - for (ASDisplayNode *orphanedSubnode in currentSubnodes) { - NSLog(@"Automatically removing orphaned subnode %@, from parent %@", orphanedSubnode, self); - [orphanedSubnode removeFromSupernode]; + // Remove any nodes that are in the tree but should not be. + // Go in reverse order so we don't shift our indexes. + if (deletions) { + for (NSUInteger i = deletions.lastIndex; i != NSNotFound; i = [deletions indexLessThanIndex:i]) { + NSLog(@"Automatically removing orphaned subnode %@, from parent %@", _subnodes[i], self); + [_subnodes[i] removeFromSupernode]; + } } } diff --git a/Source/Base/ASBaseDefines.h b/Source/Base/ASBaseDefines.h index ce0e891e6..4e880f902 100644 --- a/Source/Base/ASBaseDefines.h +++ b/Source/Base/ASBaseDefines.h @@ -147,6 +147,8 @@ #define AS_SUBCLASSING_RESTRICTED #endif +#define AS_ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0])) + #define ASCreateOnce(expr) ({ \ static dispatch_once_t onceToken; \ static __typeof__(expr) staticVar; \ From e20c651911de4ac0bc49d78ab73e4ff4a7256c9e Mon Sep 17 00:00:00 2001 From: Xavier Deloge Date: Tue, 5 Mar 2019 12:00:38 -0500 Subject: [PATCH 024/122] _ASCollectionViewCell - The point isn't converted before to send to node, impossible to touch button into the node hierarchy (#1362) * Revert node call, useless, the node has a ASDisplayView, and this view forward on the node in first. * Use convertPoint to convert the given point in hittest & pointInside methods. Keep the standard usage if the node didn't rasterized a view. * Removed isNodeLoaded, finally it's useless in this case In these methods, we are on the mainThread, we can create the view if the view is not created. --- Source/Details/_ASCollectionViewCell.mm | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Source/Details/_ASCollectionViewCell.mm b/Source/Details/_ASCollectionViewCell.mm index 39afa167d..28d8cec2a 100644 --- a/Source/Details/_ASCollectionViewCell.mm +++ b/Source/Details/_ASCollectionViewCell.mm @@ -110,12 +110,14 @@ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event return nil; } - return [self.node hitTest:point withEvent:event]; + CGPoint pointOnNode = [self.node.view convertPoint:point fromView:self]; + return [self.node hitTest:pointOnNode withEvent:event]; } - (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event { - return [self.node pointInside:point withEvent:event]; + CGPoint pointOnNode = [self.node.view convertPoint:point fromView:self]; + return [self.node pointInside:pointOnNode withEvent:event]; } @end From 954382dd1b9b65d2112ce2c7b47dd7eef455e42f Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Tue, 5 Mar 2019 18:20:17 -0800 Subject: [PATCH 025/122] Optimize ASCATransactionQueue (#1350) * Optimize ASCATransactionQueue. This queue is very busy, and it runs on the main thread so it's important for it to be fast. Avoid waking up the run loop for every single node. Avoid a ton of NSPointerArray overhead that we don't need. Avoid retain/release traffic on the singleton by using an inline function. I confirmed that in release mode, the static __strong is correctly inlined and no ARC traffic is incurred. * Comment * Unlock right * Remove magic number --- Source/ASDisplayNode.mm | 10 +- Source/ASRunLoopQueue.h | 22 +++-- Source/ASRunLoopQueue.mm | 154 ++++++++++++------------------ Tests/ASDisplayNodeTestsHelper.mm | 2 +- 4 files changed, 82 insertions(+), 106 deletions(-) diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index 10f13c5f8..3ad86ee7a 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -2840,7 +2840,7 @@ - (void)willEnterHierarchy if (![self supportsRangeManagedInterfaceState]) { self.interfaceState = ASInterfaceStateInHierarchy; - } else if (ASCATransactionQueue.sharedQueue.isEnabled) { + } else if (ASCATransactionQueueGet().enabled) { __instanceLock__.lock(); ASInterfaceState state = _preExitingInterfaceState; _preExitingInterfaceState = ASInterfaceStateNone; @@ -2900,7 +2900,7 @@ - (void)didExitHierarchy } }; - if (!ASCATransactionQueue.sharedQueue.enabled) { + if (!ASCATransactionQueueGet().enabled) { dispatch_async(dispatch_get_main_queue(), exitVisibleInterfaceState); } else { exitVisibleInterfaceState(); @@ -2965,13 +2965,13 @@ - (ASInterfaceState)interfaceState - (void)setInterfaceState:(ASInterfaceState)newState { - if (!ASCATransactionQueue.sharedQueue.enabled) { + if (!ASCATransactionQueueGet().enabled) { [self applyPendingInterfaceState:newState]; } else { ASDN::MutexLocker l(__instanceLock__); if (_pendingInterfaceState != newState) { _pendingInterfaceState = newState; - [[ASCATransactionQueue sharedQueue] enqueue:self]; + [ASCATransactionQueueGet() enqueue:self]; } } } @@ -2997,7 +2997,7 @@ - (void)applyPendingInterfaceState:(ASInterfaceState)newPendingState ASDN::MutexLocker l(__instanceLock__); // newPendingState will not be used when ASCATransactionQueue is enabled // and use _pendingInterfaceState instead for interfaceState update. - if (!ASCATransactionQueue.sharedQueue.enabled) { + if (!ASCATransactionQueueGet().enabled) { _pendingInterfaceState = newPendingState; } oldState = _interfaceState; diff --git a/Source/ASRunLoopQueue.h b/Source/ASRunLoopQueue.h index 5cb6d2f7a..fca77c0f4 100644 --- a/Source/ASRunLoopQueue.h +++ b/Source/ASRunLoopQueue.h @@ -48,12 +48,7 @@ AS_SUBCLASSING_RESTRICTED @end -AS_SUBCLASSING_RESTRICTED -@interface ASCATransactionQueue : ASAbstractRunLoopQueue - -@property (readonly) BOOL isEmpty; -@property (readonly, getter=isEnabled) BOOL enabled; /** * The queue to run on main run loop before CATransaction commit. @@ -62,13 +57,26 @@ AS_SUBCLASSING_RESTRICTED * to get last chance of updating/coalesce info like interface state. * Each node will only be called once per transaction commit to reflect interface change. */ -@property (class, readonly) ASCATransactionQueue *sharedQueue; -+ (ASCATransactionQueue *)sharedQueue NS_RETURNS_RETAINED; +AS_SUBCLASSING_RESTRICTED +@interface ASCATransactionQueue : ASAbstractRunLoopQueue + +@property (readonly) BOOL isEmpty; + +@property (readonly, getter=isEnabled) BOOL enabled; - (void)enqueue:(id)object; @end +NS_INLINE ASCATransactionQueue *ASCATransactionQueueGet(void) { + static ASCATransactionQueue *q; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + q = [[ASCATransactionQueue alloc] init]; + }); + return q; +} + @interface ASDeallocQueue : NSObject @property (class, readonly) ASDeallocQueue *sharedDeallocationQueue; diff --git a/Source/ASRunLoopQueue.mm b/Source/ASRunLoopQueue.mm index 65ce8c4f4..7ddb59103 100644 --- a/Source/ASRunLoopQueue.mm +++ b/Source/ASRunLoopQueue.mm @@ -301,9 +301,10 @@ - (void)enqueue:(id)object if (!foundObject) { [_internalQueue addPointer:(__bridge void *)object]; - - CFRunLoopSourceSignal(_runLoopSource); - CFRunLoopWakeUp(_runLoop); + if (_internalQueue.count == 1) { + CFRunLoopSourceSignal(_runLoopSource); + CFRunLoopWakeUp(_runLoop); + } } } @@ -320,11 +321,20 @@ - (BOOL)isEmpty #pragma mark - ASCATransactionQueue @interface ASCATransactionQueue () { - CFRunLoopRef _runLoop; CFRunLoopSourceRef _runLoopSource; CFRunLoopObserverRef _preTransactionObserver; - NSPointerArray *_internalQueue; - ASDN::RecursiveMutex _internalQueueLock; + + // Current buffer for new entries, only accessed from within its mutex. + std::vector> _internalQueue; + + // No retain, no release, pointer hash, pointer equality. + // Enforce uniqueness in our queue. std::unordered_set does a heap allocation for each entry – not good. + CFMutableSetRef _internalQueueHashSet; + + // Temporary buffer, only accessed from the main thread in -process. + std::vector> _batchBuffer; + + ASDN::Mutex _internalQueueLock; // In order to not pollute the top-level activities, each queue has 1 root activity. os_activity_t _rootActivity; @@ -342,22 +352,16 @@ @implementation ASCATransactionQueue // but after most other scheduled work on the runloop has processed. static int const kASASCATransactionQueueOrder = 1000000; -+ (ASCATransactionQueue *)sharedQueue NS_RETURNS_RETAINED -{ - static dispatch_once_t onceToken; - static ASCATransactionQueue *sharedQueue; - dispatch_once(&onceToken, ^{ - sharedQueue = [[ASCATransactionQueue alloc] init]; - }); - return sharedQueue; -} - - (instancetype)init { if (self = [super init]) { - _runLoop = CFRunLoopGetMain(); - NSPointerFunctionsOptions options = NSPointerFunctionsStrongMemory; - _internalQueue = [[NSPointerArray alloc] initWithOptions:options]; + _internalQueueHashSet = CFSetCreateMutable(NULL, 0, NULL); + + // This is going to be a very busy queue – every node in the preload range will enter this queue. + // Save some time on first render by reserving space up front. + static constexpr int kInternalQueueInitialCapacity = 64; + _internalQueue.reserve(kInternalQueueInitialCapacity); + _batchBuffer.reserve(kInternalQueueInitialCapacity); // We don't want to pollute the top-level app activities with run loop batches, so we create one top-level // activity per queue, and each batch activity joins that one instead. @@ -371,14 +375,13 @@ - (instancetype)init // Self is guaranteed to outlive the observer. Without the high cost of a weak pointer, // __unsafe_unretained allows us to avoid flagging the memory cycle detector. __unsafe_unretained __typeof__(self) weakSelf = self; - void (^handlerBlock) (CFRunLoopObserverRef observer, CFRunLoopActivity activity) = ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { - while (weakSelf->_internalQueue.count > 0) { - [weakSelf processQueue]; + _preTransactionObserver = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, kASASCATransactionQueueOrder, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { + while (!weakSelf->_internalQueue.empty()) { + [weakSelf processQueue]; } - }; - _preTransactionObserver = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, kASASCATransactionQueueOrder, handlerBlock); + }); - CFRunLoopAddObserver(_runLoop, _preTransactionObserver, kCFRunLoopCommonModes); + CFRunLoopAddObserver(CFRunLoopGetMain(), _preTransactionObserver, kCFRunLoopCommonModes); // It is not guaranteed that the runloop will turn if it has no scheduled work, and this causes processing of // the queue to stop. Attaching a custom loop source to the run loop and signal it if new work needs to be done @@ -388,7 +391,7 @@ - (instancetype)init sourceContext.info = (__bridge void *)self; #endif _runLoopSource = CFRunLoopSourceCreate(NULL, 0, &sourceContext); - CFRunLoopAddSource(_runLoop, _runLoopSource, kCFRunLoopCommonModes); + CFRunLoopAddSource(CFRunLoopGetMain(), _runLoopSource, kCFRunLoopCommonModes); #if ASRunLoopQueueLoggingEnabled _runloopQueueLoggingTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(checkRunLoop) userInfo:nil repeats:YES]; @@ -400,7 +403,10 @@ - (instancetype)init - (void)dealloc { - CFRunLoopRemoveSource(_runLoop, _runLoopSource, kCFRunLoopCommonModes); + ASDisplayNodeAssertMainThread(); + + CFRelease(_internalQueueHashSet); + CFRunLoopRemoveSource(CFRunLoopGetMain(), _runLoopSource, kCFRunLoopCommonModes); CFRelease(_runLoopSource); _runLoopSource = nil; @@ -420,60 +426,30 @@ - (void)checkRunLoop - (void)processQueue { - // If we have an execution block, this vector will be populated, otherwise remains empty. - // This is to avoid needlessly retaining/releasing the objects if we don't have a block. - std::vector itemsToProcess; - - { - ASDN::MutexLocker l(_internalQueueLock); - - NSInteger internalQueueCount = _internalQueue.count; - // Early-exit if the queue is empty. - if (internalQueueCount == 0) { - return; - } - - ASSignpostStart(ASSignpostRunLoopQueueBatch); + ASDisplayNodeAssertMainThread(); - /** - * For each item in the next batch, if it's non-nil then NULL it out - * and if we have an execution block then add it in. - * This could be written a bunch of different ways but - * this particular one nicely balances readability, safety, and efficiency. - */ - NSInteger foundItemCount = 0; - for (NSInteger i = 0; i < internalQueueCount && foundItemCount < internalQueueCount; i++) { - /** - * It is safe to use unsafe_unretained here. If the queue is weak, the - * object will be added to the autorelease pool. If the queue is strong, - * it will retain the object until we transfer it (retain it) in itemsToProcess. - */ - __unsafe_unretained id ptr = (__bridge id)[_internalQueue pointerAtIndex:i]; - if (ptr != nil) { - foundItemCount++; - itemsToProcess.push_back(ptr); - [_internalQueue replacePointerAtIndex:i withPointer:NULL]; - } - } - - [_internalQueue compact]; + ASDN::UniqueLock l(_internalQueueLock); + NSInteger count = _internalQueue.size(); + // Early-exit if the queue is empty. + if (count == 0) { + return; } - - // itemsToProcess will be empty if _queueConsumer == nil so no need to check again. - const auto count = itemsToProcess.size(); - if (count > 0) { - as_activity_scope_verbose(as_activity_create("Process run loop queue batch", _rootActivity, OS_ACTIVITY_FLAG_DEFAULT)); - const auto itemsEnd = itemsToProcess.cend(); - for (auto iterator = itemsToProcess.begin(); iterator < itemsEnd; iterator++) { - __unsafe_unretained id value = *iterator; - [value prepareForCATransactionCommit]; - as_log_verbose(ASDisplayLog(), "processed %@", value); - } - if (count > 1) { - as_log_verbose(ASDisplayLog(), "processed %lu items", (unsigned long)count); - } + as_activity_scope_verbose(as_activity_create("Process run loop queue batch", _rootActivity, OS_ACTIVITY_FLAG_DEFAULT)); + ASSignpostStart(ASSignpostRunLoopQueueBatch); + + // Swap buffers, clear our hash table. + _internalQueue.swap(_batchBuffer); + CFSetRemoveAllValues(_internalQueueHashSet); + + // Unlock early. We are done with internal queue, and batch buffer is main-thread-only so no lock. + l.unlock(); + + for (const id &value : _batchBuffer) { + [value prepareForCATransactionCommit]; + as_log_verbose(ASDisplayLog(), "processed %@", value); } - + _batchBuffer.clear(); + as_log_verbose(ASDisplayLog(), "processed %lu items", (unsigned long)count); ASSignpostEnd(ASSignpostRunLoopQueueBatch); } @@ -489,29 +465,21 @@ - (void)enqueue:(id)object } ASDN::MutexLocker l(_internalQueueLock); - - // Check if the object exists. - BOOL foundObject = NO; - - for (id currentObject in _internalQueue) { - if (currentObject == object) { - foundObject = YES; - break; - } + if (CFSetContainsValue(_internalQueueHashSet, (__bridge void *)object)) { + return; } - - if (!foundObject) { - [_internalQueue addPointer:(__bridge void *)object]; - + CFSetAddValue(_internalQueueHashSet, (__bridge void *)object); + _internalQueue.emplace_back(object); + if (_internalQueue.size() == 1) { CFRunLoopSourceSignal(_runLoopSource); - CFRunLoopWakeUp(_runLoop); + CFRunLoopWakeUp(CFRunLoopGetMain()); } } - (BOOL)isEmpty { ASDN::MutexLocker l(_internalQueueLock); - return _internalQueue.count == 0; + return _internalQueue.empty(); } - (BOOL)isEnabled diff --git a/Tests/ASDisplayNodeTestsHelper.mm b/Tests/ASDisplayNodeTestsHelper.mm index bc145b924..ae2054911 100644 --- a/Tests/ASDisplayNodeTestsHelper.mm +++ b/Tests/ASDisplayNodeTestsHelper.mm @@ -58,7 +58,7 @@ void ASDisplayNodeSizeToFitSizeRange(ASDisplayNode *node, ASSizeRange sizeRange) void ASCATransactionQueueWait(ASCATransactionQueue *q) { - if (!q) { q = ASCATransactionQueue.sharedQueue; } + if (!q) { q = ASCATransactionQueueGet(); } NSDate *date = [NSDate dateWithTimeIntervalSinceNow:1]; BOOL whileResult = YES; while ([date timeIntervalSinceNow] > 0 && From e4989699e027272a79a454f3e136c7bec8e1110c Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Wed, 6 Mar 2019 07:37:40 -0800 Subject: [PATCH 026/122] Cleanup Yoga Categories (#1364) --- Source/ASDisplayNode+Layout.mm | 33 +----------------------------- Source/ASDisplayNode+Yoga.h | 4 ++++ Source/ASDisplayNode+Yoga.mm | 37 +++++++++++++++++++++++++++++++++- 3 files changed, 41 insertions(+), 33 deletions(-) diff --git a/Source/ASDisplayNode+Layout.mm b/Source/ASDisplayNode+Layout.mm index 0eac1e3ab..35b670828 100644 --- a/Source/ASDisplayNode+Layout.mm +++ b/Source/ASDisplayNode+Layout.mm @@ -1046,10 +1046,7 @@ - (void)_locked_setCalculatedDisplayNodeLayout:(const ASDisplayNodeLayout &)disp #pragma mark - #pragma mark - ASDisplayNode (YogaLayout) -@implementation ASDisplayNode (YogaInternal) - -#pragma mark - -#pragma mark - ASDisplayNode (Yoga) +@implementation ASDisplayNode (YogaLayout) - (BOOL)locked_shouldLayoutFromYogaRoot { #if YOGA @@ -1071,32 +1068,4 @@ - (BOOL)locked_shouldLayoutFromYogaRoot { #endif } -- (ASLockSet)lockToRootIfNeededForLayout { - ASLockSet lockSet = ASLockSequence(^BOOL(ASAddLockBlock addLock) { - if (!addLock(self)) { - return NO; - } -#if YOGA - if (![self locked_shouldLayoutFromYogaRoot]) { - return YES; - } - if (self.nodeController && !addLock(self.nodeController)) { - return NO; - } - ASDisplayNode *parent = _supernode; - while (parent) { - if (!addLock(parent)) { - return NO; - } - if (parent.nodeController && !addLock(parent.nodeController)) { - return NO; - } - parent = parent->_supernode; - } -#endif - return true; - }); - return lockSet; -} - @end diff --git a/Source/ASDisplayNode+Yoga.h b/Source/ASDisplayNode+Yoga.h index fc8d88464..93cbf7a7c 100644 --- a/Source/ASDisplayNode+Yoga.h +++ b/Source/ASDisplayNode+Yoga.h @@ -33,6 +33,10 @@ AS_EXTERN void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode * _Nullab // Will walk up the Yoga tree and returns the root node - (ASDisplayNode *)yogaRoot; + +@end + +@interface ASDisplayNode (YogaLocking) /** * @discussion Attempts(spinning) to lock all node up to root node when yoga is enabled. * This will lock self when yoga is not enabled; diff --git a/Source/ASDisplayNode+Yoga.mm b/Source/ASDisplayNode+Yoga.mm index 0f975c885..97e7cb56b 100644 --- a/Source/ASDisplayNode+Yoga.mm +++ b/Source/ASDisplayNode+Yoga.mm @@ -20,6 +20,7 @@ #import #import #import +#import #import @@ -27,7 +28,7 @@ #pragma mark - ASDisplayNode+Yoga -@interface ASDisplayNode (YogaInternal) +@interface ASDisplayNode (YogaPrivate) @property (nonatomic, weak) ASDisplayNode *yogaParent; - (ASSizeRange)_locked_constrainedSizeForLayoutPass; @end @@ -409,6 +410,40 @@ - (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize @end +#pragma mark - ASDisplayNode (YogaLocking) + +@implementation ASDisplayNode (YogaLocking) + +- (ASLockSet)lockToRootIfNeededForLayout { + ASLockSet lockSet = ASLockSequence(^BOOL(ASAddLockBlock addLock) { + if (!addLock(self)) { + return NO; + } +#if YOGA + if (![self locked_shouldLayoutFromYogaRoot]) { + return YES; + } + if (self.nodeController && !addLock(self.nodeController)) { + return NO; + } + ASDisplayNode *parent = _supernode; + while (parent) { + if (!addLock(parent)) { + return NO; + } + if (parent.nodeController && !addLock(parent.nodeController)) { + return NO; + } + parent = parent->_supernode; + } +#endif + return true; + }); + return lockSet; +} + +@end + @implementation ASDisplayNode (YogaDebugging) - (NSString *)yogaTreeDescription { From abf0212206ba62cc656a3321005de9622f3bdc8e Mon Sep 17 00:00:00 2001 From: Eric Scheers Date: Wed, 6 Mar 2019 19:02:50 +0100 Subject: [PATCH 027/122] Add forwarding of UIAccessibilityAction methods (#1344) * Add forwarding of UIAccessibilityAction methods * Use OCMock for testing UIAccessibilityAction method forwarding --- Source/Details/_ASDisplayViewAccessiblity.mm | 28 ++++++++++++++++++ Tests/ASDisplayViewAccessibilityTests.mm | 31 ++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/Source/Details/_ASDisplayViewAccessiblity.mm b/Source/Details/_ASDisplayViewAccessiblity.mm index 74016e786..711be0294 100644 --- a/Source/Details/_ASDisplayViewAccessiblity.mm +++ b/Source/Details/_ASDisplayViewAccessiblity.mm @@ -297,4 +297,32 @@ - (NSArray *)accessibilityElements @end +@implementation _ASDisplayView (UIAccessibilityAction) + +- (BOOL)accessibilityActivate { + return [self.asyncdisplaykit_node accessibilityActivate]; +} + +- (void)accessibilityIncrement { + [self.asyncdisplaykit_node accessibilityIncrement]; +} + +- (void)accessibilityDecrement { + [self.asyncdisplaykit_node accessibilityDecrement]; +} + +- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction { + return [self.asyncdisplaykit_node accessibilityScroll:direction]; +} + +- (BOOL)accessibilityPerformEscape { + return [self.asyncdisplaykit_node accessibilityPerformEscape]; +} + +- (BOOL)accessibilityPerformMagicTap { + return [self.asyncdisplaykit_node accessibilityPerformMagicTap]; +} + +@end + #endif diff --git a/Tests/ASDisplayViewAccessibilityTests.mm b/Tests/ASDisplayViewAccessibilityTests.mm index fd518c582..bbc28cf0b 100644 --- a/Tests/ASDisplayViewAccessibilityTests.mm +++ b/Tests/ASDisplayViewAccessibilityTests.mm @@ -17,6 +17,7 @@ #import #import #import +#import @interface ASDisplayViewAccessibilityTests : XCTestCase @end @@ -269,4 +270,34 @@ - (void)testAccessibilityUpdatesWithElementsChanges XCTAssertTrue([[elements2.firstObject accessibilityLabel] isEqualToString:@"greeting"]); } +#pragma mark - +#pragma mark UIAccessibilityAction Forwarding + +- (void)testActionForwarding { + ASDisplayNode *node = [ASDisplayNode new]; + UIView *view = node.view; + + id mockNode = OCMPartialMock(node); + + OCMExpect([mockNode accessibilityActivate]); + [view accessibilityActivate]; + + OCMExpect([mockNode accessibilityIncrement]); + [view accessibilityIncrement]; + + OCMExpect([mockNode accessibilityDecrement]); + [view accessibilityDecrement]; + + OCMExpect([mockNode accessibilityScroll:UIAccessibilityScrollDirectionDown]); + [view accessibilityScroll:UIAccessibilityScrollDirectionDown]; + + OCMExpect([mockNode accessibilityPerformEscape]); + [view accessibilityPerformEscape]; + + OCMExpect([mockNode accessibilityPerformMagicTap]); + [view accessibilityPerformMagicTap]; + + OCMVerifyAll(mockNode); +} + @end From f7182c7f54cf132c7972a2068c410a5feac9806a Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 6 Mar 2019 10:05:30 -0800 Subject: [PATCH 028/122] Don't add extraneous truncation token during kCTLineTruncationMiddle. (#1297) * Don't add extraneous truncation token during kCTLineTruncationMiddle * Expand these comments a little. --- .../Private/TextExperiment/Component/ASTextLayout.mm | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Source/Private/TextExperiment/Component/ASTextLayout.mm b/Source/Private/TextExperiment/Component/ASTextLayout.mm index ec029d62e..135709a84 100644 --- a/Source/Private/TextExperiment/Component/ASTextLayout.mm +++ b/Source/Private/TextExperiment/Component/ASTextLayout.mm @@ -834,7 +834,8 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri } } int i = 0; - if (type != kCTLineTruncationStart) { // Middle or End/Tail wants text preceding truncated content. + if (type != kCTLineTruncationStart) { // Middle or End/Tail wants to collect some text (at least one line's + // worth) preceding the truncated content, with which to construct a "truncated line". i = (int)removedLines.count - 1; while (atLeastOneLine < truncatedWidth && i >= 0) { if (lastLineText.length > 0 && [lastLineText.string characterAtIndex:lastLineText.string.length - 1] == '\n') { // Explicit newlines are always "long enough". @@ -846,7 +847,8 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri } [lastLineText appendAttributedString:truncationToken]; } - if (type != kCTLineTruncationEnd && removedLines.count > 0) { // Middle or Start/Head wants text following truncated content. + if (type != kCTLineTruncationEnd && removedLines.count > 0) { // Middle or Start/Head wants to collect some + // text following the truncated content. i = 0; atLeastOneLine = removedLines[i].width; while (atLeastOneLine < truncatedWidth && i < removedLines.count) { @@ -860,7 +862,9 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri [lastLineText appendAttributedString:nextLine]; } } - [lastLineText insertAttributedString:truncationToken atIndex:0]; + if (type == kCTLineTruncationStart) { + [lastLineText insertAttributedString:truncationToken atIndex:0]; + } } CTLineRef ctLastLineExtend = CTLineCreateWithAttributedString((CFAttributedStringRef) lastLineText); From 88dd4a2ab5efb47b6e51c76a9ae6b270b2beecd1 Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Wed, 6 Mar 2019 17:09:49 -0800 Subject: [PATCH 029/122] Update the FAQ to throw less shade. (#1379) * Update the FAQ to throw less shade. Both of these technologies have their place, no point in being assholes. * Update faq.md --- docs/_docs/faq.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/_docs/faq.md b/docs/_docs/faq.md index 18672cd9f..fa7d3b2be 100755 --- a/docs/_docs/faq.md +++ b/docs/_docs/faq.md @@ -83,9 +83,9 @@ For a longer discussion and easy alternative corner rounding solutions, please r ### Texture does not support UIKit Auto Layout or InterfaceBuilder
-UIKit Auto Layout and InterfaceBuilder are not supported by Texture. It is worth noting that both of these technologies are not permitted in established and disciplined iOS development teams, such as at Facebook, Instagram, and Pinterest. +UIKit Auto Layout and InterfaceBuilder are not supported by Texture. -However, Texture's Layout API provides a variety of ASLayoutSpec objects that allow implementing automatic layout which is more efficient (multithreaded, off the main thread), easier to debug (can step into the code and see where all values come from, as it is open source), and reusable (you can build composable layouts that can be shared with different parts of the UI). +However, Texture's Layout API provides a variety of ASLayoutSpec objects that allow implementing automatic layout which is more efficient (multithreaded, off the main thread), often easier to debug (can step into the code and see where all values come from, as it is open source), and reusable (you can build composable layouts that can be shared with different parts of the UI).
### ASDisplayNode keep alive reference From 1a2e397a91aeacc778b9ce0db84a91280da1ce7b Mon Sep 17 00:00:00 2001 From: Kevin Date: Thu, 7 Mar 2019 07:20:06 -0800 Subject: [PATCH 030/122] [Updated Snapshots] Don't add extraneous truncation token during kCTLineTruncationMiddle (#1375) * Don't add extraneous truncation token during kCTLineTruncationMiddle * Expand these comments a little. * Update snapshot tests with beautiful correct behavior --- ...SLineBreakByTruncatingMiddle_0Lines@2x.png | Bin 103926 -> 103930 bytes ...SLineBreakByTruncatingMiddle_1Lines@2x.png | Bin 55422 -> 55612 bytes ...SLineBreakByTruncatingMiddle_2Lines@2x.png | Bin 68079 -> 68093 bytes ...SLineBreakByTruncatingMiddle_3Lines@2x.png | Bin 78743 -> 78830 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingMiddle_0Lines@2x.png b/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingMiddle_0Lines@2x.png index 5aa97aa29510930d7b76ec500a11282d87228f63..c1a4f2b4995092326d8a3c58377e93e9d8dccf53 100644 GIT binary patch delta 34081 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 JymI2rcWdK7djKaUM@EHqV4k!QaR9w1j8Tvo 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�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

%Ow&l=`(+>0v4U}`J z#y)!*%nBWbE{DE^V6deX|U?_srn_*L^EPB=4fh+Nq~}LVSXgZ%Lzo z_RzPdPBqSqOChZtP+j9O_~nstWK_n%iLG+$wn0I}yUANRWN0|MSPb$bi# zmod3jr7uM>ty4MZ_T1eWJV9Hsc+R{yY4_$g_LE`<&z)C1x6UVRR?bl2PPlQ^<)I{P z&r+=wflk`li=QWp*Ef<6r5*nkh*aO}Wh~kMCSYT|D88 z3)LA`dfe<7LA*0$|AnG3z0lQ4#`IiKc+M&A)bhHtWz5GZAr+gHkV-g3qGEVX?pW%P z%wX`}C~JY&hwKJF>+7Bh8IhV)^3BMVLmkH{;JA80ueX!f5LN6Nn0xtD#hKPjGUKNSEF#l{MlujYu2PoH-G*&+Uwsp delta 21918 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 diff --git a/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingMiddle_2Lines@2x.png b/Tests/ReferenceImages_iOS_10/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingMiddle_2Lines@2x.png index a4a47c6a53173adfdf86e2e4abe7838087aa4fe2..c52a7e6ddef9beb822794b353b8727b72bf9a6a7 100644 GIT binary patch delta 14139 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 l^KN+#o2TG(mcouF`vtqM;Ld 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 SaJA0|k^2$fv%Wj^^Zx^aPuBMU delta 41947 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�d%L=cV+~ssu5>?SG`OCBq&hYWdzdai>~F-Z=;Tf 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 Date: Thu, 7 Mar 2019 09:47:56 -0800 Subject: [PATCH 031/122] Launches switching ASNetworkImageNode callbacks to global queue. (#1369) * Launches switching ASNetworkImageNode callbacks to global queue. * Good catch configuration tests! --- Schemas/configuration.json | 1 - Source/ASExperimentalFeatures.h | 17 ++++++++--------- Source/ASExperimentalFeatures.mm | 1 - Source/ASNetworkImageNode.mm | 11 +---------- Tests/ASConfigurationTests.mm | 2 -- Tests/ASNetworkImageNodeTests.mm | 1 - 6 files changed, 9 insertions(+), 24 deletions(-) diff --git a/Schemas/configuration.json b/Schemas/configuration.json index 5fb3993b7..2b95d767f 100644 --- a/Schemas/configuration.json +++ b/Schemas/configuration.json @@ -18,7 +18,6 @@ "exp_interface_state_coalesce", "exp_unfair_lock", "exp_infer_layer_defaults", - "exp_network_image_queue", "exp_collection_teardown", "exp_framesetter_cache", "exp_skip_clear_data", diff --git a/Source/ASExperimentalFeatures.h b/Source/ASExperimentalFeatures.h index 87aee06aa..f78b4332c 100644 --- a/Source/ASExperimentalFeatures.h +++ b/Source/ASExperimentalFeatures.h @@ -23,15 +23,14 @@ typedef NS_OPTIONS(NSUInteger, ASExperimentalFeatures) { ASExperimentalInterfaceStateCoalescing = 1 << 2, // exp_interface_state_coalesce ASExperimentalUnfairLock = 1 << 3, // exp_unfair_lock ASExperimentalLayerDefaults = 1 << 4, // exp_infer_layer_defaults - ASExperimentalNetworkImageQueue = 1 << 5, // exp_network_image_queue - ASExperimentalCollectionTeardown = 1 << 6, // exp_collection_teardown - ASExperimentalFramesetterCache = 1 << 7, // exp_framesetter_cache - ASExperimentalSkipClearData = 1 << 8, // exp_skip_clear_data - ASExperimentalDidEnterPreloadSkipASMLayout = 1 << 9, // exp_did_enter_preload_skip_asm_layout - ASExperimentalDisableAccessibilityCache = 1 << 10, // exp_disable_a11y_cache - ASExperimentalSkipAccessibilityWait = 1 << 11, // exp_skip_a11y_wait - ASExperimentalNewDefaultCellLayoutMode = 1 << 12, // exp_new_default_cell_layout_mode - ASExperimentalDispatchApply = 1 << 13, // exp_dispatch_apply + ASExperimentalCollectionTeardown = 1 << 5, // exp_collection_teardown + ASExperimentalFramesetterCache = 1 << 6, // exp_framesetter_cache + ASExperimentalSkipClearData = 1 << 7, // exp_skip_clear_data + ASExperimentalDidEnterPreloadSkipASMLayout = 1 << 8, // exp_did_enter_preload_skip_asm_layout + ASExperimentalDisableAccessibilityCache = 1 << 9, // exp_disable_a11y_cache + ASExperimentalSkipAccessibilityWait = 1 << 10, // exp_skip_a11y_wait + ASExperimentalNewDefaultCellLayoutMode = 1 << 11, // exp_new_default_cell_layout_mode + ASExperimentalDispatchApply = 1 << 12, // exp_dispatch_apply ASExperimentalFeatureAll = 0xFFFFFFFF }; diff --git a/Source/ASExperimentalFeatures.mm b/Source/ASExperimentalFeatures.mm index bba80d5e8..0a8972fe3 100644 --- a/Source/ASExperimentalFeatures.mm +++ b/Source/ASExperimentalFeatures.mm @@ -17,7 +17,6 @@ @"exp_interface_state_coalesce", @"exp_unfair_lock", @"exp_infer_layer_defaults", - @"exp_network_image_queue", @"exp_collection_teardown", @"exp_framesetter_cache", @"exp_skip_clear_data", diff --git a/Source/ASNetworkImageNode.mm b/Source/ASNetworkImageNode.mm index 0a0245dbe..f19684459 100644 --- a/Source/ASNetworkImageNode.mm +++ b/Source/ASNetworkImageNode.mm @@ -125,16 +125,7 @@ - (void)dealloc - (dispatch_queue_t)callbackQueue { - static dispatch_once_t onceToken; - static dispatch_queue_t callbackQueue; - dispatch_once(&onceToken, ^{ - if (ASActivateExperimentalFeature(ASExperimentalNetworkImageQueue)) { - callbackQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - } else { - callbackQueue = dispatch_get_main_queue(); - } - }); - return callbackQueue; + return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); } #pragma mark - Public methods -- must lock diff --git a/Tests/ASConfigurationTests.mm b/Tests/ASConfigurationTests.mm index 4dd8de2ab..aff309176 100644 --- a/Tests/ASConfigurationTests.mm +++ b/Tests/ASConfigurationTests.mm @@ -23,7 +23,6 @@ ASExperimentalInterfaceStateCoalescing, ASExperimentalUnfairLock, ASExperimentalLayerDefaults, - ASExperimentalNetworkImageQueue, ASExperimentalCollectionTeardown, ASExperimentalFramesetterCache, ASExperimentalSkipClearData, @@ -49,7 +48,6 @@ + (NSArray *)names { @"exp_interface_state_coalesce", @"exp_unfair_lock", @"exp_infer_layer_defaults", - @"exp_network_image_queue", @"exp_collection_teardown", @"exp_framesetter_cache", @"exp_skip_clear_data", diff --git a/Tests/ASNetworkImageNodeTests.mm b/Tests/ASNetworkImageNodeTests.mm index 6b60030ff..f794ba9e7 100644 --- a/Tests/ASNetworkImageNodeTests.mm +++ b/Tests/ASNetworkImageNodeTests.mm @@ -109,7 +109,6 @@ @implementation ASTestImageCache - (void)cachedImageWithURL:(NSURL *)URL callbackQueue:(dispatch_queue_t)callbackQueue completion:(ASImageCacherCompletion)completion { - ASDisplayNodeAssert(callbackQueue == dispatch_get_main_queue(), @"ASTestImageCache expects main queue for callback."); completion(nil); } From 5b1e14cb95759471dfaeed33725ed8d0c8f9f5d4 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Thu, 7 Mar 2019 13:29:13 -0800 Subject: [PATCH 032/122] Make ASTextNode2 more forgiving when searching for links (#1374) * Make ASTextNode2 more forgiving when searching for links by searching a 44x44 square around the touch * Trailing whitespace * Safely handle end-of-line --- Source/ASTextNode2.mm | 46 +++++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/Source/ASTextNode2.mm b/Source/ASTextNode2.mm index eeaae995d..78a667e04 100644 --- a/Source/ASTextNode2.mm +++ b/Source/ASTextNode2.mm @@ -601,25 +601,41 @@ - (id)_linkAttributeValueAtPoint:(CGPoint)point NSRange visibleRange = layout.visibleRange; NSRange clampedRange = NSIntersectionRange(visibleRange, NSMakeRange(0, _attributedText.length)); - ASTextRange *range = [layout closestTextRangeAtPoint:point]; - NSRange effectiveRange = NSMakeRange(0, 0); - for (__strong NSString *attributeName in self.linkAttributeNames) { - id value = [self.attributedText attribute:attributeName atIndex:range.start.offset longestEffectiveRange:&effectiveRange inRange:clampedRange]; - if (value == nil) { - // Didn't find any links specified with this attribute. + + // Search the 9 points of a 44x44 square around the touch until we find a link. + // Start from center, then do sides, then do top/bottom, then do corners. + static constexpr CGSize kRectOffsets[9] = { + { 0, 0 }, + { -22, 0 }, { 22, 0 }, + { 0, -22 }, { 0, 22 }, + { -22, -22 }, { -22, 22 }, + { 22, -22 }, { 22, 22 } + }; + + for (const CGSize &offset : kRectOffsets) { + const CGPoint testPoint = CGPointMake(point.x + offset.width, + point.y + offset.height); + ASTextPosition *pos = [layout closestPositionToPoint:testPoint]; + if (!pos || !NSLocationInRange(pos.offset, clampedRange)) { continue; } + for (NSString *attributeName in _linkAttributeNames) { + NSRange effectiveRange = NSMakeRange(0, 0); + id value = [_attributedText attribute:attributeName atIndex:pos.offset + longestEffectiveRange:&effectiveRange inRange:clampedRange]; + if (value == nil) { + // Didn't find any links specified with this attribute. + continue; + } - // If highlighting, check with delegate first. If not implemented, assume YES. - id delegate = self.delegate; - if (highlighting - && [delegate respondsToSelector:@selector(textNode:shouldHighlightLinkAttribute:value:atPoint:)] - && ![delegate textNode:(ASTextNode *)self shouldHighlightLinkAttribute:attributeName value:value atPoint:point]) { - value = nil; - attributeName = nil; - } + // If highlighting, check with delegate first. If not implemented, assume YES. + if (highlighting + && [_delegate respondsToSelector:@selector(textNode:shouldHighlightLinkAttribute:value:atPoint:)] + && ![_delegate textNode:(ASTextNode *)self shouldHighlightLinkAttribute:attributeName + value:value atPoint:point]) { + continue; + } - if (value != nil || attributeName != nil) { *rangeOut = NSIntersectionRange(visibleRange, effectiveRange); if (attributeNameOut != NULL) { From 0547eaa6fc4b4fd1e902908c9c16e31830ad3e1f Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Fri, 8 Mar 2019 08:11:03 -0800 Subject: [PATCH 033/122] Experiment with different strategies for image downloader priority (#1349) Right now when an image node enters preload state, we kick off an image request with the default priority. Then when it enters display state, we change the priority to "imminent" which is mapped to the default priority as well. This means that requests from preload and display nodes have the same priority and are put to the same pool. The right behavior would be that preload requests should have a lower priority from the beginning. Another problem is that, due to the execution order of -didEnter(Preload|Display|Visible)State calls, a node may kick off a low priority request when it enters preload state even though it knows that it's also visible. By the time -didEnterVisibleState is called, the low priority request may have already been consumed and the download/data task won't pick up the new higher priority, or some work needs to be done to move it to another queue. A better behavior would be to always use the current interface state to determine the priority. This means that visible nodes will kick off high priority requests as soon as -didEnterPreloadState is called. The last (and smaller) issue is that a node marks its request as preload/low priority as soon as it exits visible state. I'd argue that this is too agressive. It may be reasonble for nodes in the trailing direction. Even so, we already handle this case by (almost always) have smaller trailing buffers. So this diff makes sure that nodes that exited visible state will have imminent/default priority if they remain in the display range. All of these new behaviors are wrapped in an experiment and will be tested carefully before being rolled out. * Add imports * Fix build failure * Encapsulate common logics into methods * Address comments --- Podfile.lock | 39 ++--- Schemas/configuration.json | 3 +- Source/ASExperimentalFeatures.h | 1 + Source/ASExperimentalFeatures.mm | 5 +- Source/ASMultiplexImageNode.mm | 164 ++++++++++++------- Source/ASNetworkImageNode.mm | 104 ++++++++---- Source/Details/ASBasicImageDownloader.mm | 36 +++- Source/Details/ASImageProtocols.h | 24 ++- Source/Details/ASPINRemoteImageDownloader.mm | 44 +++-- Source/Private/ASInternalHelpers.h | 18 ++ Tests/ASConfigurationTests.mm | 6 +- 11 files changed, 301 insertions(+), 143 deletions(-) diff --git a/Podfile.lock b/Podfile.lock index a0caeb263..df3c6fcb8 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,38 +1,32 @@ PODS: - FBSnapshotTestCase/Core (2.1.4) - - FLAnimatedImage (1.0.12) - JGMethodSwizzler (2.0.1) - OCMock (3.4.1) - - PINCache (3.0.1-beta.6): - - PINCache/Arc-exception-safe (= 3.0.1-beta.6) - - PINCache/Core (= 3.0.1-beta.6) - - PINCache/Arc-exception-safe (3.0.1-beta.6): + - PINCache (3.0.1-beta.7): + - PINCache/Arc-exception-safe (= 3.0.1-beta.7) + - PINCache/Core (= 3.0.1-beta.7) + - PINCache/Arc-exception-safe (3.0.1-beta.7): - PINCache/Core - - PINCache/Core (3.0.1-beta.6): - - PINOperation (~> 1.1.0) + - PINCache/Core (3.0.1-beta.7): + - PINOperation (~> 1.1.1) - PINOperation (1.1.1) - - PINRemoteImage (3.0.0-beta.13): - - PINRemoteImage/FLAnimatedImage (= 3.0.0-beta.13) - - PINRemoteImage/PINCache (= 3.0.0-beta.13) - - PINRemoteImage/Core (3.0.0-beta.13): + - PINRemoteImage (3.0.0-beta.14): + - PINRemoteImage/PINCache (= 3.0.0-beta.14) + - PINRemoteImage/Core (3.0.0-beta.14): - PINOperation - - PINRemoteImage/FLAnimatedImage (3.0.0-beta.13): - - FLAnimatedImage (>= 1.0) - - PINRemoteImage/Core - - PINRemoteImage/PINCache (3.0.0-beta.13): - - PINCache (= 3.0.1-beta.6) + - PINRemoteImage/PINCache (3.0.0-beta.14): + - PINCache (= 3.0.1-beta.7) - PINRemoteImage/Core DEPENDENCIES: - FBSnapshotTestCase/Core (~> 2.1) - JGMethodSwizzler (from `https://github.com/JonasGessner/JGMethodSwizzler`, branch `master`) - OCMock (= 3.4.1) - - PINRemoteImage (= 3.0.0-beta.13) + - PINRemoteImage (= 3.0.0-beta.14) SPEC REPOS: https://github.com/cocoapods/specs.git: - FBSnapshotTestCase - - FLAnimatedImage - OCMock - PINCache - PINOperation @@ -50,13 +44,12 @@ CHECKOUT OPTIONS: SPEC CHECKSUMS: FBSnapshotTestCase: 094f9f314decbabe373b87cc339bea235a63e07a - FLAnimatedImage: 4a0b56255d9b05f18b6dd7ee06871be5d3b89e31 JGMethodSwizzler: 7328146117fffa8a4038c42eb7cd3d4c75006f97 OCMock: 2cd0716969bab32a2283ff3a46fd26a8c8b4c5e3 - PINCache: d195fdba255283f7e9900a55e3cced377f431f9b + PINCache: 7cb9ae068c8f655717f7c644ef1dff9fd573e979 PINOperation: a6219e6fc9db9c269eb7a7b871ac193bcf400aac - PINRemoteImage: d6d51c5d2adda55f1ce30c96e850b6c4ebd2856a + PINRemoteImage: 81bbff853acc71c6de9e106e9e489a791b8bbb08 -PODFILE CHECKSUM: 42715d61f73cc22cc116bf80d7b268cb1f9e4742 +PODFILE CHECKSUM: 445046ac151568c694ff286684322273f0b597d6 -COCOAPODS: 1.5.3 +COCOAPODS: 1.6.0 diff --git a/Schemas/configuration.json b/Schemas/configuration.json index 2b95d767f..64a4c790b 100644 --- a/Schemas/configuration.json +++ b/Schemas/configuration.json @@ -25,7 +25,8 @@ "exp_disable_a11y_cache", "exp_skip_a11y_wait", "exp_new_default_cell_layout_mode", - "exp_dispatch_apply" + "exp_dispatch_apply", + "exp_image_downloader_priority" ] } } diff --git a/Source/ASExperimentalFeatures.h b/Source/ASExperimentalFeatures.h index f78b4332c..f3e6b295b 100644 --- a/Source/ASExperimentalFeatures.h +++ b/Source/ASExperimentalFeatures.h @@ -31,6 +31,7 @@ typedef NS_OPTIONS(NSUInteger, ASExperimentalFeatures) { ASExperimentalSkipAccessibilityWait = 1 << 10, // exp_skip_a11y_wait ASExperimentalNewDefaultCellLayoutMode = 1 << 11, // exp_new_default_cell_layout_mode ASExperimentalDispatchApply = 1 << 12, // exp_dispatch_apply + ASExperimentalImageDownloaderPriority = 1 << 13, // exp_image_downloader_priority ASExperimentalFeatureAll = 0xFFFFFFFF }; diff --git a/Source/ASExperimentalFeatures.mm b/Source/ASExperimentalFeatures.mm index 0a8972fe3..d1f9a7966 100644 --- a/Source/ASExperimentalFeatures.mm +++ b/Source/ASExperimentalFeatures.mm @@ -24,8 +24,9 @@ @"exp_disable_a11y_cache", @"exp_skip_a11y_wait", @"exp_new_default_cell_layout_mode", - @"exp_dispatch_apply"])); - + @"exp_dispatch_apply", + @"exp_image_downloader_priority"])); + if (flags == ASExperimentalFeatureAll) { return allNames; } diff --git a/Source/ASMultiplexImageNode.mm b/Source/ASMultiplexImageNode.mm index 2adc646f9..bd61dc02c 100644 --- a/Source/ASMultiplexImageNode.mm +++ b/Source/ASMultiplexImageNode.mm @@ -53,7 +53,13 @@ @interface ASMultiplexImageNode () @private // Core. id _cache; + id _downloader; + struct { + unsigned int downloaderImplementsSetProgress:1; + unsigned int downloaderImplementsSetPriority:1; + unsigned int downloaderImplementsDownloadWithPriority:1; + } _downloaderFlags; __weak id _delegate; struct { @@ -89,8 +95,6 @@ @interface ASMultiplexImageNode () BOOL _shouldRenderProgressImages; //set on init only - BOOL _downloaderImplementsSetProgress; - BOOL _downloaderImplementsSetPriority; BOOL _cacheSupportsClearing; } @@ -171,13 +175,14 @@ - (instancetype)initWithCache:(id)cache downloader:(id)cache; _downloader = (id)downloader; - _downloaderImplementsSetProgress = [downloader respondsToSelector:@selector(setProgressImageBlock:callbackQueue:withDownloadIdentifier:)]; - _downloaderImplementsSetPriority = [downloader respondsToSelector:@selector(setPriority:withDownloadIdentifier:)]; + _downloaderFlags.downloaderImplementsSetProgress = [downloader respondsToSelector:@selector(setProgressImageBlock:callbackQueue:withDownloadIdentifier:)]; + _downloaderFlags.downloaderImplementsSetPriority = [downloader respondsToSelector:@selector(setPriority:withDownloadIdentifier:)]; + _downloaderFlags.downloaderImplementsDownloadWithPriority = [downloader respondsToSelector:@selector(downloadImageWithURL:priority:callbackQueue:downloadProgress:completion:)]; _cacheSupportsClearing = [cache respondsToSelector:@selector(clearFetchedImageFromCacheWithURL:)]; _shouldRenderProgressImages = YES; - + self.shouldBypassEnsureDisplay = YES; return self; @@ -271,17 +276,8 @@ - (BOOL)placeholderShouldPersist - (void)displayWillStartAsynchronously:(BOOL)asynchronously { [super displayWillStartAsynchronously:asynchronously]; - [self didEnterPreloadState]; - - if (_downloaderImplementsSetPriority) { - { - ASDN::MutexLocker l(_downloadIdentifierLock); - if (_downloadIdentifier != nil) { - [_downloader setPriority:ASImageDownloaderPriorityImminent withDownloadIdentifier:_downloadIdentifier]; - } - } - } + [self _updatePriorityOnDownloaderIfNeededWithDefaultPriority:ASImageDownloaderPriorityImminent]; } /* didEnterVisibleState / didExitVisibleState in ASNetworkImageNode has a very similar implementation. Changes here are likely necessary @@ -289,31 +285,25 @@ - (void)displayWillStartAsynchronously:(BOOL)asynchronously - (void)didEnterVisibleState { [super didEnterVisibleState]; - - if (_downloaderImplementsSetPriority) { - ASDN::MutexLocker l(_downloadIdentifierLock); - if (_downloadIdentifier != nil) { - [_downloader setPriority:ASImageDownloaderPriorityVisible withDownloadIdentifier:_downloadIdentifier]; - } - } - + [self _updatePriorityOnDownloaderIfNeededWithDefaultPriority:ASImageDownloaderPriorityVisible]; [self _updateProgressImageBlockOnDownloaderIfNeeded]; } - (void)didExitVisibleState { [super didExitVisibleState]; - - if (_downloaderImplementsSetPriority) { - ASDN::MutexLocker l(_downloadIdentifierLock); - if (_downloadIdentifier != nil) { - [_downloader setPriority:ASImageDownloaderPriorityPreload withDownloadIdentifier:_downloadIdentifier]; - } - } - + [self _updatePriorityOnDownloaderIfNeededWithDefaultPriority:ASImageDownloaderPriorityPreload]; [self _updateProgressImageBlockOnDownloaderIfNeeded]; } +- (void)didExitDisplayState +{ + [super didExitDisplayState]; + if (ASActivateExperimentalFeature(ASExperimentalImageDownloaderPriority)) { + [self _updatePriorityOnDownloaderIfNeededWithDefaultPriority:ASImageDownloaderPriorityPreload]; + } +} + #pragma mark - Core - (void)setImage:(UIImage *)image @@ -449,7 +439,6 @@ - (void)_setDownloadIdentifier:(id)downloadIdentifier _downloadIdentifier = downloadIdentifier; } - #pragma mark - Image Loading Machinery - (void)_loadImageIdentifiers @@ -493,19 +482,37 @@ - (UIImage *)_bestImmediatelyAvailableImageFromDataSource:(id *)imageIdentifierO #pragma mark - -/** - @note: This should be called without _downloadIdentifierLock held. We will lock - super to read our interface state and it's best to avoid acquiring both locks. - */ +- (void)_updatePriorityOnDownloaderIfNeededWithDefaultPriority:(ASImageDownloaderPriority)defaultPriority +{ + ASAssertUnlocked(_downloadIdentifierLock); + + if (_downloaderFlags.downloaderImplementsSetPriority) { + // Read our interface state before locking so that we don't lock super while holding our lock. + ASInterfaceState interfaceState = self.interfaceState; + ASDN::MutexLocker l(_downloadIdentifierLock); + + if (_downloadIdentifier != nil) { + ASImageDownloaderPriority priority = defaultPriority; + if (ASActivateExperimentalFeature(ASExperimentalImageDownloaderPriority)) { + priority = ASImageDownloaderPriorityWithInterfaceState(interfaceState); + } + + [_downloader setPriority:priority withDownloadIdentifier:_downloadIdentifier]; + } + } +} + - (void)_updateProgressImageBlockOnDownloaderIfNeeded { + ASAssertUnlocked(_downloadIdentifierLock); + BOOL shouldRenderProgressImages = self.shouldRenderProgressImages; // Read our interface state before locking so that we don't lock super while holding our lock. ASInterfaceState interfaceState = self.interfaceState; ASDN::MutexLocker l(_downloadIdentifierLock); - if (!_downloaderImplementsSetProgress || _downloadIdentifier == nil) { + if (!_downloaderFlags.downloaderImplementsSetProgress || _downloadIdentifier == nil) { return; } @@ -825,7 +832,7 @@ - (void)_downloadImageWithIdentifier:(id)imageIdentifier URL:(NSURL *)imageURL c [_delegate multiplexImageNode:self didStartDownloadOfImageWithIdentifier:imageIdentifier]; __weak __typeof__(self) weakSelf = self; - void (^downloadProgressBlock)(CGFloat) = nil; + ASImageDownloaderProgress downloadProgressBlock = NULL; if (_delegateFlags.downloadProgress) { downloadProgressBlock = ^(CGFloat progress) { __typeof__(self) strongSelf = weakSelf; @@ -835,30 +842,67 @@ - (void)_downloadImageWithIdentifier:(id)imageIdentifier URL:(NSURL *)imageURL c }; } + ASImageDownloaderCompletion completion = ^(id imageContainer, NSError *error, id downloadIdentifier, id userInfo) { + // We dereference iVars directly, so we can't have weakSelf going nil on us. + __typeof__(self) strongSelf = weakSelf; + if (!strongSelf) + return; + + ASDN::MutexLocker l(strongSelf->_downloadIdentifierLock); + //Getting a result back for a different download identifier, download must not have been successfully canceled + if (ASObjectIsEqual(strongSelf->_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) { + return; + } + + completionBlock([imageContainer asdk_image], error); + + // Delegateify. + if (strongSelf->_delegateFlags.downloadFinish) + [strongSelf->_delegate multiplexImageNode:weakSelf didFinishDownloadingImageWithIdentifier:imageIdentifier error:error]; + }; + // Download! ASPerformBlockOnBackgroundThread(^{ - [self _setDownloadIdentifier:[_downloader downloadImageWithURL:imageURL - callbackQueue:dispatch_get_main_queue() - downloadProgress:downloadProgressBlock - completion:^(id imageContainer, NSError *error, id downloadIdentifier, id userInfo) { - // We dereference iVars directly, so we can't have weakSelf going nil on us. - __typeof__(self) strongSelf = weakSelf; - if (!strongSelf) - return; - - ASDN::MutexLocker l(_downloadIdentifierLock); - //Getting a result back for a different download identifier, download must not have been successfully canceled - if (ASObjectIsEqual(_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) { - return; - } - - completionBlock([imageContainer asdk_image], error); - - // Delegateify. - if (strongSelf->_delegateFlags.downloadFinish) - [strongSelf->_delegate multiplexImageNode:weakSelf didFinishDownloadingImageWithIdentifier:imageIdentifier error:error]; - }]]; - [self _updateProgressImageBlockOnDownloaderIfNeeded]; + __typeof__(self) strongSelf = weakSelf; + if (!strongSelf) + return; + + dispatch_queue_t callbackQueue = dispatch_get_main_queue(); + + id downloadIdentifier; + if (strongSelf->_downloaderFlags.downloaderImplementsDownloadWithPriority + && ASActivateExperimentalFeature(ASExperimentalImageDownloaderPriority)) { + + /* + Decide a priority based on the current interface state of this node. + It can happen that this method was called when the node entered preload state + but the interface state, at this point, tells us that the node is (going to be) visible, + If that's the case, we jump to a higher priority directly. + */ + ASImageDownloaderPriority priority = ASImageDownloaderPriorityWithInterfaceState(strongSelf.interfaceState); + downloadIdentifier = [strongSelf->_downloader downloadImageWithURL:imageURL + priority:priority + callbackQueue:callbackQueue + downloadProgress:downloadProgressBlock + completion:completion]; + } else { + /* + Kick off a download with default priority. + The actual "default" value is decided by the downloader. + ASBasicImageDownloader and ASPINRemoteImageDownloader both use ASImageDownloaderPriorityImminent + which is mapped to NSURLSessionTaskPriorityDefault. + + This means that preload and display nodes use the same priority + and their requests are put into the same pool. + */ + downloadIdentifier = [strongSelf->_downloader downloadImageWithURL:imageURL + callbackQueue:callbackQueue + downloadProgress:downloadProgressBlock + completion:completion]; + } + + [strongSelf _setDownloadIdentifier:downloadIdentifier]; + [strongSelf _updateProgressImageBlockOnDownloaderIfNeeded]; }); } diff --git a/Source/ASNetworkImageNode.mm b/Source/ASNetworkImageNode.mm index f19684459..e7910cd78 100644 --- a/Source/ASNetworkImageNode.mm +++ b/Source/ASNetworkImageNode.mm @@ -68,6 +68,7 @@ @interface ASNetworkImageNode () unsigned int downloaderImplementsSetPriority:1; unsigned int downloaderImplementsAnimatedImage:1; unsigned int downloaderImplementsCancelWithResume:1; + unsigned int downloaderImplementsDownloadWithPriority:1; } _downloaderFlags; // Immutable and set on init only. We don't need to lock in this case. @@ -98,6 +99,7 @@ - (instancetype)initWithCache:(id)cache downloader:(id ima NSURL *url; id downloadIdentifier; BOOL cancelAndReattempt = NO; - + ASInterfaceState interfaceState; + // Below, to avoid performance issues, we're calling downloadImageWithURL without holding the lock. This is a bit ugly because // We need to reobtain the lock after and ensure that the task we've kicked off still matches our URL. If not, we need to cancel // it and try again. { ASLockScopeSelf(); url = self->_URL; + interfaceState = self->_interfaceState; } - - downloadIdentifier = [self->_downloader downloadImageWithURL:url - callbackQueue:[self callbackQueue] - downloadProgress:NULL - completion:^(id _Nullable imageContainer, NSError * _Nullable error, id _Nullable downloadIdentifier, id _Nullable userInfo) { - if (finished != NULL) { - finished(imageContainer, error, downloadIdentifier, userInfo); - } - }]; + dispatch_queue_t callbackQueue = [self callbackQueue]; + ASImageDownloaderProgress downloadProgress = NULL; + ASImageDownloaderCompletion completion = ^(id _Nullable imageContainer, NSError * _Nullable error, id _Nullable downloadIdentifier, id _Nullable userInfo) { + if (finished != NULL) { + finished(imageContainer, error, downloadIdentifier, userInfo); + } + }; + + if (self->_downloaderFlags.downloaderImplementsDownloadWithPriority + && ASActivateExperimentalFeature(ASExperimentalImageDownloaderPriority)) { + /* + Decide a priority based on the current interface state of this node. + It can happen that this method was called when the node entered preload state + but the interface state, at this point, tells us that the node is (going to be) visible. + If that's the case, we jump to a higher priority directly. + */ + ASImageDownloaderPriority priority = ASImageDownloaderPriorityWithInterfaceState(interfaceState); + + downloadIdentifier = [self->_downloader downloadImageWithURL:url + priority:priority + callbackQueue:callbackQueue + downloadProgress:downloadProgress + completion:completion]; + } else { + /* + Kick off a download with default priority. + The actual "default" value is decided by the downloader. + ASBasicImageDownloader and ASPINRemoteImageDownloader both use ASImageDownloaderPriorityImminent + which is mapped to NSURLSessionTaskPriorityDefault. + + This means that preload and display nodes use the same priority + and their requests are put into the same pool. + */ + downloadIdentifier = [self->_downloader downloadImageWithURL:url + callbackQueue:callbackQueue + downloadProgress:downloadProgress + completion:completion]; + } as_log_verbose(ASImageLoadingLog(), "Downloading image for %@ url: %@", self, url); { diff --git a/Source/Details/ASBasicImageDownloader.mm b/Source/Details/ASBasicImageDownloader.mm index ae92c5350..6c4010f57 100644 --- a/Source/Details/ASBasicImageDownloader.mm +++ b/Source/Details/ASBasicImageDownloader.mm @@ -9,6 +9,7 @@ #import +#import #import #import @@ -25,6 +26,19 @@ NSString * const kASBasicImageDownloaderContextProgressBlock = @"kASBasicImageDownloaderContextProgressBlock"; NSString * const kASBasicImageDownloaderContextCompletionBlock = @"kASBasicImageDownloaderContextCompletionBlock"; +static inline float NSURLSessionTaskPriorityWithImageDownloaderPriority(ASImageDownloaderPriority priority) { + switch (priority) { + case ASImageDownloaderPriorityPreload: + return NSURLSessionTaskPriorityLow; + + case ASImageDownloaderPriorityImminent: + return NSURLSessionTaskPriorityDefault; + + case ASImageDownloaderPriorityVisible: + return NSURLSessionTaskPriorityHigh; + } +} + @interface ASBasicImageDownloaderContext () { BOOL _invalid; @@ -238,10 +252,23 @@ - (instancetype)_init #pragma mark ASImageDownloaderProtocol. -- (id)downloadImageWithURL:(NSURL *)URL - callbackQueue:(dispatch_queue_t)callbackQueue - downloadProgress:(nullable ASImageDownloaderProgress)downloadProgress - completion:(ASImageDownloaderCompletion)completion +- (nullable id)downloadImageWithURL:(NSURL *)URL + callbackQueue:(dispatch_queue_t)callbackQueue + downloadProgress:(nullable ASImageDownloaderProgress)downloadProgress + completion:(ASImageDownloaderCompletion)completion +{ + return [self downloadImageWithURL:URL + priority:ASImageDownloaderPriorityImminent // maps to default priority + callbackQueue:callbackQueue + downloadProgress:downloadProgress + completion:completion]; +} + +- (nullable id)downloadImageWithURL:(NSURL *)URL + priority:(ASImageDownloaderPriority)priority + callbackQueue:(dispatch_queue_t)callbackQueue + downloadProgress:(ASImageDownloaderProgress)downloadProgress + completion:(ASImageDownloaderCompletion)completion { ASBasicImageDownloaderContext *context = [ASBasicImageDownloaderContext contextForURL:URL]; @@ -266,6 +293,7 @@ - (id)downloadImageWithURL:(NSURL *)URL NSURLSessionDownloadTask *task = (NSURLSessionDownloadTask *)[context createSessionTaskIfNecessaryWithBlock:^(){return [_session downloadTaskWithURL:URL];}]; if (task) { + task.priority = NSURLSessionTaskPriorityWithImageDownloaderPriority(priority); task.originalRequest.asyncdisplaykit_context = context; // start downloading diff --git a/Source/Details/ASImageProtocols.h b/Source/Details/ASImageProtocols.h index 10ad85db3..fae3b9f5b 100644 --- a/Source/Details/ASImageProtocols.h +++ b/Source/Details/ASImageProtocols.h @@ -102,17 +102,35 @@ typedef NS_ENUM(NSUInteger, ASImageDownloaderPriority) { /** @abstract Cancels an image download. @param downloadIdentifier The opaque download identifier object returned from - `downloadImageWithURL:callbackQueue:downloadProgressBlock:completion:`. + `downloadImageWithURL:callbackQueue:downloadProgress:completion:`. @discussion This method has no effect if `downloadIdentifier` is nil. */ - (void)cancelImageDownloadForIdentifier:(id)downloadIdentifier; @optional +/** + @abstract Downloads an image with the given URL. + @param URL The URL of the image to download. + @param priority The priority at which the image should be downloaded. + @param callbackQueue The queue to call `downloadProgressBlock` and `completion` on. + @param downloadProgress The block to be invoked when the download of `URL` progresses. + @param completion The block to be invoked when the download has completed, or has failed. + @discussion This method is likely to be called on the main thread, so any custom implementations should make sure to background any expensive download operations. + @note If this method is implemented, it will be called instead of the required method (`downloadImageWithURL:callbackQueue:downloadProgress:completion:`). + @result An opaque identifier to be used in canceling the download, via `cancelImageDownloadForIdentifier:`. You must + retain the identifier if you wish to use it later. + */ +- (nullable id)downloadImageWithURL:(NSURL *)URL + priority:(ASImageDownloaderPriority)priority + callbackQueue:(dispatch_queue_t)callbackQueue + downloadProgress:(nullable ASImageDownloaderProgress)downloadProgress + completion:(ASImageDownloaderCompletion)completion; + /** @abstract Cancels an image download, however indicating resume data should be stored in case of redownload. @param downloadIdentifier The opaque download identifier object returned from - `downloadImageWithURL:callbackQueue:downloadProgressBlock:completion:`. + `downloadImageWithURL:callbackQueue:downloadProgress:completion:`. @discussion This method has no effect if `downloadIdentifier` is nil. If implemented, this method may be called instead of `cancelImageDownloadForIdentifier:` in cases where ASDK believes there's a chance the image download will be resumed (currently when an image exits preload range). You can use this to store @@ -130,7 +148,7 @@ typedef NS_ENUM(NSUInteger, ASImageDownloaderPriority) { /** @abstract Sets block to be called when a progress image is available. @param progressBlock The block to be invoked when the download has a progressive render of an image available. - @param callbackQueue The queue to call `progressBlock` on. + @param callbackQueue The queue to call `progressImageBlock` on. @param downloadIdentifier The opaque download identifier object returned from `downloadImageWithURL:callbackQueue:downloadProgressBlock:completion:`. */ diff --git a/Source/Details/ASPINRemoteImageDownloader.mm b/Source/Details/ASPINRemoteImageDownloader.mm index 0276946f9..3daf06e33 100644 --- a/Source/Details/ASPINRemoteImageDownloader.mm +++ b/Source/Details/ASPINRemoteImageDownloader.mm @@ -34,6 +34,19 @@ #import #import +static inline PINRemoteImageManagerPriority PINRemoteImageManagerPriorityWithASImageDownloaderPriority(ASImageDownloaderPriority priority) { + switch (priority) { + case ASImageDownloaderPriorityPreload: + return PINRemoteImageManagerPriorityLow; + + case ASImageDownloaderPriorityImminent: + return PINRemoteImageManagerPriorityDefault; + + case ASImageDownloaderPriorityVisible: + return PINRemoteImageManagerPriorityHigh; + } +} + #if PIN_ANIMATED_AVAILABLE @interface ASPINRemoteImageDownloader () @@ -245,6 +258,21 @@ - (nullable id)downloadImageWithURL:(NSURL *)URL downloadProgress:(ASImageDownloaderProgress)downloadProgress completion:(ASImageDownloaderCompletion)completion; { + return [self downloadImageWithURL:URL + priority:ASImageDownloaderPriorityImminent // maps to default priority + callbackQueue:callbackQueue + downloadProgress:downloadProgress + completion:completion]; +} + +- (nullable id)downloadImageWithURL:(NSURL *)URL + priority:(ASImageDownloaderPriority)priority + callbackQueue:(dispatch_queue_t)callbackQueue + downloadProgress:(ASImageDownloaderProgress)downloadProgress + completion:(ASImageDownloaderCompletion)completion +{ + PINRemoteImageManagerPriority pi_priority = PINRemoteImageManagerPriorityWithASImageDownloaderPriority(priority); + PINRemoteImageManagerProgressDownload progressDownload = ^(int64_t completedBytes, int64_t totalBytes) { if (downloadProgress == nil) { return; } @@ -274,6 +302,7 @@ - (nullable id)downloadImageWithURL:(NSURL *)URL // check the cache as part of this download. return [[self sharedPINRemoteImageManager] downloadImageWithURL:URL options:PINRemoteImageManagerDownloadOptionsSkipDecode | PINRemoteImageManagerDownloadOptionsIgnoreCache + priority:pi_priority progressImage:nil progressDownload:progressDownload completion:imageCompletion]; @@ -310,20 +339,7 @@ - (void)setPriority:(ASImageDownloaderPriority)priority withDownloadIdentifier:( { ASDisplayNodeAssert([downloadIdentifier isKindOfClass:[NSUUID class]], @"downloadIdentifier must be NSUUID"); - PINRemoteImageManagerPriority pi_priority = PINRemoteImageManagerPriorityDefault; - switch (priority) { - case ASImageDownloaderPriorityPreload: - pi_priority = PINRemoteImageManagerPriorityLow; - break; - - case ASImageDownloaderPriorityImminent: - pi_priority = PINRemoteImageManagerPriorityDefault; - break; - - case ASImageDownloaderPriorityVisible: - pi_priority = PINRemoteImageManagerPriorityHigh; - break; - } + PINRemoteImageManagerPriority pi_priority = PINRemoteImageManagerPriorityWithASImageDownloaderPriority(priority); [[self sharedPINRemoteImageManager] setPriority:pi_priority ofTaskWithUUID:downloadIdentifier]; } diff --git a/Source/Private/ASInternalHelpers.h b/Source/Private/ASInternalHelpers.h index e6d3811c7..8bc53fa0e 100644 --- a/Source/Private/ASInternalHelpers.h +++ b/Source/Private/ASInternalHelpers.h @@ -12,6 +12,8 @@ #import #import +#import +#import NS_ASSUME_NONNULL_BEGIN @@ -95,6 +97,22 @@ ASDISPLAYNODE_INLINE UIEdgeInsets ASConcatInsets(UIEdgeInsets insetsA, UIEdgeIns return insetsA; } +ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASImageDownloaderPriority ASImageDownloaderPriorityWithInterfaceState(ASInterfaceState interfaceState) { + if (ASInterfaceStateIncludesVisible(interfaceState)) { + return ASImageDownloaderPriorityVisible; + } + + if (ASInterfaceStateIncludesDisplay(interfaceState)) { + return ASImageDownloaderPriorityImminent; + } + + if (ASInterfaceStateIncludesPreload(interfaceState)) { + return ASImageDownloaderPriorityPreload; + } + + return ASImageDownloaderPriorityPreload; +} + @interface NSIndexPath (ASInverseComparison) - (NSComparisonResult)asdk_inverseCompare:(NSIndexPath *)otherIndexPath; @end diff --git a/Tests/ASConfigurationTests.mm b/Tests/ASConfigurationTests.mm index aff309176..a5f37c11c 100644 --- a/Tests/ASConfigurationTests.mm +++ b/Tests/ASConfigurationTests.mm @@ -30,7 +30,8 @@ ASExperimentalDisableAccessibilityCache, ASExperimentalSkipAccessibilityWait, ASExperimentalNewDefaultCellLayoutMode, - ASExperimentalDispatchApply + ASExperimentalDispatchApply, + ASExperimentalImageDownloaderPriority }; @interface ASConfigurationTests : ASTestCase @@ -55,7 +56,8 @@ + (NSArray *)names { @"exp_disable_a11y_cache", @"exp_skip_a11y_wait", @"exp_new_default_cell_layout_mode", - @"exp_dispatch_apply" + @"exp_dispatch_apply", + @"exp_image_downloader_priority" ]; } From 54ddc3d11c42121396880c65a6d75b4587083d62 Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Fri, 8 Mar 2019 08:18:11 -0800 Subject: [PATCH 034/122] Whoops, someone forgot an else! (#1385) Luckily it seems this didn't cause huge memory bloat because there's a check below that keeps the display flag from being set in low memory conditions when the node is off the screen. --- Source/Details/ASRangeController.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Details/ASRangeController.mm b/Source/Details/ASRangeController.mm index d2ab45a56..d16b281d5 100644 --- a/Source/Details/ASRangeController.mm +++ b/Source/Details/ASRangeController.mm @@ -280,7 +280,7 @@ - (void)_updateVisibleNodeIndexPaths } else { if (emptyDisplayRange == YES) { displayElements = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality]; - } if (equalDisplayVisible == YES) { + } else if (equalDisplayVisible == YES) { displayElements = visibleElements; } else { // Calculating only the Display range means the Preload range is either the same as Display or Visible. From 4a16e99d5d3d88abfbcace6418b2647b62a6150c Mon Sep 17 00:00:00 2001 From: Kevin Date: Fri, 8 Mar 2019 08:24:08 -0800 Subject: [PATCH 035/122] Catch invalid sizes during yoga layout (#1376) --- Source/ASDisplayNode+Yoga.mm | 18 ++++++++++++++---- Source/Layout/ASDimension.h | 6 ++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Source/ASDisplayNode+Yoga.mm b/Source/ASDisplayNode+Yoga.mm index 97e7cb56b..038459fa4 100644 --- a/Source/ASDisplayNode+Yoga.mm +++ b/Source/ASDisplayNode+Yoga.mm @@ -15,6 +15,7 @@ #import #import #import +#import #import #import #import @@ -183,6 +184,13 @@ - (ASLayout *)layoutForYogaNode CGSize size = CGSizeMake(YGNodeLayoutGetWidth(yogaNode), YGNodeLayoutGetHeight(yogaNode)); CGPoint position = CGPointMake(YGNodeLayoutGetLeft(yogaNode), YGNodeLayoutGetTop(yogaNode)); + if (!ASIsCGSizeValidForSize(size)) { + size = CGSizeZero; + } + + if (!ASIsCGPositionValidForLayout(position)) { + position = CGPointZero; + } return [ASLayout layoutWithLayoutElement:self size:size position:position sublayouts:nil]; } @@ -205,6 +213,9 @@ - (void)setupYogaCalculatedLayout // The layout for self should have position CGPointNull, but include the calculated size. CGSize size = CGSizeMake(YGNodeLayoutGetWidth(yogaNode), YGNodeLayoutGetHeight(yogaNode)); + if (!ASIsCGSizeValidForSize(size)) { + size = CGSizeZero; + } ASLayout *layout = [ASLayout layoutWithLayoutElement:self size:size sublayouts:sublayouts]; #if ASDISPLAYNODE_ASSERTIONS_ENABLED @@ -398,11 +409,10 @@ - (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize NSLog(@"******************** STARTING YOGA -> ASLAYOUT CREATION ********************"); NSLog(@"****************************************************************************"); ASDisplayNodePerformBlockOnEveryYogaChild(self, ^(ASDisplayNode * _Nonnull node) { - NSLog(@" "); // Newline NSLog(@"node = %@", node); - NSLog(@"style = %@", node.style); - NSLog(@"layout = %@", node.yogaCalculatedLayout); - YGNodePrint(node.yogaNode, (YGPrintOptions)(YGPrintOptionsStyle | YGPrintOptionsLayout)); + YGNodePrint(node.style.yogaNode, (YGPrintOptions)(YGPrintOptionsStyle | YGPrintOptionsLayout)); + NSCAssert(ASIsCGSizeValidForSize(node.yogaCalculatedLayout.size), @"Yoga layout returned an invalid size"); + NSLog(@" "); // Newline }); } #endif /* YOGA_LAYOUT_LOGGING */ diff --git a/Source/Layout/ASDimension.h b/Source/Layout/ASDimension.h index 0cad40d27..0b3c6b7be 100644 --- a/Source/Layout/ASDimension.h +++ b/Source/Layout/ASDimension.h @@ -27,9 +27,10 @@ ASDISPLAYNODE_INLINE BOOL AS_WARN_UNUSED_RESULT ASIsCGSizeValidForLayout(CGSize return (ASPointsValidForLayout(size.width) && ASPointsValidForLayout(size.height)); } +// Note we want YGUndefined (10E20) to be considered invalid, so we have picked a smaller number than CGFLOAT_MAX/2.0 ASDISPLAYNODE_INLINE BOOL AS_WARN_UNUSED_RESULT ASPointsValidForSize(CGFloat points) { - return ((isnormal(points) || points == 0.0) && points >= 0.0 && points < (FLT_MAX / 2.0)); + return ((isnormal(points) || points == 0.0) && points >= 0.0 && points < 10000000.0); } ASDISPLAYNODE_INLINE BOOL AS_WARN_UNUSED_RESULT ASIsCGSizeValidForSize(CGSize size) @@ -37,9 +38,10 @@ ASDISPLAYNODE_INLINE BOOL AS_WARN_UNUSED_RESULT ASIsCGSizeValidForSize(CGSize si return (ASPointsValidForSize(size.width) && ASPointsValidForSize(size.height)); } +// Note we want YGUndefined (10E20) to be considered invalid, so we have picked a smaller number than CGFLOAT_MAX/2.0 ASDISPLAYNODE_INLINE BOOL ASIsCGPositionPointsValidForLayout(CGFloat points) { - return ((isnormal(points) || points == 0.0) && points < (CGFLOAT_MAX / 2.0)); + return ((isnormal(points) || points == 0.0) && points < 10000000.0); } ASDISPLAYNODE_INLINE BOOL ASIsCGPositionValidForLayout(CGPoint point) From 5827e977e1f4b9b1a6ad9dd975a24912b7a688fd Mon Sep 17 00:00:00 2001 From: Kevin Date: Fri, 8 Mar 2019 08:24:24 -0800 Subject: [PATCH 036/122] Correct View vs. Layer here (tho it doesn't make any difference) (#1378) Older FBSnapshot versions, however, don't use VerifyViewOrLayer --- Tests/ASSnapshotTestCase.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/ASSnapshotTestCase.h b/Tests/ASSnapshotTestCase.h index a59079f1d..0741aaab5 100644 --- a/Tests/ASSnapshotTestCase.h +++ b/Tests/ASSnapshotTestCase.h @@ -27,7 +27,7 @@ NSOrderedSet *ASSnapshotTestCaseDefaultSuffixes(void); FBSnapshotVerifyViewWithOptions(view__, identifier__, ASSnapshotTestCaseDefaultSuffixes(), 0); #define ASSnapshotVerifyViewWithTolerance(view__, identifier__, tolerance__) \ - FBSnapshotVerifyLayerWithOptions(view__, identifier__, ASSnapshotTestCaseDefaultSuffixes(), tolerance__); + FBSnapshotVerifyViewWithOptions(view__, identifier__, ASSnapshotTestCaseDefaultSuffixes(), tolerance__); @interface ASSnapshotTestCase : FBSnapshotTestCase From 2c0dfda0141adde14dea5f71ed4109f34f5f84a6 Mon Sep 17 00:00:00 2001 From: Kevin Date: Fri, 8 Mar 2019 08:26:35 -0800 Subject: [PATCH 037/122] Expose initial constrained size before layout in case anyone is interested later during the layout on the same thread. (#1377) Optionally, clients are expected to clean it up after usage to avoid accessing outdated data. --- Source/Details/ASDataController.mm | 6 ++++++ Source/Layout/ASLayout.h | 2 ++ Source/Layout/ASLayout.mm | 2 ++ 3 files changed, 10 insertions(+) diff --git a/Source/Details/ASDataController.mm b/Source/Details/ASDataController.mm index b4e8e9a6d..f36dd3950 100644 --- a/Source/Details/ASDataController.mm +++ b/Source/Details/ASDataController.mm @@ -168,7 +168,13 @@ - (void)_allocateNodesFromElements:(NSArray *)elements } unowned ASCollectionElement *element = elements[i]; + + NSMutableDictionary *dict = [[NSThread currentThread] threadDictionary]; + dict[ASThreadDictMaxConstraintSizeKey] = + [NSValue valueWithCGSize:element.constrainedSize.max]; unowned ASCellNode *node = element.node; + [dict removeObjectForKey:ASThreadDictMaxConstraintSizeKey]; + // Layout the node if the size range is valid. ASSizeRange sizeRange = element.constrainedSize; if (ASSizeRangeHasSignificantArea(sizeRange)) { diff --git a/Source/Layout/ASLayout.h b/Source/Layout/ASLayout.h index af4ecd984..0e40574d0 100644 --- a/Source/Layout/ASLayout.h +++ b/Source/Layout/ASLayout.h @@ -18,6 +18,8 @@ AS_EXTERN CGPoint const ASPointNull; // {NAN, NAN} AS_EXTERN BOOL ASPointIsNull(CGPoint point); +AS_EXTERN NSString *const ASThreadDictMaxConstraintSizeKey; + /** * Safely calculates the layout of the given root layoutElement by guarding against nil nodes. * @param rootLayoutElement The root node to calculate the layout for. diff --git a/Source/Layout/ASLayout.mm b/Source/Layout/ASLayout.mm index 6da24e54a..055daa88f 100644 --- a/Source/Layout/ASLayout.mm +++ b/Source/Layout/ASLayout.mm @@ -21,6 +21,8 @@ #import #import +NSString *const ASThreadDictMaxConstraintSizeKey = @"kASThreadDictMaxConstraintSizeKey"; + CGPoint const ASPointNull = {NAN, NAN}; BOOL ASPointIsNull(CGPoint point) From 975cd377cc1b03118bb3493de893feb54d3129e6 Mon Sep 17 00:00:00 2001 From: Kevin Date: Fri, 8 Mar 2019 08:26:52 -0800 Subject: [PATCH 038/122] Assert for context creation failure during displayBlock. (#1373) Nothing good can possibly happen if we continue without one. --- Source/Private/ASDisplayNode+AsyncDisplay.mm | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Source/Private/ASDisplayNode+AsyncDisplay.mm b/Source/Private/ASDisplayNode+AsyncDisplay.mm index d473780e4..cb9a2f42e 100644 --- a/Source/Private/ASDisplayNode+AsyncDisplay.mm +++ b/Source/Private/ASDisplayNode+AsyncDisplay.mm @@ -234,7 +234,12 @@ - (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchro CGContextRef currentContext = UIGraphicsGetCurrentContext(); UIImage *image = nil; - + + if (shouldCreateGraphicsContext && !currentContext) { + ASDisplayNodeAssert(NO, @"Failed to create a CGContext (size: %@)", NSStringFromCGSize(bounds.size)); + return nil; + } + // For -display methods, we don't have a context, and thus will not call the _willDisplayNodeContentWithRenderingContext or // _didDisplayNodeContentWithRenderingContext blocks. It's up to the implementation of -display... to do what it needs. [self __willDisplayNodeContentWithRenderingContext:currentContext drawParameters:drawParameters]; From 46e2a81fbfac99a86d299191606d6c6997fa97b7 Mon Sep 17 00:00:00 2001 From: Kevin Date: Fri, 8 Mar 2019 08:27:49 -0800 Subject: [PATCH 039/122] Fix an issue where state change notifications can be sent mulitple times. (#1372) Before the change: both - (void)setShouldInvertStrongReference:(BOOL)shouldInvertStrongReference and - (void)setNode:(ASDisplayNode *)node will call through [self setupReferencesWithNode:node]; which call the [node addInterfaceStateDelegate:self]; --- Source/ASNodeController+Beta.mm | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Source/ASNodeController+Beta.mm b/Source/ASNodeController+Beta.mm index 83f438239..3b3a12629 100644 --- a/Source/ASNodeController+Beta.mm +++ b/Source/ASNodeController+Beta.mm @@ -50,13 +50,16 @@ - (void)setupReferencesWithNode:(ASDisplayNode *)node } [node __setNodeController:self]; - [node addInterfaceStateDelegate:self]; } - (void)setNode:(ASDisplayNode *)node { ASLockScopeSelf(); + if (node == _node) { + return; + } [self setupReferencesWithNode:node]; + [node addInterfaceStateDelegate:self]; } - (void)setShouldInvertStrongReference:(BOOL)shouldInvertStrongReference From 17f9b00ddcefab59e97ac6762b2bebb12765b36a Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Fri, 8 Mar 2019 08:32:57 -0800 Subject: [PATCH 040/122] Add Yoga support to ASButtonNode (#1381) * Add Yoga support to ASButtonNode * Drop unowned ASLayoutElementStyle parameter * Fix access of Yoga properties * Move ASButtonNode Yoga logic to Category * Update header --- AsyncDisplayKit.xcodeproj/project.pbxproj | 12 ++ Source/ASButtonNode+Private.h | 44 +++++++ Source/ASButtonNode+Yoga.h | 20 ++++ Source/ASButtonNode+Yoga.mm | 106 +++++++++++++++++ Source/ASButtonNode.mm | 134 ++++++++++------------ 5 files changed, 245 insertions(+), 71 deletions(-) create mode 100644 Source/ASButtonNode+Private.h create mode 100644 Source/ASButtonNode+Yoga.h create mode 100644 Source/ASButtonNode+Yoga.mm diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 84d4290de..c3617ea67 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -207,6 +207,9 @@ 9C8898BD1C738BB800D6B02E /* ASTextKitFontSizeAdjuster.h in Headers */ = {isa = PBXBuildFile; fileRef = A32FEDD31C501B6A004F642A /* ASTextKitFontSizeAdjuster.h */; settings = {ATTRIBUTES = (Private, ); }; }; 9CC606651D24DF9E006581A0 /* NSIndexSet+ASHelpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC4981BB1D1C7F65004E13CC /* NSIndexSet+ASHelpers.mm */; }; 9CDC18CD1B910E12004965E2 /* ASLayoutElementPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 9CDC18CB1B910E12004965E2 /* ASLayoutElementPrivate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9D302F9B2231B07E005739C3 /* ASButtonNode+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 9D302F9A2231B07E005739C3 /* ASButtonNode+Private.h */; }; + 9D302F9E2231B373005739C3 /* ASButtonNode+Yoga.h in Headers */ = {isa = PBXBuildFile; fileRef = 9D302F9C2231B373005739C3 /* ASButtonNode+Yoga.h */; }; + 9D302F9F2231B373005739C3 /* ASButtonNode+Yoga.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9D302F9D2231B373005739C3 /* ASButtonNode+Yoga.mm */; }; 9D9AA56921E23EE200172C09 /* ASDisplayNode+LayoutSpec.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AA56721E23EE200172C09 /* ASDisplayNode+LayoutSpec.mm */; }; 9D9AA56B21E254B800172C09 /* ASDisplayNode+Yoga.h in Headers */ = {isa = PBXBuildFile; fileRef = 9D9AA56A21E254B800172C09 /* ASDisplayNode+Yoga.h */; }; 9D9AA56D21E2568500172C09 /* ASDisplayNode+LayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = 9D9AA56C21E2568500172C09 /* ASDisplayNode+LayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -777,6 +780,9 @@ 9CDC18CB1B910E12004965E2 /* ASLayoutElementPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutElementPrivate.h; sourceTree = ""; }; 9CFFC6BF1CCAC73C006A6476 /* ASViewController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASViewController.mm; sourceTree = ""; }; 9CFFC6C11CCAC768006A6476 /* ASTableNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTableNode.mm; sourceTree = ""; }; + 9D302F9A2231B07E005739C3 /* ASButtonNode+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ASButtonNode+Private.h"; sourceTree = ""; }; + 9D302F9C2231B373005739C3 /* ASButtonNode+Yoga.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ASButtonNode+Yoga.h"; sourceTree = ""; }; + 9D302F9D2231B373005739C3 /* ASButtonNode+Yoga.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASButtonNode+Yoga.mm"; sourceTree = ""; }; 9D9AA56721E23EE200172C09 /* ASDisplayNode+LayoutSpec.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASDisplayNode+LayoutSpec.mm"; sourceTree = ""; }; 9D9AA56A21E254B800172C09 /* ASDisplayNode+Yoga.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ASDisplayNode+Yoga.h"; sourceTree = ""; }; 9D9AA56C21E2568500172C09 /* ASDisplayNode+LayoutSpec.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ASDisplayNode+LayoutSpec.h"; sourceTree = ""; }; @@ -1282,6 +1288,9 @@ 8021EC1B1D2B00B100799119 /* UIImage+ASConvenience.mm */, CC55A70B1E529FA200594372 /* UIResponder+AsyncDisplayKit.h */, CC55A70C1E529FA200594372 /* UIResponder+AsyncDisplayKit.mm */, + 9D302F9A2231B07E005739C3 /* ASButtonNode+Private.h */, + 9D302F9C2231B373005739C3 /* ASButtonNode+Yoga.h */, + 9D302F9D2231B373005739C3 /* ASButtonNode+Yoga.mm */, ); path = Source; sourceTree = ""; @@ -1926,6 +1935,7 @@ CCA282B81E9EA8E40037E8B7 /* AsyncDisplayKit+Tips.h in Headers */, B35062571B010F070018CF92 /* ASAssert.h in Headers */, CCBBBF5D1EB161760069AA91 /* ASRangeManagingNode.h in Headers */, + 9D302F9B2231B07E005739C3 /* ASButtonNode+Private.h in Headers */, B35062581B010F070018CF92 /* ASAvailability.h in Headers */, 9019FBBF1ED8061D00C45F72 /* ASYogaUtilities.h in Headers */, DE84918D1C8FFF2B003D89E9 /* ASRunLoopQueue.h in Headers */, @@ -1979,6 +1989,7 @@ 34EFC7691B701CE100AD841F /* ASLayoutElement.h in Headers */, 698DFF471E36B7E9002891F1 /* ASLayoutSpecUtilities.h in Headers */, 9C70F20D1CDBE9CB007D6C76 /* ASDefaultPlayButton.h in Headers */, + 9D302F9E2231B373005739C3 /* ASButtonNode+Yoga.h in Headers */, CCCCCCD51EC3EF060087FE10 /* ASTextDebugOption.h in Headers */, CC034A091E60BEB400626263 /* ASDisplayNode+Convenience.h in Headers */, 254C6B7E1BF94DF4003EC431 /* ASTextKitTailTruncater.h in Headers */, @@ -2433,6 +2444,7 @@ CCB1F95A1EFB60A5009C7475 /* ASLog.mm in Sources */, 767E7F8E1C90191D0066C000 /* AsyncDisplayKit+Debug.mm in Sources */, CCEDDDCB200C2AC300FFCD0A /* ASConfigurationInternal.mm in Sources */, + 9D302F9F2231B373005739C3 /* ASButtonNode+Yoga.mm in Sources */, CCCCCCD61EC3EF060087FE10 /* ASTextDebugOption.mm in Sources */, 34EFC75C1B701BD200AD841F /* ASDimension.mm in Sources */, B350624E1B010EFD0018CF92 /* ASDisplayNode+AsyncDisplay.mm in Sources */, diff --git a/Source/ASButtonNode+Private.h b/Source/ASButtonNode+Private.h new file mode 100644 index 000000000..2ecc89446 --- /dev/null +++ b/Source/ASButtonNode+Private.h @@ -0,0 +1,44 @@ +// +// ASButtonNode+Private.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import + +@interface ASButtonNode() { + NSAttributedString *_normalAttributedTitle; + NSAttributedString *_highlightedAttributedTitle; + NSAttributedString *_selectedAttributedTitle; + NSAttributedString *_selectedHighlightedAttributedTitle; + NSAttributedString *_disabledAttributedTitle; + + UIImage *_normalImage; + UIImage *_highlightedImage; + UIImage *_selectedImage; + UIImage *_selectedHighlightedImage; + UIImage *_disabledImage; + + UIImage *_normalBackgroundImage; + UIImage *_highlightedBackgroundImage; + UIImage *_selectedBackgroundImage; + UIImage *_selectedHighlightedBackgroundImage; + UIImage *_disabledBackgroundImage; + + CGFloat _contentSpacing; + BOOL _laysOutHorizontally; + ASVerticalAlignment _contentVerticalAlignment; + ASHorizontalAlignment _contentHorizontalAlignment; + UIEdgeInsets _contentEdgeInsets; + ASButtonNodeImageAlignment _imageAlignment; + ASTextNode *_titleNode; + ASImageNode *_imageNode; + ASImageNode *_backgroundImageNode; +} + +@end diff --git a/Source/ASButtonNode+Yoga.h b/Source/ASButtonNode+Yoga.h new file mode 100644 index 000000000..4fddc9df3 --- /dev/null +++ b/Source/ASButtonNode+Yoga.h @@ -0,0 +1,20 @@ +// +// ASButtonNode+Yoga.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ASButtonNode (Yoga) + +- (void)updateYogaLayoutIfNeeded; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/ASButtonNode+Yoga.mm b/Source/ASButtonNode+Yoga.mm new file mode 100644 index 000000000..466647556 --- /dev/null +++ b/Source/ASButtonNode+Yoga.mm @@ -0,0 +1,106 @@ +// +// ASButtonNode+Yoga.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "ASButtonNode+Yoga.h" +#import +#import +#import +#import + +#if YOGA +static void ASButtonNodeResolveHorizontalAlignmentForStyle(ASLayoutElementStyle *style, ASStackLayoutDirection _direction, ASHorizontalAlignment _horizontalAlignment, ASStackLayoutJustifyContent _justifyContent, ASStackLayoutAlignItems _alignItems) { + if (_direction == ASStackLayoutDirectionHorizontal) { + style.justifyContent = justifyContent(_horizontalAlignment, _justifyContent); + } else { + style.alignItems = alignment(_horizontalAlignment, _alignItems); + } +} + +static void ASButtonNodeResolveVerticalAlignmentForStyle(ASLayoutElementStyle *style, ASStackLayoutDirection _direction, ASVerticalAlignment _verticalAlignment, ASStackLayoutJustifyContent _justifyContent, ASStackLayoutAlignItems _alignItems) { + if (_direction == ASStackLayoutDirectionHorizontal) { + style.alignItems = alignment(_verticalAlignment, _alignItems); + } else { + style.justifyContent = justifyContent(_verticalAlignment, _justifyContent); + } +} + +@implementation ASButtonNode (Yoga) + +- (void)updateYogaLayoutIfNeeded +{ + NSMutableArray *children = [[NSMutableArray alloc] initWithCapacity:2]; + { + ASLockScopeSelf(); + + // Build up yoga children for button node again + unowned ASLayoutElementStyle *style = [self _locked_style]; + [style yogaNodeCreateIfNeeded]; + + // Setup stack layout values + style.flexDirection = _laysOutHorizontally ? ASStackLayoutDirectionHorizontal : ASStackLayoutDirectionVertical; + + // Resolve horizontal and vertical alignment + ASButtonNodeResolveHorizontalAlignmentForStyle(style, style.flexDirection, _contentHorizontalAlignment, style.justifyContent, style.alignItems); + ASButtonNodeResolveVerticalAlignmentForStyle(style, style.flexDirection, _contentVerticalAlignment, style.justifyContent, style.alignItems); + + // Setup new yoga children + if (_imageNode.image != nil) { + [_imageNode.style yogaNodeCreateIfNeeded]; + [children addObject:_imageNode]; + } + + if (_titleNode.attributedText.length > 0) { + [_titleNode.style yogaNodeCreateIfNeeded]; + if (_imageAlignment == ASButtonNodeImageAlignmentBeginning) { + [children addObject:_titleNode]; + } else { + [children insertObject:_titleNode atIndex:0]; + } + } + + // Add spacing between title and button + if (children.count == 2) { + unowned ASLayoutElementStyle *firstChildStyle = children.firstObject.style; + if (_laysOutHorizontally) { + firstChildStyle.margin = ASEdgeInsetsMake(UIEdgeInsetsMake(0, 0, 0, _contentSpacing)); + } else { + firstChildStyle.margin = ASEdgeInsetsMake(UIEdgeInsetsMake(0, 0, _contentSpacing, 0)); + } + } + + // Add padding to button + if (UIEdgeInsetsEqualToEdgeInsets(UIEdgeInsetsZero, _contentEdgeInsets) == NO) { + style.padding = ASEdgeInsetsMake(_contentEdgeInsets); + } + + // Add background node + if (_backgroundImageNode.image) { + [_backgroundImageNode.style yogaNodeCreateIfNeeded]; + [children insertObject:_backgroundImageNode atIndex:0]; + + _backgroundImageNode.style.positionType = YGPositionTypeAbsolute; + _backgroundImageNode.style.position = ASEdgeInsetsMake(UIEdgeInsetsZero); + } + } + + // Update new children + [self setYogaChildren:children]; +} + +@end + +#else + +@implementation ASButtonNode (Yoga) + +- (void)updateYogaLayoutIfNeeded {} + +@end + +#endif diff --git a/Source/ASButtonNode.mm b/Source/ASButtonNode.mm index 5ec1f2371..f91b16c2b 100644 --- a/Source/ASButtonNode.mm +++ b/Source/ASButtonNode.mm @@ -7,40 +7,16 @@ // Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 // -#import +#import +#import #import #import #import #import #import #import -#import -#import #import -@interface ASButtonNode () -{ - NSAttributedString *_normalAttributedTitle; - NSAttributedString *_highlightedAttributedTitle; - NSAttributedString *_selectedAttributedTitle; - NSAttributedString *_selectedHighlightedAttributedTitle; - NSAttributedString *_disabledAttributedTitle; - - UIImage *_normalImage; - UIImage *_highlightedImage; - UIImage *_selectedImage; - UIImage *_selectedHighlightedImage; - UIImage *_disabledImage; - - UIImage *_normalBackgroundImage; - UIImage *_highlightedBackgroundImage; - UIImage *_selectedBackgroundImage; - UIImage *_selectedHighlightedBackgroundImage; - UIImage *_disabledBackgroundImage; -} - -@end - @implementation ASButtonNode @synthesize contentSpacing = _contentSpacing; @@ -53,6 +29,8 @@ @implementation ASButtonNode @synthesize imageNode = _imageNode; @synthesize backgroundImageNode = _backgroundImageNode; +#pragma mark - Lifecycle + - (instancetype)init { if (self = [super init]) { @@ -65,6 +43,8 @@ - (instancetype)init _contentEdgeInsets = UIEdgeInsetsZero; _imageAlignment = ASButtonNodeImageAlignmentBeginning; self.accessibilityTraits = self.defaultAccessibilityTraits; + + [self updateYogaLayoutIfNeeded]; } return self; } @@ -84,6 +64,8 @@ - (ASTextNode *)titleNode return _titleNode; } +#pragma mark - Public Getter + - (ASImageNode *)imageNode { ASLockScopeSelf(); @@ -172,6 +154,7 @@ - (void)updateImage _imageNode.image = newImage; [self unlock]; + [self updateYogaLayoutIfNeeded]; [self setNeedsLayout]; return; } @@ -202,6 +185,7 @@ - (void)updateTitle [self unlock]; self.accessibilityLabel = self.defaultAccessibilityLabel; + [self updateYogaLayoutIfNeeded]; [self setNeedsLayout]; return; } @@ -229,7 +213,8 @@ - (void)updateBackgroundImage if ((_backgroundImageNode != nil || newImage != nil) && newImage != self.backgroundImageNode.image) { _backgroundImageNode.image = newImage; [self unlock]; - + + [self updateYogaLayoutIfNeeded]; [self setNeedsLayout]; return; } @@ -246,6 +231,7 @@ - (CGFloat)contentSpacing - (void)setContentSpacing:(CGFloat)contentSpacing { if (ASLockedSelfCompareAssign(_contentSpacing, contentSpacing)) { + [self updateYogaLayoutIfNeeded]; [self setNeedsLayout]; } } @@ -259,6 +245,7 @@ - (BOOL)laysOutHorizontally - (void)setLaysOutHorizontally:(BOOL)laysOutHorizontally { if (ASLockedSelfCompareAssign(_laysOutHorizontally, laysOutHorizontally)) { + [self updateYogaLayoutIfNeeded]; [self setNeedsLayout]; } } @@ -496,50 +483,6 @@ - (void)setBackgroundImage:(UIImage *)image forState:(UIControlState)state [self updateBackgroundImage]; } -- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize -{ - UIEdgeInsets contentEdgeInsets; - ASButtonNodeImageAlignment imageAlignment; - ASLayoutSpec *spec; - ASStackLayoutSpec *stack = [[ASStackLayoutSpec alloc] init]; - { - ASLockScopeSelf(); - stack.direction = _laysOutHorizontally ? ASStackLayoutDirectionHorizontal : ASStackLayoutDirectionVertical; - stack.spacing = _contentSpacing; - stack.horizontalAlignment = _contentHorizontalAlignment; - stack.verticalAlignment = _contentVerticalAlignment; - - contentEdgeInsets = _contentEdgeInsets; - imageAlignment = _imageAlignment; - } - - NSMutableArray *children = [[NSMutableArray alloc] initWithCapacity:2]; - if (_imageNode.image) { - [children addObject:_imageNode]; - } - - if (_titleNode.attributedText.length > 0) { - if (imageAlignment == ASButtonNodeImageAlignmentBeginning) { - [children addObject:_titleNode]; - } else { - [children insertObject:_titleNode atIndex:0]; - } - } - - stack.children = children; - - spec = stack; - - if (UIEdgeInsetsEqualToEdgeInsets(UIEdgeInsetsZero, contentEdgeInsets) == NO) { - spec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:contentEdgeInsets child:spec]; - } - - if (_backgroundImageNode.image) { - spec = [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild:spec background:_backgroundImageNode]; - } - - return spec; -} - (NSString *)defaultAccessibilityLabel { @@ -553,6 +496,55 @@ - (UIAccessibilityTraits)defaultAccessibilityTraits : (UIAccessibilityTraitButton | UIAccessibilityTraitNotEnabled); } +#pragma mark - Layout + +#if !YOGA +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + UIEdgeInsets contentEdgeInsets; + ASButtonNodeImageAlignment imageAlignment; + ASLayoutSpec *spec; + ASStackLayoutSpec *stack = [[ASStackLayoutSpec alloc] init]; + { + ASLockScopeSelf(); + stack.direction = _laysOutHorizontally ? ASStackLayoutDirectionHorizontal : ASStackLayoutDirectionVertical; + stack.spacing = _contentSpacing; + stack.horizontalAlignment = _contentHorizontalAlignment; + stack.verticalAlignment = _contentVerticalAlignment; + + contentEdgeInsets = _contentEdgeInsets; + imageAlignment = _imageAlignment; + } + + NSMutableArray *children = [[NSMutableArray alloc] initWithCapacity:2]; + if (_imageNode.image) { + [children addObject:_imageNode]; + } + + if (_titleNode.attributedText.length > 0) { + if (imageAlignment == ASButtonNodeImageAlignmentBeginning) { + [children addObject:_titleNode]; + } else { + [children insertObject:_titleNode atIndex:0]; + } + } + + stack.children = children; + + spec = stack; + + if (UIEdgeInsetsEqualToEdgeInsets(UIEdgeInsetsZero, contentEdgeInsets) == NO) { + spec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:contentEdgeInsets child:spec]; + } + + if (_backgroundImageNode.image) { + spec = [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild:spec background:_backgroundImageNode]; + } + + return spec; +} +#endif + - (void)layout { [super layout]; From f17b9e2384d890ac636e33f5873d989221fc995e Mon Sep 17 00:00:00 2001 From: Greg Bolsinga Date: Fri, 8 Mar 2019 09:40:51 -0800 Subject: [PATCH 041/122] Fix CTLineRef leaks (#1386) Found by Clang Static Analyzer: ~/Texture/Source/ASTextNode2.mm:663:39: warning: Potential leak of an object stored into 'truncationTokenLine' CTLineRef truncationTokenLine = CTLineCreateWithAttributedString((CFAttributedStringRef)_truncationAttributedText); ^ ~/Texture/Source/ASTextNode2.mm:666:49: warning: Potential leak of an object stored into 'additionalTruncationTokenLine' CTLineRef additionalTruncationTokenLine = CTLineCreateWithAttributedString((CFAttributedStringRef)_additionalTruncationMessage); ^ 2 warnings generated. --- Source/ASTextNode2.mm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Source/ASTextNode2.mm b/Source/ASTextNode2.mm index 78a667e04..bd8d31329 100644 --- a/Source/ASTextNode2.mm +++ b/Source/ASTextNode2.mm @@ -662,10 +662,12 @@ - (BOOL)_locked_pointInsideAdditionalTruncationMessage:(CGPoint)point withLayout CTLineRef truncationTokenLine = CTLineCreateWithAttributedString((CFAttributedStringRef)_truncationAttributedText); CFIndex truncationTokenLineGlyphCount = truncationTokenLine ? CTLineGetGlyphCount(truncationTokenLine) : 0; + CFRelease(truncationTokenLine); CTLineRef additionalTruncationTokenLine = CTLineCreateWithAttributedString((CFAttributedStringRef)_additionalTruncationMessage); CFIndex additionalTruncationTokenLineGlyphCount = additionalTruncationTokenLine ? CTLineGetGlyphCount(additionalTruncationTokenLine) : 0; - + CFRelease(additionalTruncationTokenLine); + switch (_textContainer.truncationType) { case ASTextTruncationTypeStart: { CFIndex composedTruncationTextLineGlyphCount = truncationTokenLineGlyphCount + additionalTruncationTokenLineGlyphCount; From 932289f786ca500d7c62ff744560365d2ad524e6 Mon Sep 17 00:00:00 2001 From: Kevin Date: Fri, 8 Mar 2019 09:41:25 -0800 Subject: [PATCH 042/122] Correct attributes lookup for supplementary elements. (#1318) --- Source/ASCollectionView.mm | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Source/ASCollectionView.mm b/Source/ASCollectionView.mm index 21ff2a4a9..d53768d36 100644 --- a/Source/ASCollectionView.mm +++ b/Source/ASCollectionView.mm @@ -2017,7 +2017,13 @@ - (BOOL)dataController:(ASDataController *)dataController presentedSizeForElemen ASDisplayNodeFailAssert(@"Data controller should not ask for presented size for element that is not presented."); return YES; } - UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:indexPath]; + + UICollectionViewLayoutAttributes *attributes; + if (element.supplementaryElementKind == nil) { + attributes = [self layoutAttributesForItemAtIndexPath:indexPath]; + } else { + attributes = [self layoutAttributesForSupplementaryElementOfKind:element.supplementaryElementKind atIndexPath:indexPath]; + } return CGSizeEqualToSizeWithIn(attributes.size, size, FLT_EPSILON); } From b20e007c0d63048d365537cf44022c53b4dedaff Mon Sep 17 00:00:00 2001 From: Kevin Date: Sat, 9 Mar 2019 07:55:35 -0800 Subject: [PATCH 043/122] Need some more locks for working with calculated yoga layouts (#1388) --- Source/ASDisplayNode+Yoga.mm | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/ASDisplayNode+Yoga.mm b/Source/ASDisplayNode+Yoga.mm index 038459fa4..c5bbc6827 100644 --- a/Source/ASDisplayNode+Yoga.mm +++ b/Source/ASDisplayNode+Yoga.mm @@ -286,6 +286,7 @@ - (void)updateYogaMeasureFuncIfNeeded - (void)invalidateCalculatedYogaLayout { + ASLockScopeSelf(); YGNodeRef yogaNode = self.style.yogaNode; if (yogaNode && [self shouldHaveYogaMeasureFunc]) { // Yoga internally asserts that MarkDirty() may only be called on nodes with a measurement function. @@ -338,6 +339,7 @@ - (ASLayout *)calculateLayoutYoga:(ASSizeRange)constrainedSize - (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize { + ASScopedLockSet lockSet = [self lockToRootIfNeededForLayout]; ASDisplayNode *yogaRoot = self.yogaRoot; if (self != yogaRoot) { @@ -358,7 +360,6 @@ - (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize } }]; - // Prepare all children for the layout pass with the current Yoga tree configuration. ASDisplayNodePerformBlockOnEveryYogaChild(self, ^(ASDisplayNode *_Nonnull node) { node.yogaLayoutInProgress = YES; From 19e7bc3bc8bab352950c27dee3efc24e83d48baf Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Sat, 9 Mar 2019 07:55:46 -0800 Subject: [PATCH 044/122] Adds support for using UIGraphicsImageRenderer in ASTextNode. (#1384) * Adds support for using UIGraphicsImageRenderer in ASTextNode. In many cases this reduces the backing store of text nodes by 1/2. * Guard for UIGraphicsRenderer availability. * Comma --- Schemas/configuration.json | 3 +- Source/ASExperimentalFeatures.h | 1 + Source/ASExperimentalFeatures.mm | 4 +- Source/ASTextNode.mm | 87 +++++++++++++++++++++++++------- Source/Details/_ASDisplayLayer.h | 2 +- Tests/ASConfigurationTests.mm | 6 ++- 6 files changed, 80 insertions(+), 23 deletions(-) diff --git a/Schemas/configuration.json b/Schemas/configuration.json index 64a4c790b..3b90c1ed8 100644 --- a/Schemas/configuration.json +++ b/Schemas/configuration.json @@ -26,7 +26,8 @@ "exp_skip_a11y_wait", "exp_new_default_cell_layout_mode", "exp_dispatch_apply", - "exp_image_downloader_priority" + "exp_image_downloader_priority", + "exp_text_drawing" ] } } diff --git a/Source/ASExperimentalFeatures.h b/Source/ASExperimentalFeatures.h index f3e6b295b..b15845b4e 100644 --- a/Source/ASExperimentalFeatures.h +++ b/Source/ASExperimentalFeatures.h @@ -32,6 +32,7 @@ typedef NS_OPTIONS(NSUInteger, ASExperimentalFeatures) { ASExperimentalNewDefaultCellLayoutMode = 1 << 11, // exp_new_default_cell_layout_mode ASExperimentalDispatchApply = 1 << 12, // exp_dispatch_apply ASExperimentalImageDownloaderPriority = 1 << 13, // exp_image_downloader_priority + ASExperimentalTextDrawing = 1 << 14, // exp_text_drawing ASExperimentalFeatureAll = 0xFFFFFFFF }; diff --git a/Source/ASExperimentalFeatures.mm b/Source/ASExperimentalFeatures.mm index d1f9a7966..89a876c57 100644 --- a/Source/ASExperimentalFeatures.mm +++ b/Source/ASExperimentalFeatures.mm @@ -25,8 +25,8 @@ @"exp_skip_a11y_wait", @"exp_new_default_cell_layout_mode", @"exp_dispatch_apply", - @"exp_image_downloader_priority"])); - + @"exp_image_downloader_priority", + @"exp_text_drawing"])); if (flags == ASExperimentalFeatureAll) { return allNames; } diff --git a/Source/ASTextNode.mm b/Source/ASTextNode.mm index 7ce9c8322..e872a0929 100644 --- a/Source/ASTextNode.mm +++ b/Source/ASTextNode.mm @@ -18,6 +18,7 @@ #import #import +#import #import #import #import @@ -140,6 +141,9 @@ @interface ASTextNodeDrawParameter : NSObject { ASTextKitAttributes _rendererAttributes; UIColor *_backgroundColor; UIEdgeInsets _textContainerInsets; + CGFloat _contentScale; + BOOL _opaque; + CGRect _bounds; } @end @@ -148,12 +152,18 @@ @implementation ASTextNodeDrawParameter - (instancetype)initWithRendererAttributes:(ASTextKitAttributes)rendererAttributes backgroundColor:(/*nullable*/ UIColor *)backgroundColor textContainerInsets:(UIEdgeInsets)textContainerInsets + contentScale:(CGFloat)contentScale + opaque:(BOOL)opaque + bounds:(CGRect)bounds { self = [super init]; if (self != nil) { _rendererAttributes = rendererAttributes; _backgroundColor = backgroundColor; _textContainerInsets = textContainerInsets; + _contentScale = contentScale; + _opaque = opaque; + _bounds = bounds; } return self; } @@ -526,31 +536,74 @@ - (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer return [[ASTextNodeDrawParameter alloc] initWithRendererAttributes:[self _locked_rendererAttributes] backgroundColor:self.backgroundColor - textContainerInsets:_textContainerInset]; + textContainerInsets:_textContainerInset + contentScale:_contentsScaleForDisplay + opaque:self.isOpaque + bounds:[self threadSafeBounds]]; } -+ (void)drawRect:(CGRect)bounds withParameters:(id)parameters isCancelled:(NS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing ++ (UIImage *)displayWithParameters:(id)parameters isCancelled:(NS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelled { ASTextNodeDrawParameter *drawParameter = (ASTextNodeDrawParameter *)parameters; - UIColor *backgroundColor = (isRasterizing || drawParameter == nil) ? nil : drawParameter->_backgroundColor; - UIEdgeInsets textContainerInsets = drawParameter ? drawParameter->_textContainerInsets : UIEdgeInsetsZero; - ASTextKitRenderer *renderer = [drawParameter rendererForBounds:bounds]; - CGContextRef context = UIGraphicsGetCurrentContext(); - ASDisplayNodeAssert(context, @"This is no good without a context."); - - CGContextSaveGState(context); - CGContextTranslateCTM(context, textContainerInsets.left, textContainerInsets.top); + if (drawParameter->_bounds.size.width <= 0 || drawParameter->_bounds.size.height <= 0) { + return nil; + } + + UIImage *result = nil; + UIColor *backgroundColor = drawParameter->_backgroundColor; + UIEdgeInsets textContainerInsets = drawParameter ? drawParameter->_textContainerInsets : UIEdgeInsetsZero; + ASTextKitRenderer *renderer = [drawParameter rendererForBounds:drawParameter->_bounds]; + BOOL renderedWithGraphicsRenderer = NO; - // Fill background - if (backgroundColor != nil) { - [backgroundColor setFill]; - UIRectFillUsingBlendMode(CGContextGetClipBoundingBox(context), kCGBlendModeCopy); + if (AS_AVAILABLE_IOS_TVOS(10, 10)) { + if (ASActivateExperimentalFeature(ASExperimentalTextDrawing)) { + renderedWithGraphicsRenderer = YES; + UIGraphicsImageRenderer *graphicsRenderer = [[UIGraphicsImageRenderer alloc] initWithSize:CGSizeMake(drawParameter->_bounds.size.width, drawParameter->_bounds.size.height)]; + result = [graphicsRenderer imageWithActions:^(UIGraphicsImageRendererContext * _Nonnull rendererContext) { + CGContextRef context = rendererContext.CGContext; + ASDisplayNodeAssert(context, @"This is no good without a context."); + + CGContextSaveGState(context); + CGContextTranslateCTM(context, textContainerInsets.left, textContainerInsets.top); + + // Fill background + if (backgroundColor != nil) { + [backgroundColor setFill]; + UIRectFillUsingBlendMode(CGContextGetClipBoundingBox(context), kCGBlendModeCopy); + } + + // Draw text + [renderer drawInContext:context bounds:drawParameter->_bounds]; + CGContextRestoreGState(context); + }]; + } } - // Draw text - [renderer drawInContext:context bounds:bounds]; - CGContextRestoreGState(context); + if (!renderedWithGraphicsRenderer) { + UIGraphicsBeginImageContextWithOptions(CGSizeMake(drawParameter->_bounds.size.width, drawParameter->_bounds.size.height), drawParameter->_opaque, drawParameter->_contentScale); + + CGContextRef context = UIGraphicsGetCurrentContext(); + ASDisplayNodeAssert(context, @"This is no good without a context."); + + CGContextSaveGState(context); + CGContextTranslateCTM(context, textContainerInsets.left, textContainerInsets.top); + + // Fill background + if (backgroundColor != nil) { + [backgroundColor setFill]; + UIRectFillUsingBlendMode(CGContextGetClipBoundingBox(context), kCGBlendModeCopy); + } + + // Draw text + [renderer drawInContext:context bounds:drawParameter->_bounds]; + CGContextRestoreGState(context); + + result = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + } + + return result; } #pragma mark - Attributes diff --git a/Source/Details/_ASDisplayLayer.h b/Source/Details/_ASDisplayLayer.h index e69ab71fd..1066a36f1 100644 --- a/Source/Details/_ASDisplayLayer.h +++ b/Source/Details/_ASDisplayLayer.h @@ -112,7 +112,7 @@ NS_ASSUME_NONNULL_BEGIN @summary Delegate override to provide new layer contents as a UIImage. @param parameters An object describing all of the properties you need to draw. Return this from -drawParametersForAsyncLayer: @param isCancelledBlock Execute this block to check whether the current drawing operation has been cancelled to avoid unnecessary work. A return value of YES means cancel drawing and return. - @return A UIImage with contents that are ready to display on the main thread. Make sure that the image is already decoded before returning it here. + @return A UIImage (backed by a CGImage) with contents that are ready to display on the main thread. Make sure that the image is already decoded before returning it here. */ + (UIImage *)displayWithParameters:(nullable id)parameters isCancelled:(AS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelledBlock; diff --git a/Tests/ASConfigurationTests.mm b/Tests/ASConfigurationTests.mm index a5f37c11c..1a4553200 100644 --- a/Tests/ASConfigurationTests.mm +++ b/Tests/ASConfigurationTests.mm @@ -31,7 +31,8 @@ ASExperimentalSkipAccessibilityWait, ASExperimentalNewDefaultCellLayoutMode, ASExperimentalDispatchApply, - ASExperimentalImageDownloaderPriority + ASExperimentalImageDownloaderPriority, + ASExperimentalTextDrawing }; @interface ASConfigurationTests : ASTestCase @@ -57,7 +58,8 @@ + (NSArray *)names { @"exp_skip_a11y_wait", @"exp_new_default_cell_layout_mode", @"exp_dispatch_apply", - @"exp_image_downloader_priority" + @"exp_image_downloader_priority", + @"exp_text_drawing" ]; } From 36d14ac0bf6165e0c1787cf09bd84defd296ed23 Mon Sep 17 00:00:00 2001 From: Jacob Farkas Date: Sat, 9 Mar 2019 07:56:10 -0800 Subject: [PATCH 045/122] Add UIDataSourceModelAssociation to ASTableView and ASCollectionView (#1354) * Add UIDataSourceModelAssociation protocol conformance to ASTableView and ASCollectionView. * Implementing review feedback from @Adlai-Holler --- Source/ASCollectionNode.h | 23 ++++++++++++++ Source/ASCollectionView.mm | 27 +++++++++++++++++ Source/ASTableNode.h | 23 ++++++++++++++ Source/ASTableView.mm | 25 ++++++++++++++++ Source/Details/ASDelegateProxy.mm | 50 +++++++++++++++++++++++++++++-- 5 files changed, 146 insertions(+), 2 deletions(-) diff --git a/Source/ASCollectionNode.h b/Source/ASCollectionNode.h index 4615ecb3a..15a3c52a6 100644 --- a/Source/ASCollectionNode.h +++ b/Source/ASCollectionNode.h @@ -672,6 +672,29 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)collectionNode:(ASCollectionNode *)collectionNode moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath; +/** + * Generate a unique identifier for an element in a collection. This helps state restoration persist the scroll position + * of a collection view even when the data in that table changes. See the documentation for UIDataSourceModelAssociation for more information. + * + * @param indexPath The index path of the requested node. + * + * @param collectionNode The sender. + * + * @return a unique identifier for the element at the given path. Return nil if the index path does not exist in the collection. + */ +- (nullable NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)indexPath inNode:(ASCollectionNode *)collectionNode; + +/** + * Similar to -collectionView:cellForItemAtIndexPath:. See the documentation for UIDataSourceModelAssociation for more information. + * + * @param identifier The model identifier of the element, previously generated by a call to modelIdentifierForElementAtIndexPath + * + * @param collectionNode The sender. + * + * @return the index path to the current position of the matching element in the collection. Return nil if the element is not found. + */ +- (nullable NSIndexPath *)indexPathForElementWithModelIdentifier:(NSString *)identifier inNode:(ASCollectionNode *)collectionNode; + /** * Similar to -collectionView:cellForItemAtIndexPath:. * diff --git a/Source/ASCollectionView.mm b/Source/ASCollectionView.mm index d53768d36..834699ec5 100644 --- a/Source/ASCollectionView.mm +++ b/Source/ASCollectionView.mm @@ -230,6 +230,7 @@ @interface ASCollectionView () )asyncDataSource _asyncDataSourceFlags.interopViewForSupplementaryElement = [interopDataSource respondsToSelector:@selector(collectionView:viewForSupplementaryElementOfKind:atIndexPath:)]; } + _asyncDataSourceFlags.modelIdentifierMethods = [_asyncDataSource respondsToSelector:@selector(modelIdentifierForElementAtIndexPath:inNode:)] && [_asyncDataSource respondsToSelector:@selector(indexPathForElementWithModelIdentifier:inNode:)]; + + ASDisplayNodeAssert(_asyncDataSourceFlags.collectionNodeNumberOfItemsInSection || _asyncDataSourceFlags.collectionViewNumberOfItemsInSection, @"Data source must implement collectionNode:numberOfItemsInSection:"); ASDisplayNodeAssert(_asyncDataSourceFlags.collectionNodeNodeBlockForItem || _asyncDataSourceFlags.collectionNodeNodeForItem @@ -805,6 +809,29 @@ - (void)invalidateFlowLayoutDelegateMetrics // Subclass hook } +- (nullable NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)indexPath inView:(UIView *)view { + if (_asyncDataSourceFlags.modelIdentifierMethods) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, nil); + NSIndexPath *convertedPath = [self convertIndexPathToCollectionNode:indexPath]; + if (convertedPath == nil) { + return nil; + } else { + return [_asyncDataSource modelIdentifierForElementAtIndexPath:convertedPath inNode:collectionNode]; + } + } else { + return nil; + } +} + +- (nullable NSIndexPath *)indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view { + if (_asyncDataSourceFlags.modelIdentifierMethods) { + GET_COLLECTIONNODE_OR_RETURN(collectionNode, nil); + return [_asyncDataSource indexPathForElementWithModelIdentifier:identifier inNode:collectionNode]; + } else { + return nil; + } +} + #pragma mark Internal - (void)_configureCollectionViewLayout:(nonnull UICollectionViewLayout *)layout diff --git a/Source/ASTableNode.h b/Source/ASTableNode.h index 2cd2394c8..9ab7c35b7 100644 --- a/Source/ASTableNode.h +++ b/Source/ASTableNode.h @@ -569,6 +569,29 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)tableViewUnlockDataSource:(ASTableView *)tableView ASDISPLAYNODE_DEPRECATED_MSG("Data source accesses are on the main thread. Method will not be called."); +/** + * Generate a unique identifier for an element in a table. This helps state restoration persist the scroll position + * of a table view even when the data in that table changes. See the documentation for UIDataSourceModelAssociation for more information. + * + * @param indexPath The index path of the requested node. + * + * @param tableNode The sender. + * + * @return a unique identifier for the element at the given path. Return nil if the index path does not exist in the table. + */ +- (nullable NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)indexPath inNode:(ASTableNode *)tableNode; + +/** + * Similar to -tableView:cellForRowAtIndexPath:. See the documentation for UIDataSourceModelAssociation for more information. + * + * @param identifier The model identifier of the element, previously generated by a call to modelIdentifierForElementAtIndexPath. + * + * @param tableNode The sender. + * + * @return the index path to the current position of the matching element in the table. Return nil if the element is not found. + */ +- (nullable NSIndexPath *)indexPathForElementWithModelIdentifier:(NSString *)identifier inNode:(ASTableNode *)tableNode; + @end /** diff --git a/Source/ASTableView.mm b/Source/ASTableView.mm index 3807c5761..af39ee11c 100644 --- a/Source/ASTableView.mm +++ b/Source/ASTableView.mm @@ -271,6 +271,7 @@ @interface ASTableView () )asyncDataSource _asyncDataSourceFlags.tableViewCanMoveRow = [_asyncDataSource respondsToSelector:@selector(tableView:canMoveRowAtIndexPath:)]; _asyncDataSourceFlags.tableViewMoveRow = [_asyncDataSource respondsToSelector:@selector(tableView:moveRowAtIndexPath:toIndexPath:)]; _asyncDataSourceFlags.sectionIndexMethods = [_asyncDataSource respondsToSelector:@selector(sectionIndexTitlesForTableView:)] && [_asyncDataSource respondsToSelector:@selector(tableView:sectionForSectionIndexTitle:atIndex:)]; + _asyncDataSourceFlags.modelIdentifierMethods = [_asyncDataSource respondsToSelector:@selector(modelIdentifierForElementAtIndexPath:inNode:)] && [_asyncDataSource respondsToSelector:@selector(indexPathForElementWithModelIdentifier:inNode:)]; ASDisplayNodeAssert(_asyncDataSourceFlags.tableViewNodeBlockForRow || _asyncDataSourceFlags.tableViewNodeForRow @@ -961,6 +963,29 @@ - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger return [_dataController.visibleMap numberOfItemsInSection:section]; } +- (nullable NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)indexPath inView:(UIView *)view { + if (_asyncDataSourceFlags.modelIdentifierMethods) { + GET_TABLENODE_OR_RETURN(tableNode, nil); + NSIndexPath *convertedPath = [self convertIndexPathToTableNode:indexPath]; + if (convertedPath == nil) { + return nil; + } else { + return [_asyncDataSource modelIdentifierForElementAtIndexPath:convertedPath inNode:tableNode]; + } + } else { + return nil; + } +} + +- (nullable NSIndexPath *)indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view { + if (_asyncDataSourceFlags.modelIdentifierMethods) { + GET_TABLENODE_OR_RETURN(tableNode, nil); + return [_asyncDataSource indexPathForElementWithModelIdentifier:identifier inNode:tableNode]; + } else { + return nil; + } +} + - (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath { if (_asyncDataSourceFlags.tableViewCanMoveRow) { diff --git a/Source/Details/ASDelegateProxy.mm b/Source/Details/ASDelegateProxy.mm index bc51bb3e3..40f1f306a 100644 --- a/Source/Details/ASDelegateProxy.mm +++ b/Source/Details/ASDelegateProxy.mm @@ -12,6 +12,20 @@ #import #import +// UIKit performs a class check for UIDataSourceModelAssociation protocol conformance rather than an instance check, so +// the implementation of conformsToProtocol: below never gets called. We need to declare the two as conforming to the protocol here, then +// we need to implement dummy methods to get rid of a compiler warning about not conforming to the protocol. +@interface ASTableViewProxy () +@end + +@interface ASCollectionViewProxy () +@end + +@interface ASDelegateProxy (UIDataSourceModelAssociationPrivate) +- (nullable NSString *)_modelIdentifierForElementAtIndexPath:(NSIndexPath *)indexPath inView:(UIView *)view; +- (nullable NSIndexPath *)_indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view; +@end + @implementation ASTableViewProxy - (BOOL)interceptsSelector:(SEL)selector @@ -54,10 +68,22 @@ - (BOOL)interceptsSelector:(SEL)selector // used for batch fetching API selector == @selector(scrollViewWillEndDragging:withVelocity:targetContentOffset:) || - selector == @selector(scrollViewDidEndDecelerating:) + selector == @selector(scrollViewDidEndDecelerating:) || + + // UIDataSourceModelAssociation + selector == @selector(modelIdentifierForElementAtIndexPath:inView:) || + selector == @selector(indexPathForElementWithModelIdentifier:inView:) ); } +- (nullable NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)indexPath inView:(UIView *)view { + return [self _modelIdentifierForElementAtIndexPath:indexPath inView:view]; +} + +- (nullable NSIndexPath *)indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view { + return [self _indexPathForElementWithModelIdentifier:identifier inView:view]; +} + @end @implementation ASCollectionViewProxy @@ -110,10 +136,22 @@ - (BOOL)interceptsSelector:(SEL)selector // intercepted due to not being supported by ASCollectionView (prevent bugs caused by usage) selector == @selector(collectionView:canMoveItemAtIndexPath:) || - selector == @selector(collectionView:moveItemAtIndexPath:toIndexPath:) + selector == @selector(collectionView:moveItemAtIndexPath:toIndexPath:) || + + // UIDataSourceModelAssociation + selector == @selector(modelIdentifierForElementAtIndexPath:inView:) || + selector == @selector(indexPathForElementWithModelIdentifier:inView:) ); } +- (nullable NSString *)modelIdentifierForElementAtIndexPath:(NSIndexPath *)indexPath inView:(UIView *)view { + return [self _modelIdentifierForElementAtIndexPath:indexPath inView:view]; +} + +- (nullable NSIndexPath *)indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view { + return [self _indexPathForElementWithModelIdentifier:identifier inView:view]; +} + @end @implementation ASPagerNodeProxy @@ -220,4 +258,12 @@ - (BOOL)interceptsSelector:(SEL)selector return NO; } +- (nullable NSString *)_modelIdentifierForElementAtIndexPath:(NSIndexPath *)indexPath inView:(UIView *)view { + return [(id)_interceptor modelIdentifierForElementAtIndexPath:indexPath inView:view]; +} + +- (nullable NSIndexPath *)_indexPathForElementWithModelIdentifier:(NSString *)identifier inView:(UIView *)view { + return [(id)_interceptor indexPathForElementWithModelIdentifier:identifier inView:view]; +} + @end From fbddcd477275fdf285d78c39f457526c2c5bbb4e Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Sat, 9 Mar 2019 12:14:58 -0800 Subject: [PATCH 046/122] Rename ASDN C++ namespace to AS (#1366) * Rename ASDN C++ namespace to "AS." Referring to the framework as ASDisplayNode is pretty out-dated and verbose. See CoreAnimation which usees CA for their internal namespace. More using * More cases --- Source/ASCollectionNode.mm | 2 +- Source/ASDisplayNode+Layout.mm | 46 +++--- Source/ASDisplayNode+LayoutSpec.mm | 16 +- Source/ASDisplayNode+Yoga.mm | 2 +- Source/ASDisplayNode.mm | 150 +++++++++--------- Source/ASEditableTextNode.mm | 82 +++++----- Source/ASImageNode+AnimatedImage.mm | 10 +- Source/ASImageNode.mm | 34 ++-- Source/ASMultiplexImageNode.mm | 24 +-- Source/ASNodeController+Beta.mm | 2 +- Source/ASRunLoopQueue.mm | 20 +-- Source/ASTableNode.mm | 2 +- Source/ASTextNode2.mm | 8 +- Source/ASVideoNode.mm | 2 +- Source/Details/ASBasicImageDownloader.mm | 27 ++-- Source/Details/ASCollectionLayoutState.mm | 4 +- Source/Details/ASDataController.mm | 2 +- Source/Details/ASEventLog.mm | 6 +- Source/Details/ASMainSerialQueue.mm | 8 +- Source/Details/ASThread.h | 4 +- Source/Details/ASTraitCollection.h | 2 +- Source/Layout/ASLayoutElement.mm | 16 +- Source/Layout/ASLayoutSpec.mm | 8 +- Source/Private/ASCollectionLayoutCache.mm | 12 +- Source/Private/ASDisplayNode+AsyncDisplay.mm | 9 +- Source/Private/ASDisplayNode+UIViewBridge.mm | 8 +- Source/Private/ASDisplayNodeInternal.h | 2 +- .../ASImageNode+AnimatedImagePrivate.h | 2 +- Source/Private/ASLayoutTransition.mm | 24 +-- Source/Private/ASPendingStateController.mm | 4 +- Source/Private/Layout/ASLayoutSpecPrivate.h | 2 +- Source/Private/_ASScopeTimer.h | 4 +- Source/TextKit/ASTextKitContext.mm | 12 +- Source/TextKit/ASTextKitFontSizeAdjuster.mm | 4 +- 34 files changed, 288 insertions(+), 272 deletions(-) diff --git a/Source/ASCollectionNode.mm b/Source/ASCollectionNode.mm index e79b08a56..51cb7d330 100644 --- a/Source/ASCollectionNode.mm +++ b/Source/ASCollectionNode.mm @@ -106,7 +106,7 @@ - (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeMo @interface ASCollectionNode () { - ASDN::RecursiveMutex _environmentStateLock; + AS::RecursiveMutex _environmentStateLock; Class _collectionViewClass; id _batchFetchingDelegate; } diff --git a/Source/ASDisplayNode+Layout.mm b/Source/ASDisplayNode+Layout.mm index 35b670828..8800aa6fc 100644 --- a/Source/ASDisplayNode+Layout.mm +++ b/Source/ASDisplayNode+Layout.mm @@ -21,6 +21,8 @@ #import #import +using AS::MutexLocker; + @interface ASDisplayNode (ASLayoutElementStyleDelegate) @end @@ -42,7 +44,7 @@ @implementation ASDisplayNode (ASLayoutElement) - (BOOL)implementsLayoutMethod { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return (_methodOverrides & (ASDisplayNodeMethodOverrideLayoutSpecThatFits | ASDisplayNodeMethodOverrideCalcLayoutThatFits | ASDisplayNodeMethodOverrideCalcSizeThatFits)) != 0 || _layoutSpecBlock != nil; @@ -51,7 +53,7 @@ - (BOOL)implementsLayoutMethod - (ASLayoutElementStyle *)style { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return [self _locked_style]; } @@ -165,7 +167,7 @@ @implementation ASDisplayNode (ASLayout) - (ASLayoutEngineType)layoutEngineType { #if YOGA - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); YGNodeRef yogaNode = _style.yogaNode; BOOL hasYogaParent = (_yogaParent != nil); BOOL hasYogaChildren = (_yogaChildren.count > 0); @@ -179,13 +181,13 @@ - (ASLayoutEngineType)layoutEngineType - (ASLayout *)calculatedLayout { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _calculatedDisplayNodeLayout.layout; } - (CGSize)calculatedSize { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); if (_pendingDisplayNodeLayout.isValid(_layoutVersion)) { return _pendingDisplayNodeLayout.layout.size; } @@ -194,7 +196,7 @@ - (CGSize)calculatedSize - (ASSizeRange)constrainedSizeForCalculatedLayout { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return [self _locked_constrainedSizeForCalculatedLayout]; } @@ -447,7 +449,7 @@ - (void)_u_measureNodeWithBoundsIfNecessary:(CGRect)bounds - (ASSizeRange)_constrainedSizeForLayoutPass { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return [self _locked_constrainedSizeForLayoutPass]; } @@ -487,7 +489,7 @@ - (void)_layoutSublayouts ASLayout *layout; { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); if (_calculatedDisplayNodeLayout.version < _layoutVersion) { return; } @@ -517,13 +519,13 @@ @implementation ASDisplayNode (ASAutomaticSubnodeManagement) - (BOOL)automaticallyManagesSubnodes { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _automaticallyManagesSubnodes; } - (void)setAutomaticallyManagesSubnodes:(BOOL)automaticallyManagesSubnodes { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); _automaticallyManagesSubnodes = automaticallyManagesSubnodes; } @@ -536,7 +538,7 @@ @implementation ASDisplayNode (ASLayoutTransition) - (BOOL)_isLayoutTransitionInvalid { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return [self _locked_isLayoutTransitionInvalid]; } @@ -598,7 +600,7 @@ - (void)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize } { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); // Check if we are a subnode in a layout transition. // In this case no measurement is needed as we're part of the layout transition. @@ -680,7 +682,7 @@ - (void)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize { // Grab __instanceLock__ here to make sure this transition isn't invalidated // right after it passed the validation test and before it proceeds - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); // Update calculated layout const auto previousLayout = _calculatedDisplayNodeLayout; @@ -750,37 +752,37 @@ - (void)cancelLayoutTransition - (void)setDefaultLayoutTransitionDuration:(NSTimeInterval)defaultLayoutTransitionDuration { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); _defaultLayoutTransitionDuration = defaultLayoutTransitionDuration; } - (NSTimeInterval)defaultLayoutTransitionDuration { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _defaultLayoutTransitionDuration; } - (void)setDefaultLayoutTransitionDelay:(NSTimeInterval)defaultLayoutTransitionDelay { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); _defaultLayoutTransitionDelay = defaultLayoutTransitionDelay; } - (NSTimeInterval)defaultLayoutTransitionDelay { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _defaultLayoutTransitionDelay; } - (void)setDefaultLayoutTransitionOptions:(UIViewAnimationOptions)defaultLayoutTransitionOptions { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); _defaultLayoutTransitionOptions = defaultLayoutTransitionOptions; } - (UIViewAnimationOptions)defaultLayoutTransitionOptions { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _defaultLayoutTransitionOptions; } @@ -942,7 +944,7 @@ - (void)_assertSubnodeState return; } - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); NSArray *sublayouts = _calculatedDisplayNodeLayout.layout.sublayouts; unowned ASLayout *cSublayouts[sublayouts.count]; [sublayouts getObjects:cSublayouts range:NSMakeRange(0, AS_ARRAY_SIZE(cSublayouts))]; @@ -993,7 +995,7 @@ - (void)_pendingLayoutTransitionDidComplete [self calculatedLayoutDidChange]; // Grab lock after calling out to subclass - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); // We generate placeholders at -layoutThatFits: time so that a node is guaranteed to have a placeholder ready to go. // This is also because measurement is usually asynchronous, but placeholders need to be set up synchronously. @@ -1027,7 +1029,7 @@ - (void)_pendingLayoutTransitionDidComplete - (void)_setCalculatedDisplayNodeLayout:(const ASDisplayNodeLayout &)displayNodeLayout { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); [self _locked_setCalculatedDisplayNodeLayout:displayNodeLayout]; } diff --git a/Source/ASDisplayNode+LayoutSpec.mm b/Source/ASDisplayNode+LayoutSpec.mm index b4ecb2e55..d79073ffd 100644 --- a/Source/ASDisplayNode+LayoutSpec.mm +++ b/Source/ASDisplayNode+LayoutSpec.mm @@ -26,19 +26,19 @@ - (void)setLayoutSpecBlock:(ASLayoutSpecBlock)layoutSpecBlock // For now there should never be an override of layoutSpecThatFits: and a layoutSpecBlock together. ASDisplayNodeAssert(!(_methodOverrides & ASDisplayNodeMethodOverrideLayoutSpecThatFits), @"Nodes with a .layoutSpecBlock must not also implement -layoutSpecThatFits:"); - ASDN::MutexLocker l(__instanceLock__); + AS::MutexLocker l(__instanceLock__); _layoutSpecBlock = layoutSpecBlock; } - (ASLayoutSpecBlock)layoutSpecBlock { - ASDN::MutexLocker l(__instanceLock__); + AS::MutexLocker l(__instanceLock__); return _layoutSpecBlock; } - (ASLayout *)calculateLayoutLayoutSpec:(ASSizeRange)constrainedSize { - ASDN::UniqueLock l(__instanceLock__); + AS::UniqueLock l(__instanceLock__); // Manual size calculation via calculateSizeThatFits: if (_layoutSpecBlock == NULL && (_methodOverrides & ASDisplayNodeMethodOverrideLayoutSpecThatFits) == 0) { @@ -82,7 +82,7 @@ - (ASLayout *)calculateLayoutLayoutSpec:(ASSizeRange)constrainedSize // Manually propagate the trait collection here so that any layoutSpec children of layoutSpec will get a traitCollection { - ASDN::SumScopeTimer t(_layoutSpecTotalTime, measureLayoutSpec); + AS::SumScopeTimer t(_layoutSpecTotalTime, measureLayoutSpec); ASTraitCollectionPropagateDown(layoutElement, self.primitiveTraitCollection); } @@ -93,7 +93,7 @@ - (ASLayout *)calculateLayoutLayoutSpec:(ASSizeRange)constrainedSize // Layout element layout creation ASLayout *layout = ({ - ASDN::SumScopeTimer t(_layoutComputationTotalTime, measureLayoutComputation); + AS::SumScopeTimer t(_layoutComputationTotalTime, measureLayoutComputation); [layoutElement layoutThatFits:constrainedSize]; }); ASDisplayNodeAssertNotNil(layout, @"[ASLayoutElement layoutThatFits:] should never return nil! %@, %@", self, layout); @@ -123,13 +123,13 @@ - (ASLayout *)calculateLayoutLayoutSpec:(ASSizeRange)constrainedSize if (_layoutSpecBlock != NULL) { return ({ - ASDN::MutexLocker l(__instanceLock__); - ASDN::SumScopeTimer t(_layoutSpecTotalTime, measureLayoutSpec); + AS::MutexLocker l(__instanceLock__); + AS::SumScopeTimer t(_layoutSpecTotalTime, measureLayoutSpec); _layoutSpecBlock(self, constrainedSize); }); } else { return ({ - ASDN::SumScopeTimer t(_layoutSpecTotalTime, measureLayoutSpec); + AS::SumScopeTimer t(_layoutSpecTotalTime, measureLayoutSpec); [self layoutSpecThatFits:constrainedSize]; }); } diff --git a/Source/ASDisplayNode+Yoga.mm b/Source/ASDisplayNode+Yoga.mm index c5bbc6827..c52eeb679 100644 --- a/Source/ASDisplayNode+Yoga.mm +++ b/Source/ASDisplayNode+Yoga.mm @@ -306,7 +306,7 @@ - (void)invalidateCalculatedYogaLayout - (ASLayout *)calculateLayoutYoga:(ASSizeRange)constrainedSize { - ASDN::UniqueLock l(__instanceLock__); + AS::UniqueLock l(__instanceLock__); // There are several cases where Yoga could arrive here: // - This node is not in a Yoga tree: it has neither a yogaParent nor yogaChildren. diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index 3ad86ee7a..b4034f291 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -51,7 +51,7 @@ // Conditionally time these scopes to our debug ivars (only exist in debug/profile builds) #if TIME_DISPLAYNODE_OPS - #define TIME_SCOPED(outVar) ASDN::ScopeTimer t(outVar) + #define TIME_SCOPED(outVar) AS::ScopeTimer t(outVar) #else #define TIME_SCOPED(outVar) #endif @@ -60,6 +60,8 @@ // TODO(wsdwsd0829): Rework enabling code to ensure that interface state behavior is not altered when ASCATransactionQueue is disabled. #define ENABLE_NEW_EXIT_HIERARCHY_BEHAVIOR 0 +using AS::MutexLocker; + static ASDisplayNodeNonFatalErrorBlock _nonFatalErrorBlock = nil; // Forward declare CALayerDelegate protocol as the iOS 10 SDK moves CALayerDelegate from an informal delegate to a protocol. @@ -450,7 +452,7 @@ - (ASDisplayNodeMethodOverrides)methodOverrides - (void)onDidLoad:(ASDisplayNodeDidLoadBlock)body { - ASDN::UniqueLock l(__instanceLock__); + AS::UniqueLock l(__instanceLock__); if ([self _locked_isNodeLoaded]) { ASDisplayNodeAssertThreadAffinity(self); @@ -615,7 +617,7 @@ - (BOOL)isNodeLoaded // where the state of this property can change. As an optimization, we can avoid locking. return _loaded(self); } else { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return [self _locked_isNodeLoaded]; } } @@ -630,7 +632,7 @@ - (BOOL)_locked_isNodeLoaded - (UIView *)view { - ASDN::UniqueLock l(__instanceLock__); + AS::UniqueLock l(__instanceLock__); ASDisplayNodeAssert(!_flags.layerBacked, @"Call to -view undefined on layer-backed nodes"); BOOL isLayerBacked = _flags.layerBacked; @@ -676,7 +678,7 @@ - (UIView *)view - (CALayer *)layer { - ASDN::UniqueLock l(__instanceLock__); + AS::UniqueLock l(__instanceLock__); if (_layer != nil) { return _layer; } @@ -717,7 +719,7 @@ - (CALayer *)layer // Returns nil if the layer is not an _ASDisplayLayer; will not create the layer if nil. - (_ASDisplayLayer *)asyncLayer { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return [self _locked_asyncLayer]; } @@ -737,7 +739,7 @@ - (void)setLayerBacked:(BOOL)isLayerBacked // Only call this if assertions are enabled – it could be expensive. ASDisplayNodeAssert(!isLayerBacked || self.supportsLayerBacking, @"Node %@ does not support layer backing.", self); - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); if (_flags.layerBacked == isLayerBacked) { return; } @@ -752,31 +754,31 @@ - (void)setLayerBacked:(BOOL)isLayerBacked - (BOOL)isLayerBacked { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _flags.layerBacked; } - (BOOL)supportsLayerBacking { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return !checkFlag(Synchronous) && !_flags.viewEverHadAGestureRecognizerAttached && _viewClass == [_ASDisplayView class] && _layerClass == [_ASDisplayLayer class]; } - (BOOL)shouldAnimateSizeChanges { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _flags.shouldAnimateSizeChanges; } - (void)setShouldAnimateSizeChanges:(BOOL)shouldAnimateSizeChanges { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); _flags.shouldAnimateSizeChanges = shouldAnimateSizeChanges; } - (CGRect)threadSafeBounds { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return [self _locked_threadSafeBounds]; } @@ -788,19 +790,19 @@ - (CGRect)_locked_threadSafeBounds - (void)setThreadSafeBounds:(CGRect)newBounds { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); _threadSafeBounds = newBounds; } - (void)nodeViewDidAddGestureRecognizer { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); _flags.viewEverHadAGestureRecognizerAttached = YES; } - (UIEdgeInsets)fallbackSafeAreaInsets { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _fallbackSafeAreaInsets; } @@ -810,7 +812,7 @@ - (void)setFallbackSafeAreaInsets:(UIEdgeInsets)insets BOOL updatesLayoutMargins; { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); ASDisplayNodeAssertThreadAffinity(self); if (UIEdgeInsetsEqualToEdgeInsets(insets, _fallbackSafeAreaInsets)) { @@ -861,37 +863,37 @@ - (void)_fallbackUpdateSafeAreaOnChildren - (BOOL)isViewControllerRoot { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _isViewControllerRoot; } - (void)setViewControllerRoot:(BOOL)flag { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); _isViewControllerRoot = flag; } - (BOOL)automaticallyRelayoutOnSafeAreaChanges { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _automaticallyRelayoutOnSafeAreaChanges; } - (void)setAutomaticallyRelayoutOnSafeAreaChanges:(BOOL)flag { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); _automaticallyRelayoutOnSafeAreaChanges = flag; } - (BOOL)automaticallyRelayoutOnLayoutMarginsChanges { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _automaticallyRelayoutOnLayoutMarginsChanges; } - (void)setAutomaticallyRelayoutOnLayoutMarginsChanges:(BOOL)flag { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); _automaticallyRelayoutOnLayoutMarginsChanges = flag; } @@ -1002,13 +1004,13 @@ - (BOOL)__isFirstResponder - (NSString *)debugName { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _debugName; } - (void)setDebugName:(NSString *)debugName { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); if (!ASObjectIsEqual(_debugName, debugName)) { _debugName = [debugName copy]; } @@ -1032,7 +1034,7 @@ - (void)__setNeedsLayout - (void)invalidateCalculatedLayout { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); _layoutVersion++; @@ -1050,7 +1052,7 @@ - (void)__layout BOOL loaded = NO; { - ASDN::UniqueLock l(__instanceLock__); + AS::UniqueLock l(__instanceLock__); loaded = [self _locked_isNodeLoaded]; CGRect bounds = _threadSafeBounds; @@ -1207,7 +1209,7 @@ - (void)transitionContext:(_ASTransitionContext *)context didComplete:(BOOL)didC - (BOOL)displaysAsynchronously { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return [self _locked_displaysAsynchronously]; } @@ -1224,7 +1226,7 @@ - (void)setDisplaysAsynchronously:(BOOL)displaysAsynchronously { ASDisplayNodeAssertThreadAffinity(self); - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); // Can't do this for synchronous nodes (using layers that are not _ASDisplayLayer and so we can't control display prevention/cancel) if (checkFlag(Synchronous)) { @@ -1242,13 +1244,13 @@ - (void)setDisplaysAsynchronously:(BOOL)displaysAsynchronously - (BOOL)rasterizesSubtree { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _flags.rasterizesSubtree; } - (void)enableSubtreeRasterization { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); // Already rasterized from self. if (_flags.rasterizesSubtree) { return; @@ -1285,14 +1287,14 @@ - (void)enableSubtreeRasterization - (CGFloat)contentsScaleForDisplay { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _contentsScaleForDisplay; } - (void)setContentsScaleForDisplay:(CGFloat)contentsScaleForDisplay { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); if (_contentsScaleForDisplay == contentsScaleForDisplay) { return; @@ -1321,7 +1323,7 @@ - (void)__setNeedsDisplay { BOOL shouldScheduleForDisplay = NO; { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); BOOL nowDisplay = ASInterfaceStateIncludesDisplay(_interfaceState); // FIXME: This should not need to recursively display, so create a non-recursive variant. // The semantics of setNeedsDisplay (as defined by CALayer behavior) are not recursive. @@ -1360,7 +1362,7 @@ + (void)scheduleNodeForRecursiveDisplay:(ASDisplayNode *)node /// Helper method to summarize whether or not the node run through the display process - (BOOL)_implementsDisplay { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _flags.implementsDrawRect || _flags.implementsImageDisplay || _flags.rasterizesSubtree; } @@ -1421,7 +1423,7 @@ - (void)_pendingNodeDidDisplay:(ASDisplayNode *)node // For details look at the comment on the canCallSetNeedsDisplayOfLayer flag - (BOOL)_canCallSetNeedsDisplayOfLayer { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _flags.canCallSetNeedsDisplayOfLayer; } @@ -1491,20 +1493,20 @@ - (void)recursivelyEnsureDisplaySynchronously:(BOOL)synchronously - (void)setShouldBypassEnsureDisplay:(BOOL)shouldBypassEnsureDisplay { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); _flags.shouldBypassEnsureDisplay = shouldBypassEnsureDisplay; } - (BOOL)shouldBypassEnsureDisplay { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _flags.shouldBypassEnsureDisplay; } - (void)setNeedsDisplayAtScale:(CGFloat)contentsScale { { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); if (contentsScale == _contentsScaleForDisplay) { return; } @@ -1702,7 +1704,7 @@ static void _recursivelySetDisplaySuspended(ASDisplayNode *node, CALayer *layer, - (BOOL)displaySuspended { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _flags.displaySuspended; } @@ -1999,7 +2001,7 @@ ASDISPLAYNODE_INLINE BOOL subtreeIsRasterized(ASDisplayNode *node) { // NOTE: This method must be dealloc-safe (should not retain self). - (ASDisplayNode *)supernode { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _supernode; } @@ -2008,7 +2010,7 @@ - (void)_setSupernode:(ASDisplayNode *)newSupernode BOOL supernodeDidChange = NO; ASDisplayNode *oldSupernode = nil; { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); if (_supernode != newSupernode) { oldSupernode = _supernode; // Access supernode properties outside of lock to avoid remote chance of deadlock, // in case supernode implementation must access one of our properties. @@ -2085,7 +2087,7 @@ - (void)_setSupernode:(ASDisplayNode *)newSupernode - (NSArray *)subnodes { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); if (_cachedSubnodes == nil) { _cachedSubnodes = [_subnodes copy]; } else { @@ -2231,7 +2233,7 @@ - (void)_addSubnode:(ASDisplayNode *)subnode NSUInteger subnodesIndex; NSUInteger sublayersIndex; { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); subnodesIndex = _subnodes.count; sublayersIndex = _layer.sublayers.count; } @@ -2286,7 +2288,7 @@ - (void)_replaceSubnode:(ASDisplayNode *)oldSubnode withSubnode:(ASDisplayNode * NSInteger subnodeIndex; NSInteger sublayerIndex = NSNotFound; { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); ASDisplayNodeAssert(_subnodes, @"You should have subnodes if you have a subnode"); subnodeIndex = [_subnodes indexOfObjectIdenticalTo:oldSubnode]; @@ -2333,7 +2335,7 @@ - (void)_insertSubnode:(ASDisplayNode *)subnode belowSubnode:(ASDisplayNode *)be NSInteger belowSubnodeIndex; NSInteger belowSublayerIndex = NSNotFound; { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); ASDisplayNodeAssert(_subnodes, @"You should have subnodes if you have a subnode"); belowSubnodeIndex = [_subnodes indexOfObjectIdenticalTo:below]; @@ -2398,7 +2400,7 @@ - (void)_insertSubnode:(ASDisplayNode *)subnode aboveSubnode:(ASDisplayNode *)ab NSInteger aboveSubnodeIndex; NSInteger aboveSublayerIndex = NSNotFound; { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); ASDisplayNodeAssert(_subnodes, @"You should have subnodes if you have a subnode"); aboveSubnodeIndex = [_subnodes indexOfObjectIdenticalTo:above]; @@ -2455,7 +2457,7 @@ - (void)_insertSubnode:(ASDisplayNode *)subnode atIndex:(NSInteger)idx NSInteger sublayerIndex = NSNotFound; { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); if (idx > _subnodes.count || idx < 0) { ASDisplayNodeFailAssert(@"Cannot insert a subnode at index %ld. Count is %ld", (long)idx, (long)_subnodes.count); @@ -2568,13 +2570,13 @@ - (void)_removeFromSupernode:(ASDisplayNode *)supernode view:(UIView *)view laye - (BOOL)__visibilityNotificationsDisabled { // Currently, this method is only used by the testing infrastructure to verify this internal feature. - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _flags.visibilityNotificationsDisabled > 0; } - (BOOL)__selfOrParentHasVisibilityNotificationsDisabled { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return (_hierarchyState & ASHierarchyStateTransitioningSupernodes); } @@ -2676,7 +2678,7 @@ - (BOOL)placeholderShouldPersist - (BOOL)isInHierarchy { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _flags.isInHierarchy; } @@ -2782,7 +2784,7 @@ - (void)exitHierarchyState:(ASHierarchyState)hierarchyState - (ASHierarchyState)hierarchyState { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _hierarchyState; } @@ -2790,7 +2792,7 @@ - (void)setHierarchyState:(ASHierarchyState)newState { ASHierarchyState oldState = ASHierarchyStateNormal; { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); if (_hierarchyState == newState) { return; } @@ -2821,7 +2823,7 @@ - (void)setHierarchyState:(ASHierarchyState)newState // Entering layout pending state } else { // Leaving layout pending state, reset related properties - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); _pendingTransitionID = ASLayoutElementContextInvalidTransitionID; _pendingLayoutTransition = nil; } @@ -2916,7 +2918,7 @@ - (void)didExitHierarchy */ - (BOOL)supportsRangeManagedInterfaceState { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return ASHierarchyStateIncludesRangeManaged(_hierarchyState); } @@ -2959,7 +2961,7 @@ - (void)recursivelySetInterfaceState:(ASInterfaceState)newInterfaceState - (ASInterfaceState)interfaceState { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _interfaceState; } @@ -2968,7 +2970,7 @@ - (void)setInterfaceState:(ASInterfaceState)newState if (!ASCATransactionQueueGet().enabled) { [self applyPendingInterfaceState:newState]; } else { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); if (_pendingInterfaceState != newState) { _pendingInterfaceState = newState; [ASCATransactionQueueGet() enqueue:self]; @@ -2978,7 +2980,7 @@ - (void)setInterfaceState:(ASInterfaceState)newState - (ASInterfaceState)pendingInterfaceState { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _pendingInterfaceState; } @@ -2994,7 +2996,7 @@ - (void)applyPendingInterfaceState:(ASInterfaceState)newPendingState ASInterfaceState oldState = ASInterfaceStateNone; ASInterfaceState newState = ASInterfaceStateNone; { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); // newPendingState will not be used when ASCATransactionQueue is enabled // and use _pendingInterfaceState instead for interfaceState update. if (!ASCATransactionQueueGet().enabled) { @@ -3137,7 +3139,7 @@ - (BOOL)shouldScheduleDisplayWithNewInterfaceState:(ASInterfaceState)newInterfac - (void)addInterfaceStateDelegate:(id )interfaceStateDelegate { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); _hasHadInterfaceStateDelegates = YES; for (int i = 0; i < AS_MAX_INTERFACE_STATE_DELEGATES; i++) { if (_interfaceStateDelegates[i] == nil) { @@ -3150,7 +3152,7 @@ - (void)addInterfaceStateDelegate:(id )interfaceStateD - (void)removeInterfaceStateDelegate:(id )interfaceStateDelegate { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); for (int i = 0; i < AS_MAX_INTERFACE_STATE_DELEGATES; i++) { if (_interfaceStateDelegates[i] == interfaceStateDelegate) { _interfaceStateDelegates[i] = nil; @@ -3161,7 +3163,7 @@ - (void)removeInterfaceStateDelegate:(id )interfaceSta - (BOOL)isVisible { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return ASInterfaceStateIncludesVisible(_interfaceState); } @@ -3199,7 +3201,7 @@ - (void)_didExitVisibleState - (BOOL)isInDisplayState { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return ASInterfaceStateIncludesDisplay(_interfaceState); } @@ -3225,7 +3227,7 @@ - (void)_didExitDisplayState - (BOOL)isInPreloadState { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return ASInterfaceStateIncludesPreload(_interfaceState); } @@ -3292,7 +3294,7 @@ - (void)clearContents ASDisplayNodeAssertMainThread(); ASAssertUnlocked(__instanceLock__); - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); if (_flags.canClearContentsOfLayer) { // No-op if these haven't been created yet, as that guarantees they don't have contents that needs to be released. _layer.contents = nil; @@ -3381,13 +3383,13 @@ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event - (void)setHitTestSlop:(UIEdgeInsets)hitTestSlop { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); _hitTestSlop = hitTestSlop; } - (UIEdgeInsets)hitTestSlop { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _hitTestSlop; } @@ -3431,7 +3433,7 @@ - (void)applyPendingViewState ASDisplayNodeAssertMainThread(); ASAssertUnlocked(__instanceLock__); - ASDN::UniqueLock l(__instanceLock__); + AS::UniqueLock l(__instanceLock__); // FIXME: Ideally we'd call this as soon as the node receives -setNeedsLayout // but automatic subnode management would require us to modify the node tree // in the background on a loaded node, which isn't currently supported. @@ -3502,19 +3504,19 @@ - (ASDisplayNode *)_supernodeWithClass:(Class)supernodeClass checkViewHierarchy: - (void)setMeasurementOptions:(ASDisplayNodePerformanceMeasurementOptions)measurementOptions { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); _measurementOptions = measurementOptions; } - (ASDisplayNodePerformanceMeasurementOptions)measurementOptions { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _measurementOptions; } - (ASDisplayNodePerformanceMeasurements)performanceMeasurements { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); ASDisplayNodePerformanceMeasurements measurements = { .layoutSpecNumberOfPasses = -1, .layoutSpecTotalTime = NAN, .layoutComputationNumberOfPasses = -1, .layoutComputationTotalTime = NAN }; if (_measurementOptions & ASDisplayNodePerformanceMeasurementOptionLayoutSpec) { measurements.layoutSpecNumberOfPasses = _layoutSpecNumberOfPasses; @@ -3531,13 +3533,13 @@ - (ASDisplayNodePerformanceMeasurements)performanceMeasurements - (void)setIsAccessibilityContainer:(BOOL)isAccessibilityContainer { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); _isAccessibilityContainer = isAccessibilityContainer; } - (BOOL)isAccessibilityContainer { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _isAccessibilityContainer; } @@ -3717,7 +3719,7 @@ + (BOOL)shouldStoreUnflattenedLayouts - (ASLayout *)unflattenedCalculatedLayout { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _unflattenedLayout; } @@ -3751,7 +3753,7 @@ - (NSString *)_recursiveDescriptionHelperWithIndent:(NSString *)indent - (NSString *)detailedLayoutDescription { ASPushMainThreadAssertionsDisabled(); - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); const auto props = [[NSMutableArray alloc] init]; [props addObject:@{ @"layoutVersion": @(_layoutVersion.load()) }]; diff --git a/Source/ASEditableTextNode.mm b/Source/ASEditableTextNode.mm index 0adc8b0ad..2740de9c7 100644 --- a/Source/ASEditableTextNode.mm +++ b/Source/ASEditableTextNode.mm @@ -117,14 +117,14 @@ @interface ASEditableTextNode () BOOL _delegateDidUpdateEnqueued; // TextKit. - ASDN::RecursiveMutex _textKitLock; + AS::RecursiveMutex _textKitLock; ASTextKitComponents *_textKitComponents; ASTextKitComponents *_placeholderTextKitComponents; // Forwards NSLayoutManagerDelegate methods related to word kerning ASTextNodeWordKerner *_wordKerner; // UITextInputTraits - ASDN::RecursiveMutex _textInputTraitsLock; + AS::RecursiveMutex _textInputTraitsLock; _ASTextInputTraitsPendingState *_textInputTraits; // Misc. State. @@ -188,7 +188,7 @@ - (void)didLoad // Configure textView with UITextInputTraits { - ASDN::MutexLocker l(_textInputTraitsLock); + AS::MutexLocker l(_textInputTraitsLock); if (_textInputTraits) { textView.autocapitalizationType = _textInputTraits.autocapitalizationType; textView.autocorrectionType = _textInputTraits.autocorrectionType; @@ -204,7 +204,7 @@ - (void)didLoad [self.view addSubview:textView]; }; - ASDN::MutexLocker l(_textKitLock); + AS::MutexLocker l(_textKitLock); // Create and configure the placeholder text view. _placeholderTextKitComponents.textView = [[ASTextKitComponentsTextView alloc] initWithFrame:CGRectZero textContainer:_placeholderTextKitComponents.textContainer]; @@ -259,7 +259,7 @@ - (void)setBackgroundColor:(UIColor *)backgroundColor { [super setBackgroundColor:backgroundColor]; - ASDN::MutexLocker l(_textKitLock); + AS::MutexLocker l(_textKitLock); // If showing the placeholder, don't propagate backgroundColor/opaque to the editable textView. It is positioned over the placeholder to accept taps to begin editing, and if it's opaque/colored then it'll obscure the placeholder. // The backgroundColor/opaque will be propagated to the editable textView when editing begins. @@ -271,7 +271,7 @@ - (void)setBackgroundColor:(UIColor *)backgroundColor - (void)setTextContainerInset:(UIEdgeInsets)textContainerInset { - ASDN::MutexLocker l(_textKitLock); + AS::MutexLocker l(_textKitLock); _textContainerInset = textContainerInset; _textKitComponents.textView.textContainerInset = textContainerInset; @@ -282,7 +282,7 @@ - (void)setOpaque:(BOOL)opaque { [super setOpaque:opaque]; - ASDN::MutexLocker l(_textKitLock); + AS::MutexLocker l(_textKitLock); // If showing the placeholder, don't propagate backgroundColor/opaque to the editable textView. It is positioned over the placeholder to accept taps to begin editing, and if it's opaque/colored then it'll obscure the placeholder. // The backgroundColor/opaque will be propagated to the editable textView when editing begins. @@ -308,7 +308,7 @@ - (BOOL)supportsLayerBacking - (void)setScrollEnabled:(BOOL)scrollEnabled { - ASDN::MutexLocker l(_textKitLock); + AS::MutexLocker l(_textKitLock); _scrollEnabled = scrollEnabled; [_textKitComponents.textView setScrollEnabled:_scrollEnabled]; } @@ -342,7 +342,7 @@ - (void)setTypingAttributes:(NSDictionary *)typingAttributes _typingAttributes = [typingAttributes copy]; - ASDN::MutexLocker l(_textKitLock); + AS::MutexLocker l(_textKitLock); _textKitComponents.textView.typingAttributes = _typingAttributes; } @@ -352,13 +352,13 @@ - (void)setTypingAttributes:(NSDictionary *)typingAttributes - (NSRange)selectedRange { - ASDN::MutexLocker l(_textKitLock); + AS::MutexLocker l(_textKitLock); return _textKitComponents.textView.selectedRange; } - (void)setSelectedRange:(NSRange)selectedRange { - ASDN::MutexLocker l(_textKitLock); + AS::MutexLocker l(_textKitLock); _textKitComponents.textView.selectedRange = selectedRange; } @@ -372,14 +372,14 @@ - (BOOL)isDisplayingPlaceholder @dynamic attributedPlaceholderText; - (NSAttributedString *)attributedPlaceholderText { - ASDN::MutexLocker l(_textKitLock); + AS::MutexLocker l(_textKitLock); return [_placeholderTextKitComponents.textStorage copy]; } - (void)setAttributedPlaceholderText:(NSAttributedString *)attributedPlaceholderText { - ASDN::MutexLocker l(_textKitLock); + AS::MutexLocker l(_textKitLock); if (ASObjectIsEqual(_placeholderTextKitComponents.textStorage, attributedPlaceholderText)) return; @@ -396,14 +396,14 @@ - (NSAttributedString *)attributedText if ([self isDisplayingPlaceholder]) return nil; - ASDN::MutexLocker l(_textKitLock); + AS::MutexLocker l(_textKitLock); return [_textKitComponents.textStorage copy]; } - (void)setAttributedText:(NSAttributedString *)attributedText { - ASDN::MutexLocker l(_textKitLock); + AS::MutexLocker l(_textKitLock); // If we (_cmd) are called while the text view itself is updating (-textViewDidUpdate:), you cannot update the text storage and expect perfect propagation to the text view. // Thus, we always update the textview directly if it's been created already. @@ -445,7 +445,7 @@ - (void)setAttributedText:(NSAttributedString *)attributedText #pragma mark - Core - (void)_updateDisplayingPlaceholder { - ASDN::MutexLocker l(_textKitLock); + AS::MutexLocker l(_textKitLock); // Show the placeholder if necessary. _displayingPlaceholder = (_textKitComponents.textStorage.length == 0); @@ -463,7 +463,7 @@ - (void)_updateDisplayingPlaceholder - (void)_layoutTextView { - ASDN::MutexLocker l(_textKitLock); + AS::MutexLocker l(_textKitLock); // Layout filling our bounds. _textKitComponents.textView.frame = self.bounds; @@ -481,35 +481,35 @@ - (void)_layoutTextView @dynamic textInputMode; - (UITextInputMode *)textInputMode { - ASDN::MutexLocker l(_textKitLock); + AS::MutexLocker l(_textKitLock); return [_textKitComponents.textView textInputMode]; } - (BOOL)isFirstResponder { - ASDN::MutexLocker l(_textKitLock); + AS::MutexLocker l(_textKitLock); return [_textKitComponents.textView isFirstResponder]; } - (BOOL)canBecomeFirstResponder { - ASDN::MutexLocker l(_textKitLock); + AS::MutexLocker l(_textKitLock); return [_textKitComponents.textView canBecomeFirstResponder]; } - (BOOL)becomeFirstResponder { - ASDN::MutexLocker l(_textKitLock); + AS::MutexLocker l(_textKitLock); return [_textKitComponents.textView becomeFirstResponder]; } - (BOOL)canResignFirstResponder { - ASDN::MutexLocker l(_textKitLock); + AS::MutexLocker l(_textKitLock); return [_textKitComponents.textView canResignFirstResponder]; } - (BOOL)resignFirstResponder { - ASDN::MutexLocker l(_textKitLock); + AS::MutexLocker l(_textKitLock); return [_textKitComponents.textView resignFirstResponder]; } @@ -525,7 +525,7 @@ - (_ASTextInputTraitsPendingState *)textInputTraits - (void)setAutocapitalizationType:(UITextAutocapitalizationType)autocapitalizationType { - ASDN::MutexLocker l(_textInputTraitsLock); + AS::MutexLocker l(_textInputTraitsLock); if (self.isNodeLoaded) { [self.textView setAutocapitalizationType:autocapitalizationType]; } else { @@ -535,7 +535,7 @@ - (void)setAutocapitalizationType:(UITextAutocapitalizationType)autocapitalizati - (UITextAutocapitalizationType)autocapitalizationType { - ASDN::MutexLocker l(_textInputTraitsLock); + AS::MutexLocker l(_textInputTraitsLock); if (self.isNodeLoaded) { return [self.textView autocapitalizationType]; } else { @@ -545,7 +545,7 @@ - (UITextAutocapitalizationType)autocapitalizationType - (void)setAutocorrectionType:(UITextAutocorrectionType)autocorrectionType { - ASDN::MutexLocker l(_textInputTraitsLock); + AS::MutexLocker l(_textInputTraitsLock); if (self.isNodeLoaded) { [self.textView setAutocorrectionType:autocorrectionType]; } else { @@ -555,7 +555,7 @@ - (void)setAutocorrectionType:(UITextAutocorrectionType)autocorrectionType - (UITextAutocorrectionType)autocorrectionType { - ASDN::MutexLocker l(_textInputTraitsLock); + AS::MutexLocker l(_textInputTraitsLock); if (self.isNodeLoaded) { return [self.textView autocorrectionType]; } else { @@ -565,7 +565,7 @@ - (UITextAutocorrectionType)autocorrectionType - (void)setSpellCheckingType:(UITextSpellCheckingType)spellCheckingType { - ASDN::MutexLocker l(_textInputTraitsLock); + AS::MutexLocker l(_textInputTraitsLock); if (self.isNodeLoaded) { [self.textView setSpellCheckingType:spellCheckingType]; } else { @@ -575,7 +575,7 @@ - (void)setSpellCheckingType:(UITextSpellCheckingType)spellCheckingType - (UITextSpellCheckingType)spellCheckingType { - ASDN::MutexLocker l(_textInputTraitsLock); + AS::MutexLocker l(_textInputTraitsLock); if (self.isNodeLoaded) { return [self.textView spellCheckingType]; } else { @@ -585,7 +585,7 @@ - (UITextSpellCheckingType)spellCheckingType - (void)setEnablesReturnKeyAutomatically:(BOOL)enablesReturnKeyAutomatically { - ASDN::MutexLocker l(_textInputTraitsLock); + AS::MutexLocker l(_textInputTraitsLock); if (self.isNodeLoaded) { [self.textView setEnablesReturnKeyAutomatically:enablesReturnKeyAutomatically]; } else { @@ -595,7 +595,7 @@ - (void)setEnablesReturnKeyAutomatically:(BOOL)enablesReturnKeyAutomatically - (BOOL)enablesReturnKeyAutomatically { - ASDN::MutexLocker l(_textInputTraitsLock); + AS::MutexLocker l(_textInputTraitsLock); if (self.isNodeLoaded) { return [self.textView enablesReturnKeyAutomatically]; } else { @@ -605,7 +605,7 @@ - (BOOL)enablesReturnKeyAutomatically - (void)setKeyboardAppearance:(UIKeyboardAppearance)setKeyboardAppearance { - ASDN::MutexLocker l(_textInputTraitsLock); + AS::MutexLocker l(_textInputTraitsLock); if (self.isNodeLoaded) { [self.textView setKeyboardAppearance:setKeyboardAppearance]; } else { @@ -615,7 +615,7 @@ - (void)setKeyboardAppearance:(UIKeyboardAppearance)setKeyboardAppearance - (UIKeyboardAppearance)keyboardAppearance { - ASDN::MutexLocker l(_textInputTraitsLock); + AS::MutexLocker l(_textInputTraitsLock); if (self.isNodeLoaded) { return [self.textView keyboardAppearance]; } else { @@ -625,7 +625,7 @@ - (UIKeyboardAppearance)keyboardAppearance - (void)setKeyboardType:(UIKeyboardType)keyboardType { - ASDN::MutexLocker l(_textInputTraitsLock); + AS::MutexLocker l(_textInputTraitsLock); if (self.isNodeLoaded) { [self.textView setKeyboardType:keyboardType]; } else { @@ -635,7 +635,7 @@ - (void)setKeyboardType:(UIKeyboardType)keyboardType - (UIKeyboardType)keyboardType { - ASDN::MutexLocker l(_textInputTraitsLock); + AS::MutexLocker l(_textInputTraitsLock); if (self.isNodeLoaded) { return [self.textView keyboardType]; } else { @@ -645,7 +645,7 @@ - (UIKeyboardType)keyboardType - (void)setReturnKeyType:(UIReturnKeyType)returnKeyType { - ASDN::MutexLocker l(_textInputTraitsLock); + AS::MutexLocker l(_textInputTraitsLock); if (self.isNodeLoaded) { [self.textView setReturnKeyType:returnKeyType]; } else { @@ -655,7 +655,7 @@ - (void)setReturnKeyType:(UIReturnKeyType)returnKeyType - (UIReturnKeyType)returnKeyType { - ASDN::MutexLocker l(_textInputTraitsLock); + AS::MutexLocker l(_textInputTraitsLock); if (self.isNodeLoaded) { return [self.textView returnKeyType]; } else { @@ -665,7 +665,7 @@ - (UIReturnKeyType)returnKeyType - (void)setSecureTextEntry:(BOOL)secureTextEntry { - ASDN::MutexLocker l(_textInputTraitsLock); + AS::MutexLocker l(_textInputTraitsLock); if (self.isNodeLoaded) { [self.textView setSecureTextEntry:secureTextEntry]; } else { @@ -675,7 +675,7 @@ - (void)setSecureTextEntry:(BOOL)secureTextEntry - (BOOL)isSecureTextEntry { - ASDN::MutexLocker l(_textInputTraitsLock); + AS::MutexLocker l(_textInputTraitsLock); if (self.isNodeLoaded) { return [self.textView isSecureTextEntry]; } else { @@ -704,7 +704,7 @@ - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range r - (void)textViewDidChange:(UITextView *)textView { - ASDN::MutexLocker l(_textKitLock); + AS::MutexLocker l(_textKitLock); // Note we received a text changed event. // This is used by _delegateDidChangeSelectionFromSelectedRange:toSelectedRange: to distinguish between selection changes that happen because of editing or pure selection changes. @@ -767,7 +767,7 @@ - (CGRect)layoutManager:(NSLayoutManager *)layoutManager boundingBoxForControlGl #pragma mark - Geometry - (CGRect)frameForTextRange:(NSRange)textRange { - ASDN::MutexLocker l(_textKitLock); + AS::MutexLocker l(_textKitLock); // Bail on invalid range. if (NSMaxRange(textRange) > [_textKitComponents.textStorage length]) { diff --git a/Source/ASImageNode+AnimatedImage.mm b/Source/ASImageNode+AnimatedImage.mm index 99ecf82d5..c07630cba 100644 --- a/Source/ASImageNode+AnimatedImage.mm +++ b/Source/ASImageNode+AnimatedImage.mm @@ -166,13 +166,13 @@ - (void)_locked_setCoverImage:(UIImage *)coverImage - (NSString *)animatedImageRunLoopMode { - ASDN::MutexLocker l(_displayLinkLock); + AS::MutexLocker l(_displayLinkLock); return _animatedImageRunLoopMode; } - (void)setAnimatedImageRunLoopMode:(NSString *)runLoopMode { - ASDN::MutexLocker l(_displayLinkLock); + AS::MutexLocker l(_displayLinkLock); if (runLoopMode == nil) { runLoopMode = ASAnimatedImageDefaultRunLoopMode; @@ -249,7 +249,7 @@ - (void)_locked_startAnimating // Get frame interval before holding display link lock to avoid deadlock NSUInteger frameInterval = self.animatedImage.frameInterval; - ASDN::MutexLocker l(_displayLinkLock); + AS::MutexLocker l(_displayLinkLock); if (_displayLink == nil) { _playHead = 0; _displayLink = [CADisplayLink displayLinkWithTarget:[ASWeakProxy weakProxyWithTarget:self] selector:@selector(displayLinkFired:)]; @@ -279,7 +279,7 @@ - (void)_locked_stopAnimating NSLog(@"stopping animation: %p", self); #endif ASDisplayNodeAssertMainThread(); - ASDN::MutexLocker l(_displayLinkLock); + AS::MutexLocker l(_displayLinkLock); _displayLink.paused = YES; self.lastDisplayLinkFire = 0; @@ -398,7 +398,7 @@ @implementation ASImageNode(AnimatedImageInvalidation) - (void)invalidateAnimatedImage { - ASDN::MutexLocker l(_displayLinkLock); + AS::MutexLocker l(_displayLinkLock); #if ASAnimatedImageDebug if (_displayLink) { NSLog(@"invalidating display link"); diff --git a/Source/ASImageNode.mm b/Source/ASImageNode.mm index ce473e3e7..063617b5a 100644 --- a/Source/ASImageNode.mm +++ b/Source/ASImageNode.mm @@ -202,7 +202,7 @@ - (UIImage *)placeholderImage return nil; } - ASDN::MutexLocker l(__instanceLock__); + AS::MutexLocker l(__instanceLock__); ASGraphicsBeginImageContextWithOptions(size, NO, 1); [self.placeholderColor setFill]; @@ -229,7 +229,7 @@ - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize - (void)setImage:(UIImage *)image { - ASDN::MutexLocker l(__instanceLock__); + AS::MutexLocker l(__instanceLock__); [self _locked_setImage:image]; } @@ -436,13 +436,13 @@ + (UIImage *)displayWithParameters:(id)parameter isCancelled:(NS_NOESC + (ASWeakMapEntry *)contentsForkey:(ASImageNodeContentsKey *)key drawParameters:(id)drawParameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelled { static dispatch_once_t onceToken; - static ASDN::Mutex *cacheLock = nil; + static AS::Mutex *cacheLock = nil; dispatch_once(&onceToken, ^{ - cacheLock = new ASDN::Mutex(); + cacheLock = new AS::Mutex(); }); { - ASDN::MutexLocker l(*cacheLock); + AS::MutexLocker l(*cacheLock); if (!cache) { cache = [[ASWeakMap alloc] init]; } @@ -459,7 +459,7 @@ + (ASWeakMapEntry *)contentsForkey:(ASImageNodeContentsKey *)key drawParameters: } { - ASDN::MutexLocker l(*cacheLock); + AS::MutexLocker l(*cacheLock); return [cache setObject:contents forKey:key]; } } @@ -581,7 +581,7 @@ - (void)setNeedsDisplayWithCompletion:(void (^ _Nullable)(BOOL canceled))display // Stash the block and call-site queue. We'll invoke it in -displayDidFinish. { - ASDN::MutexLocker l(__instanceLock__); + AS::MutexLocker l(__instanceLock__); if (_displayCompletionBlock != displayCompletionBlock) { _displayCompletionBlock = displayCompletionBlock; } @@ -596,7 +596,7 @@ - (void)clearContents { [super clearContents]; - ASDN::MutexLocker l(__instanceLock__); + AS::MutexLocker l(__instanceLock__); _weakCacheEntry = nil; // release contents from the cache. } @@ -604,7 +604,7 @@ - (void)clearContents - (BOOL)isCropEnabled { - ASDN::MutexLocker l(__instanceLock__); + AS::MutexLocker l(__instanceLock__); return _cropEnabled; } @@ -640,14 +640,14 @@ - (void)setCropEnabled:(BOOL)cropEnabled recropImmediately:(BOOL)recropImmediate - (CGRect)cropRect { - ASDN::MutexLocker l(__instanceLock__); + AS::MutexLocker l(__instanceLock__); return _cropRect; } - (void)setCropRect:(CGRect)cropRect { { - ASDN::MutexLocker l(__instanceLock__); + AS::MutexLocker l(__instanceLock__); if (CGRectEqualToRect(_cropRect, cropRect)) { return; } @@ -670,37 +670,37 @@ - (void)setCropRect:(CGRect)cropRect - (BOOL)forceUpscaling { - ASDN::MutexLocker l(__instanceLock__); + AS::MutexLocker l(__instanceLock__); return _forceUpscaling; } - (void)setForceUpscaling:(BOOL)forceUpscaling { - ASDN::MutexLocker l(__instanceLock__); + AS::MutexLocker l(__instanceLock__); _forceUpscaling = forceUpscaling; } - (CGSize)forcedSize { - ASDN::MutexLocker l(__instanceLock__); + AS::MutexLocker l(__instanceLock__); return _forcedSize; } - (void)setForcedSize:(CGSize)forcedSize { - ASDN::MutexLocker l(__instanceLock__); + AS::MutexLocker l(__instanceLock__); _forcedSize = forcedSize; } - (asimagenode_modification_block_t)imageModificationBlock { - ASDN::MutexLocker l(__instanceLock__); + AS::MutexLocker l(__instanceLock__); return _imageModificationBlock; } - (void)setImageModificationBlock:(asimagenode_modification_block_t)imageModificationBlock { - ASDN::MutexLocker l(__instanceLock__); + AS::MutexLocker l(__instanceLock__); _imageModificationBlock = imageModificationBlock; } diff --git a/Source/ASMultiplexImageNode.mm b/Source/ASMultiplexImageNode.mm index bd61dc02c..ff5e9357e 100644 --- a/Source/ASMultiplexImageNode.mm +++ b/Source/ASMultiplexImageNode.mm @@ -32,6 +32,8 @@ #import #endif +using AS::MutexLocker; + NSString *const ASMultiplexImageNodeErrorDomain = @"ASMultiplexImageNodeErrorDomain"; #if AS_USE_ASSETS_LIBRARY @@ -80,7 +82,7 @@ @interface ASMultiplexImageNode () // Image flags. BOOL _downloadsIntermediateImages; // Defaults to NO. - ASDN::Mutex _imageIdentifiersLock; + AS::Mutex _imageIdentifiersLock; NSArray *_imageIdentifiers; id _loadedImageIdentifier; id _loadingImageIdentifier; @@ -88,7 +90,7 @@ @interface ASMultiplexImageNode () __weak NSOperation *_phImageRequestOperation; // Networking. - ASDN::RecursiveMutex _downloadIdentifierLock; + AS::RecursiveMutex _downloadIdentifierLock; id _downloadIdentifier; // Properties @@ -371,14 +373,14 @@ - (BOOL)shouldRenderProgressImages - (NSArray *)imageIdentifiers { - ASDN::MutexLocker l(_imageIdentifiersLock); + MutexLocker l(_imageIdentifiersLock); return _imageIdentifiers; } - (void)setImageIdentifiers:(NSArray *)imageIdentifiers { { - ASDN::MutexLocker l(_imageIdentifiersLock); + MutexLocker l(_imageIdentifiersLock); if (ASObjectIsEqual(_imageIdentifiers, imageIdentifiers)) { return; } @@ -429,7 +431,7 @@ - (void)_setDisplayedImageIdentifier:(id)displayedImageIdentifier withImage:(UII - (void)_setDownloadIdentifier:(id)downloadIdentifier { - ASDN::MutexLocker l(_downloadIdentifierLock); + MutexLocker l(_downloadIdentifierLock); if (ASObjectIsEqual(downloadIdentifier, _downloadIdentifier)) return; @@ -454,7 +456,7 @@ - (void)_loadImageIdentifiers - (UIImage *)_bestImmediatelyAvailableImageFromDataSource:(id *)imageIdentifierOut { - ASDN::MutexLocker l(_imageIdentifiersLock); + MutexLocker l(_imageIdentifiersLock); // If we don't have any identifiers to load or don't implement the image DS method, bail. if ([_imageIdentifiers count] == 0 || !_dataSourceFlags.image) { @@ -489,7 +491,7 @@ - (void)_updatePriorityOnDownloaderIfNeededWithDefaultPriority:(ASImageDownloade if (_downloaderFlags.downloaderImplementsSetPriority) { // Read our interface state before locking so that we don't lock super while holding our lock. ASInterfaceState interfaceState = self.interfaceState; - ASDN::MutexLocker l(_downloadIdentifierLock); + MutexLocker l(_downloadIdentifierLock); if (_downloadIdentifier != nil) { ASImageDownloaderPriority priority = defaultPriority; @@ -510,7 +512,7 @@ - (void)_updateProgressImageBlockOnDownloaderIfNeeded // Read our interface state before locking so that we don't lock super while holding our lock. ASInterfaceState interfaceState = self.interfaceState; - ASDN::MutexLocker l(_downloadIdentifierLock); + MutexLocker l(_downloadIdentifierLock); if (!_downloaderFlags.downloaderImplementsSetProgress || _downloadIdentifier == nil) { return; @@ -525,7 +527,7 @@ - (void)_updateProgressImageBlockOnDownloaderIfNeeded return; } - ASDN::MutexLocker l(strongSelf->_downloadIdentifierLock); + MutexLocker l(strongSelf->_downloadIdentifierLock); //Getting a result back for a different download identifier, download must not have been successfully canceled if (ASObjectIsEqual(strongSelf->_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) { return; @@ -554,7 +556,7 @@ - (void)_clearImage #pragma mark - - (id)_nextImageIdentifierToDownload { - ASDN::MutexLocker l(_imageIdentifiersLock); + MutexLocker l(_imageIdentifiersLock); // If we've already loaded the best identifier, we've got nothing else to do. id bestImageIdentifier = _imageIdentifiers.firstObject; @@ -848,7 +850,7 @@ - (void)_downloadImageWithIdentifier:(id)imageIdentifier URL:(NSURL *)imageURL c if (!strongSelf) return; - ASDN::MutexLocker l(strongSelf->_downloadIdentifierLock); + MutexLocker l(strongSelf->_downloadIdentifierLock); //Getting a result back for a different download identifier, download must not have been successfully canceled if (ASObjectIsEqual(strongSelf->_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) { return; diff --git a/Source/ASNodeController+Beta.mm b/Source/ASNodeController+Beta.mm index 3b3a12629..14aae3363 100644 --- a/Source/ASNodeController+Beta.mm +++ b/Source/ASNodeController+Beta.mm @@ -18,7 +18,7 @@ @implementation ASNodeController { ASDisplayNode *_strongNode; __weak ASDisplayNode *_weakNode; - ASDN::RecursiveMutex __instanceLock__; + AS::RecursiveMutex __instanceLock__; } - (void)loadNode diff --git a/Source/ASRunLoopQueue.mm b/Source/ASRunLoopQueue.mm index 7ddb59103..c85dccb92 100644 --- a/Source/ASRunLoopQueue.mm +++ b/Source/ASRunLoopQueue.mm @@ -22,6 +22,8 @@ #define ASRunLoopQueueLoggingEnabled 0 #define ASRunLoopQueueVerboseLoggingEnabled 0 +using AS::MutexLocker; + static void runLoopSourceCallback(void *info) { // No-op #if ASRunLoopQueueVerboseLoggingEnabled @@ -33,7 +35,7 @@ static void runLoopSourceCallback(void *info) { @implementation ASDeallocQueue { std::vector _queue; - ASDN::Mutex _lock; + AS::Mutex _lock; } + (ASDeallocQueue *)sharedDeallocationQueue NS_RETURNS_RETAINED @@ -111,7 +113,7 @@ @interface ASRunLoopQueue () { CFRunLoopSourceRef _runLoopSource; CFRunLoopObserverRef _runLoopObserver; NSPointerArray *_internalQueue; // Use NSPointerArray so we can decide __strong or __weak per-instance. - ASDN::RecursiveMutex _internalQueueLock; + AS::RecursiveMutex _internalQueueLock; // In order to not pollute the top-level activities, each queue has 1 root activity. os_activity_t _rootActivity; @@ -205,7 +207,7 @@ - (void)processQueue BOOL isQueueDrained = NO; { - ASDN::MutexLocker l(_internalQueueLock); + MutexLocker l(_internalQueueLock); NSInteger internalQueueCount = _internalQueue.count; // Early-exit if the queue is empty. @@ -285,7 +287,7 @@ - (void)enqueue:(id)object return; } - ASDN::MutexLocker l(_internalQueueLock); + MutexLocker l(_internalQueueLock); // Check if the object exists. BOOL foundObject = NO; @@ -310,7 +312,7 @@ - (void)enqueue:(id)object - (BOOL)isEmpty { - ASDN::MutexLocker l(_internalQueueLock); + MutexLocker l(_internalQueueLock); return _internalQueue.count == 0; } @@ -334,7 +336,7 @@ @interface ASCATransactionQueue () { // Temporary buffer, only accessed from the main thread in -process. std::vector> _batchBuffer; - ASDN::Mutex _internalQueueLock; + AS::Mutex _internalQueueLock; // In order to not pollute the top-level activities, each queue has 1 root activity. os_activity_t _rootActivity; @@ -428,7 +430,7 @@ - (void)processQueue { ASDisplayNodeAssertMainThread(); - ASDN::UniqueLock l(_internalQueueLock); + AS::UniqueLock l(_internalQueueLock); NSInteger count = _internalQueue.size(); // Early-exit if the queue is empty. if (count == 0) { @@ -464,7 +466,7 @@ - (void)enqueue:(id)object return; } - ASDN::MutexLocker l(_internalQueueLock); + MutexLocker l(_internalQueueLock); if (CFSetContainsValue(_internalQueueHashSet, (__bridge void *)object)) { return; } @@ -478,7 +480,7 @@ - (void)enqueue:(id)object - (BOOL)isEmpty { - ASDN::MutexLocker l(_internalQueueLock); + MutexLocker l(_internalQueueLock); return _internalQueue.empty(); } diff --git a/Source/ASTableNode.mm b/Source/ASTableNode.mm index 652496281..7ef33d138 100644 --- a/Source/ASTableNode.mm +++ b/Source/ASTableNode.mm @@ -100,7 +100,7 @@ - (void)setTuningParameters:(ASRangeTuningParameters)tuningParameters forRangeMo @interface ASTableNode () { - ASDN::RecursiveMutex _environmentStateLock; + AS::RecursiveMutex _environmentStateLock; id _batchFetchingDelegate; } diff --git a/Source/ASTextNode2.mm b/Source/ASTextNode2.mm index bd8d31329..7fa844ba1 100644 --- a/Source/ASTextNode2.mm +++ b/Source/ASTextNode2.mm @@ -32,7 +32,7 @@ @interface ASTextCacheValue : NSObject { @package - ASDN::Mutex _m; + AS::Mutex _m; std::deque> _layouts; } @end @@ -55,10 +55,10 @@ @implementation ASTextCacheValue */ static NS_RETURNS_RETAINED ASTextLayout *ASTextNodeCompatibleLayoutWithContainerAndText(ASTextContainer *container, NSAttributedString *text) { static dispatch_once_t onceToken; - static ASDN::Mutex *layoutCacheLock; + static AS::Mutex *layoutCacheLock; static NSCache *textLayoutCache; dispatch_once(&onceToken, ^{ - layoutCacheLock = new ASDN::Mutex(); + layoutCacheLock = new AS::Mutex(); textLayoutCache = [[NSCache alloc] init]; }); @@ -71,7 +71,7 @@ @implementation ASTextCacheValue } // Lock the cache item for the rest of the method. Only after acquiring can we release the NSCache. - ASDN::MutexLocker lock(cacheValue->_m); + AS::MutexLocker lock(cacheValue->_m); layoutCacheLock->unlock(); CGRect containerBounds = (CGRect){ .size = container.size }; diff --git a/Source/ASVideoNode.mm b/Source/ASVideoNode.mm index 654ce9384..a9453fc76 100644 --- a/Source/ASVideoNode.mm +++ b/Source/ASVideoNode.mm @@ -326,7 +326,7 @@ - (void)setVideoPlaceholderImage:(UIImage *)image - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { - ASDN::UniqueLock l(__instanceLock__); + AS::UniqueLock l(__instanceLock__); if (object == _currentPlayerItem) { if ([keyPath isEqualToString:kStatus]) { diff --git a/Source/Details/ASBasicImageDownloader.mm b/Source/Details/ASBasicImageDownloader.mm index 6c4010f57..178d53211 100644 --- a/Source/Details/ASBasicImageDownloader.mm +++ b/Source/Details/ASBasicImageDownloader.mm @@ -16,6 +16,7 @@ #import #import +using AS::MutexLocker; #pragma mark - /** @@ -42,7 +43,7 @@ static inline float NSURLSessionTaskPriorityWithImageDownloaderPriority(ASImageD @interface ASBasicImageDownloaderContext () { BOOL _invalid; - ASDN::RecursiveMutex __instanceLock__; + AS::RecursiveMutex __instanceLock__; } @property (nonatomic) NSMutableArray *callbackDatas; @@ -53,19 +54,19 @@ @implementation ASBasicImageDownloaderContext static NSMutableDictionary *currentRequests = nil; -+ (ASDN::Mutex *)currentRequestLock ++ (AS::Mutex *)currentRequestLock { static dispatch_once_t onceToken; - static ASDN::Mutex *currentRequestsLock; + static AS::Mutex *currentRequestsLock; dispatch_once(&onceToken, ^{ - currentRequestsLock = new ASDN::Mutex(); + currentRequestsLock = new AS::Mutex(); }); return currentRequestsLock; } + (ASBasicImageDownloaderContext *)contextForURL:(NSURL *)URL { - ASDN::MutexLocker l(*self.currentRequestLock); + MutexLocker l(*self.currentRequestLock); if (!currentRequests) { currentRequests = [[NSMutableDictionary alloc] init]; } @@ -79,7 +80,7 @@ + (ASBasicImageDownloaderContext *)contextForURL:(NSURL *)URL + (void)cancelContextWithURL:(NSURL *)URL { - ASDN::MutexLocker l(*self.currentRequestLock); + MutexLocker l(*self.currentRequestLock); if (currentRequests) { [currentRequests removeObjectForKey:URL]; } @@ -96,7 +97,7 @@ - (instancetype)initWithURL:(NSURL *)URL - (void)cancel { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); NSURLSessionTask *sessionTask = self.sessionTask; if (sessionTask) { @@ -110,19 +111,19 @@ - (void)cancel - (BOOL)isCancelled { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _invalid; } - (void)addCallbackData:(NSDictionary *)callbackData { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); [self.callbackDatas addObject:callbackData]; } - (void)performProgressBlocks:(CGFloat)progress { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); for (NSDictionary *callbackData in self.callbackDatas) { ASImageDownloaderProgress progressBlock = callbackData[kASBasicImageDownloaderContextProgressBlock]; dispatch_queue_t callbackQueue = callbackData[kASBasicImageDownloaderContextCallbackQueue]; @@ -137,7 +138,7 @@ - (void)performProgressBlocks:(CGFloat)progress - (void)completeWithImage:(UIImage *)image error:(NSError *)error { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); for (NSDictionary *callbackData in self.callbackDatas) { ASImageDownloaderCompletion completionBlock = callbackData[kASBasicImageDownloaderContextCompletionBlock]; dispatch_queue_t callbackQueue = callbackData[kASBasicImageDownloaderContextCallbackQueue]; @@ -155,7 +156,7 @@ - (void)completeWithImage:(UIImage *)image error:(NSError *)error - (NSURLSessionTask *)createSessionTaskIfNecessaryWithBlock:(NSURLSessionTask *(^)())creationBlock { { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); if (self.isCancelled) { return nil; @@ -169,7 +170,7 @@ - (NSURLSessionTask *)createSessionTaskIfNecessaryWithBlock:(NSURLSessionTask *( NSURLSessionTask *newTask = creationBlock(); { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); if (self.isCancelled) { return nil; diff --git a/Source/Details/ASCollectionLayoutState.mm b/Source/Details/ASCollectionLayoutState.mm index 47087a192..eb869b334 100644 --- a/Source/Details/ASCollectionLayoutState.mm +++ b/Source/Details/ASCollectionLayoutState.mm @@ -32,7 +32,7 @@ @implementation NSMapTable (ASCollectionLayoutConvenience) @end @implementation ASCollectionLayoutState { - ASDN::Mutex __instanceLock__; + AS::Mutex __instanceLock__; CGSize _contentSize; ASCollectionLayoutContext *_context; NSMapTable *_elementToLayoutAttributesTable; @@ -182,7 +182,7 @@ - (ASPageToLayoutAttributesTable *)getAndRemoveUnmeasuredLayoutAttributesPageTab CGSize pageSize = _context.viewportSize; CGSize contentSize = _contentSize; - ASDN::MutexLocker l(__instanceLock__); + AS::MutexLocker l(__instanceLock__); if (_unmeasuredPageToLayoutAttributesTable.count == 0 || CGRectIsNull(rect) || CGRectIsEmpty(rect) || CGSizeEqualToSize(CGSizeZero, contentSize) || CGSizeEqualToSize(CGSizeZero, pageSize)) { return nil; } diff --git a/Source/Details/ASDataController.mm b/Source/Details/ASDataController.mm index f36dd3950..07388f4c6 100644 --- a/Source/Details/ASDataController.mm +++ b/Source/Details/ASDataController.mm @@ -567,7 +567,7 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet NSTimeInterval transactionQueueFlushDuration = 0.0f; { - ASDN::ScopeTimer t(transactionQueueFlushDuration); + AS::ScopeTimer t(transactionQueueFlushDuration); dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); } diff --git a/Source/Details/ASEventLog.mm b/Source/Details/ASEventLog.mm index 00bdbb558..25ecc2996 100644 --- a/Source/Details/ASEventLog.mm +++ b/Source/Details/ASEventLog.mm @@ -13,7 +13,7 @@ #import @implementation ASEventLog { - ASDN::RecursiveMutex __instanceLock__; + AS::RecursiveMutex __instanceLock__; // The index of the most recent log entry. -1 until first entry. NSInteger _eventLogHead; @@ -61,7 +61,7 @@ - (void)logEventWithBacktrace:(NSArray *)backtrace format:(NSString arguments:args]; va_end(args); - ASDN::MutexLocker l(__instanceLock__); + AS::MutexLocker l(__instanceLock__); NSCache *cache = [ASEventLog contentsCache]; NSMutableArray *events = [cache objectForKey:self]; if (events == nil) { @@ -87,7 +87,7 @@ - (void)logEventWithBacktrace:(NSArray *)backtrace format:(NSString return nil; } - ASDN::MutexLocker l(__instanceLock__); + AS::MutexLocker l(__instanceLock__); NSUInteger tail = (_eventLogHead + 1); NSUInteger count = events.count; diff --git a/Source/Details/ASMainSerialQueue.mm b/Source/Details/ASMainSerialQueue.mm index 3a5fa00f8..06cb7c3b9 100644 --- a/Source/Details/ASMainSerialQueue.mm +++ b/Source/Details/ASMainSerialQueue.mm @@ -14,7 +14,7 @@ @interface ASMainSerialQueue () { - ASDN::Mutex _serialQueueLock; + AS::Mutex _serialQueueLock; NSMutableArray *_blocks; } @@ -34,14 +34,14 @@ - (instancetype)init - (NSUInteger)numberOfScheduledBlocks { - ASDN::MutexLocker l(_serialQueueLock); + AS::MutexLocker l(_serialQueueLock); return _blocks.count; } - (void)performBlockOnMainThread:(dispatch_block_t)block { - ASDN::UniqueLock l(_serialQueueLock); + AS::UniqueLock l(_serialQueueLock); [_blocks addObject:block]; { l.unlock(); @@ -53,7 +53,7 @@ - (void)performBlockOnMainThread:(dispatch_block_t)block - (void)runBlocks { dispatch_block_t mainThread = ^{ - ASDN::UniqueLock l(self->_serialQueueLock); + AS::UniqueLock l(self->_serialQueueLock); do { dispatch_block_t block; if (self->_blocks.count > 0) { diff --git a/Source/Details/ASThread.h b/Source/Details/ASThread.h index d5896fd13..9faa35743 100644 --- a/Source/Details/ASThread.h +++ b/Source/Details/ASThread.h @@ -101,7 +101,7 @@ ASDISPLAYNODE_INLINE void _ASUnlockScopeCleanup(id __strong *lockPtr) #define ASAssertLocked(m) m.AssertHeld() #define ASAssertUnlocked(m) m.AssertNotHeld() -namespace ASDN { +namespace AS { // Set once in Mutex constructor. Linker fails if this is a member variable. ?? static bool gMutex_unfair; @@ -290,6 +290,6 @@ namespace ASDN { typedef std::lock_guard MutexLocker; typedef std::unique_lock UniqueLock; -} // namespace ASDN +} // namespace AS #endif /* __cplusplus */ diff --git a/Source/Details/ASTraitCollection.h b/Source/Details/ASTraitCollection.h index 9ecf433ec..68244a660 100644 --- a/Source/Details/ASTraitCollection.h +++ b/Source/Details/ASTraitCollection.h @@ -119,7 +119,7 @@ AS_EXTERN void ASTraitCollectionPropagateDown(id element, ASPri #define ASLayoutElementCollectionTableSetTraitCollection(lock) \ - (void)setPrimitiveTraitCollection:(ASPrimitiveTraitCollection)traitCollection\ {\ - ASDN::MutexLocker l(lock);\ + AS::MutexLocker l(lock);\ \ ASPrimitiveTraitCollection oldTraits = self.primitiveTraitCollection;\ [super setPrimitiveTraitCollection:traitCollection];\ diff --git a/Source/Layout/ASLayoutElement.mm b/Source/Layout/ASLayoutElement.mm index 7cc168083..8876ab1be 100644 --- a/Source/Layout/ASLayoutElement.mm +++ b/Source/Layout/ASLayoutElement.mm @@ -17,6 +17,8 @@ #import +using AS::MutexLocker; + #if YOGA #import YOGA_HEADER_PATH #import @@ -160,7 +162,7 @@ void ASLayoutElementPopContext() } while(0) @implementation ASLayoutElementStyle { - ASDN::RecursiveMutex __instanceLock__; + AS::RecursiveMutex __instanceLock__; ASLayoutElementStyleExtensions _extensions; std::atomic _size; @@ -545,7 +547,7 @@ - (void)setLayoutOptionExtensionBool:(BOOL)value atIndex:(int)idx { NSCAssert(idx < kMaxLayoutElementBoolExtensions, @"Setting index outside of max bool extensions space"); - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); _extensions.boolExtensions[idx] = value; } @@ -553,7 +555,7 @@ - (BOOL)layoutOptionExtensionBoolAtIndex:(int)idx\ { NSCAssert(idx < kMaxLayoutElementBoolExtensions, @"Accessing index outside of max bool extensions space"); - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _extensions.boolExtensions[idx]; } @@ -561,7 +563,7 @@ - (void)setLayoutOptionExtensionInteger:(NSInteger)value atIndex:(int)idx { NSCAssert(idx < kMaxLayoutElementStateIntegerExtensions, @"Setting index outside of max integer extensions space"); - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); _extensions.integerExtensions[idx] = value; } @@ -569,7 +571,7 @@ - (NSInteger)layoutOptionExtensionIntegerAtIndex:(int)idx { NSCAssert(idx < kMaxLayoutElementStateIntegerExtensions, @"Accessing index outside of max integer extensions space"); - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _extensions.integerExtensions[idx]; } @@ -577,7 +579,7 @@ - (void)setLayoutOptionExtensionEdgeInsets:(UIEdgeInsets)value atIndex:(int)idx { NSCAssert(idx < kMaxLayoutElementStateEdgeInsetExtensions, @"Setting index outside of max edge insets extensions space"); - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); _extensions.edgeInsetsExtensions[idx] = value; } @@ -585,7 +587,7 @@ - (UIEdgeInsets)layoutOptionExtensionEdgeInsetsAtIndex:(int)idx { NSCAssert(idx < kMaxLayoutElementStateEdgeInsetExtensions, @"Accessing index outside of max edge insets extensions space"); - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _extensions.edgeInsetsExtensions[idx]; } diff --git a/Source/Layout/ASLayoutSpec.mm b/Source/Layout/ASLayoutSpec.mm index bcec51599..8da22d3d0 100644 --- a/Source/Layout/ASLayoutSpec.mm +++ b/Source/Layout/ASLayoutSpec.mm @@ -62,7 +62,7 @@ - (BOOL)implementsLayoutMethod - (ASLayoutElementStyle *)style { - ASDN::MutexLocker l(__instanceLock__); + AS::MutexLocker l(__instanceLock__); if (_style == nil) { _style = [[ASLayoutElementStyle alloc] init]; } @@ -142,7 +142,7 @@ - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state object - (ASTraitCollection *)asyncTraitCollection { - ASDN::MutexLocker l(__instanceLock__); + AS::MutexLocker l(__instanceLock__); return [ASTraitCollection traitCollectionWithASPrimitiveTraitCollection:self.primitiveTraitCollection]; } @@ -221,13 +221,13 @@ - (void)_findDuplicatedElementsInSubtreeWithWorkingSet:(NSHashTable #import +using AS::MutexLocker; + @implementation ASCollectionLayoutCache { - ASDN::Mutex __instanceLock__; + AS::Mutex __instanceLock__; /** * The underlying data structure of this cache. @@ -46,7 +48,7 @@ - (ASCollectionLayoutState *)layoutForContext:(ASCollectionLayoutContext *)conte return nil; } - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return [[_map objectForKey:elements] objectForKey:context]; } @@ -57,7 +59,7 @@ - (void)setLayout:(ASCollectionLayoutState *)layout forContext:(ASCollectionLayo return; } - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); auto innerMap = [_map objectForKey:elements]; if (innerMap == nil) { innerMap = [NSMapTable strongToStrongObjectsMapTable]; @@ -73,13 +75,13 @@ - (void)removeLayoutForContext:(ASCollectionLayoutContext *)context return; } - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); [[_map objectForKey:elements] removeObjectForKey:context]; } - (void)removeAllLayouts { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); [_map removeAllObjects]; } diff --git a/Source/Private/ASDisplayNode+AsyncDisplay.mm b/Source/Private/ASDisplayNode+AsyncDisplay.mm index cb9a2f42e..31662bd70 100644 --- a/Source/Private/ASDisplayNode+AsyncDisplay.mm +++ b/Source/Private/ASDisplayNode+AsyncDisplay.mm @@ -18,6 +18,7 @@ #import #import +using AS::MutexLocker; @interface ASDisplayNode () <_ASDisplayLayerDelegate> @end @@ -467,25 +468,25 @@ - (void)cancelDisplayAsyncLayer:(_ASDisplayLayer *)asyncLayer - (ASDisplayNodeContextModifier)willDisplayNodeContentWithRenderingContext { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _willDisplayNodeContentWithRenderingContext; } - (ASDisplayNodeContextModifier)didDisplayNodeContentWithRenderingContext { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); return _didDisplayNodeContentWithRenderingContext; } - (void)setWillDisplayNodeContentWithRenderingContext:(ASDisplayNodeContextModifier)contextModifier { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); _willDisplayNodeContentWithRenderingContext = contextModifier; } - (void)setDidDisplayNodeContentWithRenderingContext:(ASDisplayNodeContextModifier)contextModifier; { - ASDN::MutexLocker l(__instanceLock__); + MutexLocker l(__instanceLock__); _didDisplayNodeContentWithRenderingContext = contextModifier; } diff --git a/Source/Private/ASDisplayNode+UIViewBridge.mm b/Source/Private/ASDisplayNode+UIViewBridge.mm index acb821801..ebc63071c 100644 --- a/Source/Private/ASDisplayNode+UIViewBridge.mm +++ b/Source/Private/ASDisplayNode+UIViewBridge.mm @@ -34,8 +34,8 @@ #define DISPLAYNODE_USE_LOCKS 1 #if DISPLAYNODE_USE_LOCKS -#define _bridge_prologue_read ASDN::MutexLocker l(__instanceLock__); ASDisplayNodeAssertThreadAffinity(self) -#define _bridge_prologue_write ASDN::MutexLocker l(__instanceLock__) +#define _bridge_prologue_read AS::MutexLocker l(__instanceLock__); ASDisplayNodeAssertThreadAffinity(self) +#define _bridge_prologue_write AS::MutexLocker l(__instanceLock__) #else #define _bridge_prologue_read ASDisplayNodeAssertThreadAffinity(self) #define _bridge_prologue_write @@ -175,7 +175,7 @@ - (void)setAlpha:(CGFloat)newAlpha - (CGFloat)cornerRadius { - ASDN::MutexLocker l(__instanceLock__); + AS::MutexLocker l(__instanceLock__); return _cornerRadius; } @@ -186,7 +186,7 @@ - (void)setCornerRadius:(CGFloat)newCornerRadius - (ASCornerRoundingType)cornerRoundingType { - ASDN::MutexLocker l(__instanceLock__); + AS::MutexLocker l(__instanceLock__); return _cornerRoundingType; } diff --git a/Source/Private/ASDisplayNodeInternal.h b/Source/Private/ASDisplayNodeInternal.h index f030db6d9..8cc49441e 100644 --- a/Source/Private/ASDisplayNodeInternal.h +++ b/Source/Private/ASDisplayNodeInternal.h @@ -79,7 +79,7 @@ AS_EXTERN NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimest @interface ASDisplayNode () <_ASTransitionContextCompletionDelegate> { @package - ASDN::RecursiveMutex __instanceLock__; + AS::RecursiveMutex __instanceLock__; _ASPendingState *_pendingViewState; ASInterfaceState _pendingInterfaceState; diff --git a/Source/Private/ASImageNode+AnimatedImagePrivate.h b/Source/Private/ASImageNode+AnimatedImagePrivate.h index 39368c71b..f20ae44b5 100644 --- a/Source/Private/ASImageNode+AnimatedImagePrivate.h +++ b/Source/Private/ASImageNode+AnimatedImagePrivate.h @@ -13,7 +13,7 @@ @interface ASImageNode () { - ASDN::Mutex _displayLinkLock; + AS::Mutex _displayLinkLock; id _animatedImage; BOOL _animatedImagePaused; NSString *_animatedImageRunLoopMode; diff --git a/Source/Private/ASLayoutTransition.mm b/Source/Private/ASLayoutTransition.mm index db9e2850a..2fea2b8ad 100644 --- a/Source/Private/ASLayoutTransition.mm +++ b/Source/Private/ASLayoutTransition.mm @@ -22,6 +22,8 @@ #import #endif +using AS::MutexLocker; + /** * Search the whole layout stack if at least one layout has a layoutElement object that can not be layed out asynchronous. * This can be the case for example if a node was already loaded @@ -52,7 +54,7 @@ static inline BOOL ASLayoutCanTransitionAsynchronous(ASLayout *layout) { } @implementation ASLayoutTransition { - std::shared_ptr __instanceLock__; + std::shared_ptr __instanceLock__; BOOL _calculatedSubnodeOperations; NSArray *_insertedSubnodes; @@ -69,7 +71,7 @@ - (instancetype)initWithNode:(ASDisplayNode *)node { self = [super init]; if (self) { - __instanceLock__ = std::make_shared(); + __instanceLock__ = std::make_shared(); _node = node; _pendingLayout = pendingLayout; @@ -80,7 +82,7 @@ - (instancetype)initWithNode:(ASDisplayNode *)node - (BOOL)isSynchronous { - ASDN::MutexLocker l(*__instanceLock__); + MutexLocker l(*__instanceLock__); return !ASLayoutCanTransitionAsynchronous(_pendingLayout.layout); } @@ -92,7 +94,7 @@ - (void)commitTransition - (void)applySubnodeInsertionsAndMoves { - ASDN::MutexLocker l(*__instanceLock__); + MutexLocker l(*__instanceLock__); [self calculateSubnodeOperationsIfNeeded]; // Create an activity even if no subnodes affected. @@ -130,7 +132,7 @@ - (void)applySubnodeInsertionsAndMoves - (void)applySubnodeRemovals { as_activity_scope(as_activity_create("Apply subnode removals", AS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT)); - ASDN::MutexLocker l(*__instanceLock__); + MutexLocker l(*__instanceLock__); [self calculateSubnodeOperationsIfNeeded]; if (_removedSubnodes.count == 0) { @@ -150,7 +152,7 @@ - (void)applySubnodeRemovals - (void)calculateSubnodeOperationsIfNeeded { - ASDN::MutexLocker l(*__instanceLock__); + MutexLocker l(*__instanceLock__); if (_calculatedSubnodeOperations) { return; } @@ -205,27 +207,27 @@ - (void)calculateSubnodeOperationsIfNeeded - (NSArray *)currentSubnodesWithTransitionContext:(_ASTransitionContext *)context { - ASDN::MutexLocker l(*__instanceLock__); + MutexLocker l(*__instanceLock__); return _node.subnodes; } - (NSArray *)insertedSubnodesWithTransitionContext:(_ASTransitionContext *)context { - ASDN::MutexLocker l(*__instanceLock__); + MutexLocker l(*__instanceLock__); [self calculateSubnodeOperationsIfNeeded]; return _insertedSubnodes; } - (NSArray *)removedSubnodesWithTransitionContext:(_ASTransitionContext *)context { - ASDN::MutexLocker l(*__instanceLock__); + MutexLocker l(*__instanceLock__); [self calculateSubnodeOperationsIfNeeded]; return _removedSubnodes; } - (ASLayout *)transitionContext:(_ASTransitionContext *)context layoutForKey:(NSString *)key { - ASDN::MutexLocker l(*__instanceLock__); + MutexLocker l(*__instanceLock__); if ([key isEqualToString:ASTransitionContextFromLayoutKey]) { return _previousLayout.layout; } else if ([key isEqualToString:ASTransitionContextToLayoutKey]) { @@ -237,7 +239,7 @@ - (ASLayout *)transitionContext:(_ASTransitionContext *)context layoutForKey:(NS - (ASSizeRange)transitionContext:(_ASTransitionContext *)context constrainedSizeForKey:(NSString *)key { - ASDN::MutexLocker l(*__instanceLock__); + MutexLocker l(*__instanceLock__); if ([key isEqualToString:ASTransitionContextFromLayoutKey]) { return _previousLayout.constrainedSize; } else if ([key isEqualToString:ASTransitionContextToLayoutKey]) { diff --git a/Source/Private/ASPendingStateController.mm b/Source/Private/ASPendingStateController.mm index 89dedfa59..269b37e94 100644 --- a/Source/Private/ASPendingStateController.mm +++ b/Source/Private/ASPendingStateController.mm @@ -14,7 +14,7 @@ @interface ASPendingStateController() { - ASDN::Mutex _lock; + AS::Mutex _lock; struct ASPendingStateControllerFlags { unsigned pendingFlush:1; @@ -52,7 +52,7 @@ + (ASPendingStateController *)sharedInstance - (void)registerNode:(ASDisplayNode *)node { ASDisplayNodeAssert(node.nodeLoaded, @"Expected display node to be loaded before it was registered with ASPendingStateController. Node: %@", node); - ASDN::MutexLocker l(_lock); + AS::MutexLocker l(_lock); [_dirtyNodes addObject:node]; [self scheduleFlushIfNeeded]; diff --git a/Source/Private/Layout/ASLayoutSpecPrivate.h b/Source/Private/Layout/ASLayoutSpecPrivate.h index 0a4a39b5d..930232096 100644 --- a/Source/Private/Layout/ASLayoutSpecPrivate.h +++ b/Source/Private/Layout/ASLayoutSpecPrivate.h @@ -19,7 +19,7 @@ NS_ASSUME_NONNULL_BEGIN @interface ASLayoutSpec() { - ASDN::RecursiveMutex __instanceLock__; + AS::RecursiveMutex __instanceLock__; std::atomic _primitiveTraitCollection; ASLayoutElementStyle *_style; NSMutableArray *_childrenArray; diff --git a/Source/Private/_ASScopeTimer.h b/Source/Private/_ASScopeTimer.h index 543e7c2f1..523599dd0 100644 --- a/Source/Private/_ASScopeTimer.h +++ b/Source/Private/_ASScopeTimer.h @@ -18,14 +18,14 @@ { // some scope - ASDisplayNode::ScopeTimer t(placeToStoreTiming); + AS::ScopeTimer t(placeToStoreTiming); DoPotentiallySlowWork(); MorePotentiallySlowWork(); } */ -namespace ASDN { +namespace AS { struct ScopeTimer { NSTimeInterval begin; NSTimeInterval &outT; diff --git a/Source/TextKit/ASTextKitContext.mm b/Source/TextKit/ASTextKitContext.mm index 75582f471..8883f3175 100644 --- a/Source/TextKit/ASTextKitContext.mm +++ b/Source/TextKit/ASTextKitContext.mm @@ -19,7 +19,7 @@ @implementation ASTextKitContext { // All TextKit operations (even non-mutative ones) must be executed serially. - std::shared_ptr __instanceLock__; + std::shared_ptr __instanceLock__; NSLayoutManager *_layoutManager; NSTextStorage *_textStorage; @@ -35,15 +35,15 @@ - (instancetype)initWithAttributedString:(NSAttributedString *)attributedString { if (self = [super init]) { static dispatch_once_t onceToken; - static ASDN::Mutex *mutex; + static AS::Mutex *mutex; dispatch_once(&onceToken, ^{ - mutex = new ASDN::Mutex(); + mutex = new AS::Mutex(); }); // Concurrently initialising TextKit components crashes (rdar://18448377) so we use a global lock. - ASDN::MutexLocker l(*mutex); + AS::MutexLocker l(*mutex); - __instanceLock__ = std::make_shared(); + __instanceLock__ = std::make_shared(); // Create the TextKit component stack with our default configuration. @@ -73,7 +73,7 @@ - (void)performBlockWithLockedTextKitComponents:(NS_NOESCAPE void (^)(NSLayoutMa NSTextStorage *, NSTextContainer *))block { - ASDN::MutexLocker l(*__instanceLock__); + AS::MutexLocker l(*__instanceLock__); if (block) { block(_layoutManager, _textStorage, _textContainer); } diff --git a/Source/TextKit/ASTextKitFontSizeAdjuster.mm b/Source/TextKit/ASTextKitFontSizeAdjuster.mm index fed70c6a4..aeea44d7c 100644 --- a/Source/TextKit/ASTextKitFontSizeAdjuster.mm +++ b/Source/TextKit/ASTextKitFontSizeAdjuster.mm @@ -33,7 +33,7 @@ @implementation ASTextKitFontSizeAdjuster ASTextKitAttributes _attributes; BOOL _measured; CGFloat _scaleFactor; - ASDN::Mutex __instanceLock__; + AS::Mutex __instanceLock__; } @synthesize sizingLayoutManager = _sizingLayoutManager; @@ -127,7 +127,7 @@ - (CGSize)boundingBoxForString:(NSAttributedString *)attributedString - (NSLayoutManager *)sizingLayoutManager { - ASDN::MutexLocker l(__instanceLock__); + AS::MutexLocker l(__instanceLock__); if (_sizingLayoutManager == nil) { _sizingLayoutManager = [[ASLayoutManager alloc] init]; _sizingLayoutManager.usesFontLeading = NO; From a1768e22bd768d687a604453a826db970dbf1a5f Mon Sep 17 00:00:00 2001 From: Greg Bolsinga Date: Sat, 9 Mar 2019 12:15:20 -0800 Subject: [PATCH 047/122] Clean up a clang analyzer cast error (#1387) ~/Texture/Source/Private/ASMutableElementMap.mm:32:24: warning: Conversion from value of type 'NSMutableArray *' to incompatible type 'ASMutableCollectionElementTwoDimensionalArray *' _sectionsOfItems = (id)ASTwoDimensionalArrayDeepMutableCopy(items); ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1 warning generated. --- Source/Private/ASMutableElementMap.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Private/ASMutableElementMap.mm b/Source/Private/ASMutableElementMap.mm index 0ef1f0170..dffe47d0f 100644 --- a/Source/Private/ASMutableElementMap.mm +++ b/Source/Private/ASMutableElementMap.mm @@ -29,7 +29,7 @@ - (instancetype)initWithSections:(NSArray *)sections items:(ASColle { if (self = [super init]) { _sections = [sections mutableCopy]; - _sectionsOfItems = (id)ASTwoDimensionalArrayDeepMutableCopy(items); + _sectionsOfItems = (ASMutableCollectionElementTwoDimensionalArray *)ASTwoDimensionalArrayDeepMutableCopy(items); _supplementaryElements = [ASMutableElementMap deepMutableCopyOfElementsDictionary:supplementaryElements]; } return self; From f67f968915cd8c5ce3104fa86b399f68239d7334 Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Mon, 11 Mar 2019 09:09:28 -0700 Subject: [PATCH 048/122] Update for 9.4.1 CI (#1392) --- build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sh b/build.sh index e2b54973f..e866f305a 100755 --- a/build.sh +++ b/build.sh @@ -1,7 +1,7 @@ #!/bin/bash PLATFORM="${TEXTURE_BUILD_PLATFORM:-platform=iOS Simulator,OS=10.2,name=iPhone 7}" -SDK="${TEXTURE_BUILD_SDK:-iphonesimulator11.0}" +SDK="${TEXTURE_BUILD_SDK:-iphonesimulator11.4}" DERIVED_DATA_PATH="~/ASDKDerivedData" # It is pitch black. From fb5820aea55d18fc9e681bf502e070fd8c2bf67f Mon Sep 17 00:00:00 2001 From: ernestmama <43187788+ernestmama@users.noreply.github.com> Date: Mon, 11 Mar 2019 13:34:35 -0700 Subject: [PATCH 049/122] Remove experimental features (exp_skip_a11y_wait && exp_new_default_cell_layout_mode) (#1383) * Remove experimental features * prevent blocking main thread * remove small content all together as none is the default * Update ASExperimentalFeatures.h --- Schemas/configuration.json | 2 -- Source/ASCollectionView.mm | 36 +++++++++++------------------- Source/ASCollectionViewProtocols.h | 6 ----- Source/ASExperimentalFeatures.h | 10 ++++----- Source/ASExperimentalFeatures.mm | 2 -- Source/ASTableView.mm | 30 +++++++++++-------------- Tests/ASCollectionViewTests.mm | 6 ----- Tests/ASConfigurationTests.mm | 4 ---- 8 files changed, 30 insertions(+), 66 deletions(-) diff --git a/Schemas/configuration.json b/Schemas/configuration.json index 3b90c1ed8..12957a12b 100644 --- a/Schemas/configuration.json +++ b/Schemas/configuration.json @@ -23,8 +23,6 @@ "exp_skip_clear_data", "exp_did_enter_preload_skip_asm_layout", "exp_disable_a11y_cache", - "exp_skip_a11y_wait", - "exp_new_default_cell_layout_mode", "exp_dispatch_apply", "exp_image_downloader_priority", "exp_text_drawing" diff --git a/Source/ASCollectionView.mm b/Source/ASCollectionView.mm index 834699ec5..4ea247095 100644 --- a/Source/ASCollectionView.mm +++ b/Source/ASCollectionView.mm @@ -318,12 +318,6 @@ - (instancetype)_initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionV [self _configureCollectionViewLayout:layout]; - if (ASActivateExperimentalFeature(ASExperimentalNewDefaultCellLayoutMode)) { - _cellLayoutMode = ASCellLayoutModeSyncForSmallContent; - } else { - _cellLayoutMode = ASCellLayoutModeNone; - } - return self; } @@ -1912,20 +1906,18 @@ - (BOOL)dataController:(ASDataController *)dataController shouldSynchronouslyPro if (ASCellLayoutModeIncludes(ASCellLayoutModeAlwaysAsync)) { return NO; } - if (ASCellLayoutModeIncludes(ASCellLayoutModeSyncForSmallContent)) { - // Reload data is expensive, don't block main while doing so. - if (changeSet.includesReloadData) { - return NO; - } - // If we have very few ASCellNodes (besides UIKit passthrough ones), match UIKit by blocking. - if (changeSet.countForAsyncLayout < 2) { - return YES; - } - CGSize contentSize = self.contentSize; - CGSize boundsSize = self.bounds.size; - if (contentSize.height <= boundsSize.height && contentSize.width <= boundsSize.width) { - return YES; - } + // Reload data is expensive, don't block main while doing so. + if (changeSet.includesReloadData) { + return NO; + } + // If we have very few ASCellNodes (besides UIKit passthrough ones), match UIKit by blocking. + if (changeSet.countForAsyncLayout < 2) { + return YES; + } + CGSize contentSize = self.contentSize; + CGSize boundsSize = self.bounds.size; + if (contentSize.height <= boundsSize.height && contentSize.width <= boundsSize.width) { + return YES; } return NO; // ASCellLayoutModeNone } @@ -2520,9 +2512,7 @@ - (void)setPrefetchingEnabled:(BOOL)prefetchingEnabled - (NSArray *)accessibilityElements { - if (!ASActivateExperimentalFeature(ASExperimentalSkipAccessibilityWait)) { - [self waitUntilAllUpdatesAreCommitted]; - } + [self waitUntilAllUpdatesAreCommitted]; return [super accessibilityElements]; } diff --git a/Source/ASCollectionViewProtocols.h b/Source/ASCollectionViewProtocols.h index 9d5a27faa..4cf6fe1e7 100644 --- a/Source/ASCollectionViewProtocols.h +++ b/Source/ASCollectionViewProtocols.h @@ -46,12 +46,6 @@ typedef NS_OPTIONS(NSUInteger, ASCellLayoutMode) { * cell height animations are desired. */ ASCellLayoutModeAlwaysBatchUpdateSectionReload = 1 << 9, // Default OFF - - /** - * If ASCellLayoutModeSyncForSmallContent is enabled it will cause ASDataController to wait on the - * background queue if the amount of new content is small. - */ - ASCellLayoutModeSyncForSmallContent = 1 << 10, }; NS_ASSUME_NONNULL_BEGIN diff --git a/Source/ASExperimentalFeatures.h b/Source/ASExperimentalFeatures.h index b15845b4e..c3d56eb7d 100644 --- a/Source/ASExperimentalFeatures.h +++ b/Source/ASExperimentalFeatures.h @@ -27,12 +27,10 @@ typedef NS_OPTIONS(NSUInteger, ASExperimentalFeatures) { ASExperimentalFramesetterCache = 1 << 6, // exp_framesetter_cache ASExperimentalSkipClearData = 1 << 7, // exp_skip_clear_data ASExperimentalDidEnterPreloadSkipASMLayout = 1 << 8, // exp_did_enter_preload_skip_asm_layout - ASExperimentalDisableAccessibilityCache = 1 << 9, // exp_disable_a11y_cache - ASExperimentalSkipAccessibilityWait = 1 << 10, // exp_skip_a11y_wait - ASExperimentalNewDefaultCellLayoutMode = 1 << 11, // exp_new_default_cell_layout_mode - ASExperimentalDispatchApply = 1 << 12, // exp_dispatch_apply - ASExperimentalImageDownloaderPriority = 1 << 13, // exp_image_downloader_priority - ASExperimentalTextDrawing = 1 << 14, // exp_text_drawing + ASExperimentalDisableAccessibilityCache = 1 << 9, // exp_disable_a11y_cache + ASExperimentalDispatchApply = 1 << 10, // exp_dispatch_apply + ASExperimentalImageDownloaderPriority = 1 << 11, // exp_image_downloader_priority + ASExperimentalTextDrawing = 1 << 12, // exp_text_drawing ASExperimentalFeatureAll = 0xFFFFFFFF }; diff --git a/Source/ASExperimentalFeatures.mm b/Source/ASExperimentalFeatures.mm index 89a876c57..653db2c37 100644 --- a/Source/ASExperimentalFeatures.mm +++ b/Source/ASExperimentalFeatures.mm @@ -22,8 +22,6 @@ @"exp_skip_clear_data", @"exp_did_enter_preload_skip_asm_layout", @"exp_disable_a11y_cache", - @"exp_skip_a11y_wait", - @"exp_new_default_cell_layout_mode", @"exp_dispatch_apply", @"exp_image_downloader_priority", @"exp_text_drawing"])); diff --git a/Source/ASTableView.mm b/Source/ASTableView.mm index af39ee11c..34b78eb64 100644 --- a/Source/ASTableView.mm +++ b/Source/ASTableView.mm @@ -1703,20 +1703,18 @@ - (BOOL)dataControllerShouldSerializeNodeCreation:(ASDataController *)dataContro - (BOOL)dataController:(ASDataController *)dataController shouldSynchronouslyProcessChangeSet:(_ASHierarchyChangeSet *)changeSet { - if (ASActivateExperimentalFeature(ASExperimentalNewDefaultCellLayoutMode)) { - // Reload data is expensive, don't block main while doing so. - if (changeSet.includesReloadData) { - return NO; - } - // For more details on this method, see the comment in the ASCollectionView implementation. - if (changeSet.countForAsyncLayout < 2) { - return YES; - } - CGSize contentSize = self.contentSize; - CGSize boundsSize = self.bounds.size; - if (contentSize.height <= boundsSize.height && contentSize.width <= boundsSize.width) { - return YES; - } + // Reload data is expensive, don't block main while doing so. + if (changeSet.includesReloadData) { + return NO; + } + // For more details on this method, see the comment in the ASCollectionView implementation. + if (changeSet.countForAsyncLayout < 2) { + return YES; + } + CGSize contentSize = self.contentSize; + CGSize boundsSize = self.bounds.size; + if (contentSize.height <= boundsSize.height && contentSize.width <= boundsSize.width) { + return YES; } return NO; } @@ -2036,9 +2034,7 @@ - (void)didMoveToSuperview - (NSArray *)accessibilityElements { - if (!ASActivateExperimentalFeature(ASExperimentalSkipAccessibilityWait)) { - [self waitUntilAllUpdatesAreCommitted]; - } + [self waitUntilAllUpdatesAreCommitted]; return [super accessibilityElements]; } diff --git a/Tests/ASCollectionViewTests.mm b/Tests/ASCollectionViewTests.mm index b1476f3aa..3ff5af440 100644 --- a/Tests/ASCollectionViewTests.mm +++ b/Tests/ASCollectionViewTests.mm @@ -1050,12 +1050,6 @@ - (void)testInitialRangeBounds shouldWaitUntilAllUpdatesAreProcessed:YES]; } -- (void)testInitialRangeBoundsCellLayoutModeSyncForSmallContent -{ - [self testInitialRangeBoundsWithCellLayoutMode:ASCellLayoutModeSyncForSmallContent - shouldWaitUntilAllUpdatesAreProcessed:YES]; // Need to wait because the first initial data load is always async -} - - (void)testInitialRangeBoundsCellLayoutModeAlwaysAsync { [self testInitialRangeBoundsWithCellLayoutMode:ASCellLayoutModeAlwaysAsync diff --git a/Tests/ASConfigurationTests.mm b/Tests/ASConfigurationTests.mm index 1a4553200..695292485 100644 --- a/Tests/ASConfigurationTests.mm +++ b/Tests/ASConfigurationTests.mm @@ -28,8 +28,6 @@ ASExperimentalSkipClearData, ASExperimentalDidEnterPreloadSkipASMLayout, ASExperimentalDisableAccessibilityCache, - ASExperimentalSkipAccessibilityWait, - ASExperimentalNewDefaultCellLayoutMode, ASExperimentalDispatchApply, ASExperimentalImageDownloaderPriority, ASExperimentalTextDrawing @@ -55,8 +53,6 @@ + (NSArray *)names { @"exp_skip_clear_data", @"exp_did_enter_preload_skip_asm_layout", @"exp_disable_a11y_cache", - @"exp_skip_a11y_wait", - @"exp_new_default_cell_layout_mode", @"exp_dispatch_apply", @"exp_image_downloader_priority", @"exp_text_drawing" From ad538983e3707546c8c5814d316936f898aeb60b Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Tue, 12 Mar 2019 07:43:38 -0700 Subject: [PATCH 050/122] Make experiment checks faster (#1393) * Make experiment checks use dispatch_once when not debugging, clean up singleton * One more * Pull out the variable --- Source/ASConfigurationInternal.h | 16 +++++++++++++++- Source/ASConfigurationInternal.mm | 29 +++++++++++++---------------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/Source/ASConfigurationInternal.h b/Source/ASConfigurationInternal.h index 3bad3fd57..eb639c224 100644 --- a/Source/ASConfigurationInternal.h +++ b/Source/ASConfigurationInternal.h @@ -20,7 +20,21 @@ NS_ASSUME_NONNULL_BEGIN * * The delegate will be notified asynchronously. */ -AS_EXTERN BOOL ASActivateExperimentalFeature(ASExperimentalFeatures option); +#if DEBUG +#define ASActivateExperimentalFeature(opt) _ASActivateExperimentalFeature(opt) +#else +#define ASActivateExperimentalFeature(opt) ({\ + static BOOL result;\ + static dispatch_once_t onceToken;\ + dispatch_once(&onceToken, ^{ result = _ASActivateExperimentalFeature(opt); });\ + result;\ +}) +#endif + +/** + * Internal function. Use the macro without the underbar. + */ +AS_EXTERN BOOL _ASActivateExperimentalFeature(ASExperimentalFeatures option); /** * Notify the configuration delegate that the framework initialized, if needed. diff --git a/Source/ASConfigurationInternal.mm b/Source/ASConfigurationInternal.mm index ec2382361..485fef0c5 100644 --- a/Source/ASConfigurationInternal.mm +++ b/Source/ASConfigurationInternal.mm @@ -12,7 +12,15 @@ #import #import -#define ASGetSharedConfigMgr() (__bridge ASConfigurationManager *)ASConfigurationManager.sharedInstance +static ASConfigurationManager *ASSharedConfigurationManager; +static dispatch_once_t ASSharedConfigurationManagerOnceToken; + +NS_INLINE ASConfigurationManager *ASConfigurationManagerGet() { + dispatch_once(&ASSharedConfigurationManagerOnceToken, ^{ + ASSharedConfigurationManager = [[ASConfigurationManager alloc] init]; + }); + return ASSharedConfigurationManager; +} @implementation ASConfigurationManager { ASConfiguration *_config; @@ -21,17 +29,6 @@ @implementation ASConfigurationManager { _Atomic(ASExperimentalFeatures) _activatedExperiments; } -/// Return CFTypeRef to avoid retain/release on this singleton. -+ (CFTypeRef)sharedInstance -{ - static CFTypeRef inst; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - inst = (__bridge_retained CFTypeRef)[[ASConfigurationManager alloc] init]; - }); - return inst; -} - + (ASConfiguration *)defaultConfiguration NS_RETURNS_RETAINED { ASConfiguration *config = [[ASConfiguration alloc] init]; @@ -96,19 +93,19 @@ - (BOOL)activateExperimentalFeature:(ASExperimentalFeatures)requested // Define this even when !DEBUG, since we may run our tests in release mode. + (void)test_resetWithConfiguration:(ASConfiguration *)configuration { - ASConfigurationManager *inst = ASGetSharedConfigMgr(); + ASConfigurationManager *inst = ASConfigurationManagerGet(); inst->_config = configuration ?: [self defaultConfiguration]; atomic_store(&inst->_activatedExperiments, 0); } @end -BOOL ASActivateExperimentalFeature(ASExperimentalFeatures feature) +BOOL _ASActivateExperimentalFeature(ASExperimentalFeatures feature) { - return [ASGetSharedConfigMgr() activateExperimentalFeature:feature]; + return [ASConfigurationManagerGet() activateExperimentalFeature:feature]; } void ASNotifyInitialized() { - [ASGetSharedConfigMgr() frameworkDidInitialize]; + [ASConfigurationManagerGet() frameworkDidInitialize]; } From 79323c11eb5537bf0f397fa70a0f773852d5ffa2 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Tue, 12 Mar 2019 08:22:21 -0700 Subject: [PATCH 051/122] Make shared CA transaction queue variable extern so it's actually shared (#1397) --- Source/ASRunLoopQueue.h | 11 ++++++----- Source/ASRunLoopQueue.mm | 3 +++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Source/ASRunLoopQueue.h b/Source/ASRunLoopQueue.h index fca77c0f4..07f3682bb 100644 --- a/Source/ASRunLoopQueue.h +++ b/Source/ASRunLoopQueue.h @@ -68,13 +68,14 @@ AS_SUBCLASSING_RESTRICTED @end +extern ASCATransactionQueue *_ASSharedCATransactionQueue; +extern dispatch_once_t _ASSharedCATransactionQueueOnceToken; + NS_INLINE ASCATransactionQueue *ASCATransactionQueueGet(void) { - static ASCATransactionQueue *q; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - q = [[ASCATransactionQueue alloc] init]; + dispatch_once(&_ASSharedCATransactionQueueOnceToken, ^{ + _ASSharedCATransactionQueue = [[ASCATransactionQueue alloc] init]; }); - return q; + return _ASSharedCATransactionQueue; } @interface ASDeallocQueue : NSObject diff --git a/Source/ASRunLoopQueue.mm b/Source/ASRunLoopQueue.mm index c85dccb92..4aefeeb89 100644 --- a/Source/ASRunLoopQueue.mm +++ b/Source/ASRunLoopQueue.mm @@ -354,6 +354,9 @@ @implementation ASCATransactionQueue // but after most other scheduled work on the runloop has processed. static int const kASASCATransactionQueueOrder = 1000000; +ASCATransactionQueue *_ASSharedCATransactionQueue; +dispatch_once_t _ASSharedCATransactionQueueOnceToken; + - (instancetype)init { if (self = [super init]) { From d5339ac43957d6cc2e26b65d92ed21f87c1d6bdc Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Tue, 12 Mar 2019 14:34:58 -0700 Subject: [PATCH 052/122] Fix header typo (#1402) --- Source/ASDisplayNode.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/ASDisplayNode.h b/Source/ASDisplayNode.h index d3414b894..cedb7364d 100644 --- a/Source/ASDisplayNode.h +++ b/Source/ASDisplayNode.h @@ -648,7 +648,7 @@ AS_EXTERN NSInteger const ASDefaultDrawingPriority; * more efficient than CALayer. The only limitation of this approach is that it cannot clip children, and * thus works best for ASImageNodes or containers showing a background around their children. * - * - ASCornerRoundingTypeClipping: overlays 4 seperate opaque corners on top of the content that needs + * - ASCornerRoundingTypeClipping: overlays 4 separate opaque corners on top of the content that needs * corner rounding. Requires .backgroundColor and .cornerRadius to be set. Use clip corners in situations * in which is movement through the corner, with an opaque background (no movement underneath the corner). * Clipped corners are ideal for animating / resizing views, and still outperform CALayer. From 902131a2a6a38f93be0d4b98a0ab35965a8aa883 Mon Sep 17 00:00:00 2001 From: Garrett Moon Date: Tue, 12 Mar 2019 15:32:58 -0700 Subject: [PATCH 053/122] Context is a pretty general name that will cause conflicts with subclasses (#1399) --- Source/ASDisplayNode.h | 6 +++--- .../CatDealsCollectionView/Sample/ASDisplayNode+CatDeals.mm | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Source/ASDisplayNode.h b/Source/ASDisplayNode.h index cedb7364d..59fe93678 100644 --- a/Source/ASDisplayNode.h +++ b/Source/ASDisplayNode.h @@ -97,11 +97,11 @@ AS_EXTERN NSInteger const ASDefaultDrawingPriority; @interface ASDisplayNode : NSObject { @public /** - * The _context ivar is unused by Texture, but provided to enable advanced clients to make powerful extensions to base class functionality. - * For example, _context can be used to implement category methods on ASDisplayNode that add functionality to all node subclass types. + * The _displayNodeContext ivar is unused by Texture, but provided to enable advanced clients to make powerful extensions to base class functionality. + * For example, _displayNodeContext can be used to implement category methods on ASDisplayNode that add functionality to all node subclass types. * Code demonstrating this technique can be found in the CatDealsCollectionView example. */ - void *_context; + void *_displayNodeContext; } /** @name Initializing a node object */ diff --git a/examples/CatDealsCollectionView/Sample/ASDisplayNode+CatDeals.mm b/examples/CatDealsCollectionView/Sample/ASDisplayNode+CatDeals.mm index 76d9eb277..f7154ae5e 100644 --- a/examples/CatDealsCollectionView/Sample/ASDisplayNode+CatDeals.mm +++ b/examples/CatDealsCollectionView/Sample/ASDisplayNode+CatDeals.mm @@ -14,16 +14,16 @@ NSString *loggingID = nil; }; -// Convenience to cast _context into our struct reference. +// Convenience to cast _displayNodeContext into our struct reference. NS_INLINE CatDealsNodeContext &GetNodeContext(ASDisplayNode *node) { - return *static_cast(node->_context); + return *static_cast(node->_displayNodeContext); } @implementation ASDisplayNode (CatDeals) - (void)baseDidInit { - _context = new CatDealsNodeContext; + _displayNodeContext = new CatDealsNodeContext; } - (void)baseWillDealloc From 88291f27e8532969a5bc2168ffba01555dfdc72a Mon Sep 17 00:00:00 2001 From: Greg Bolsinga Date: Wed, 13 Mar 2019 10:56:11 -0700 Subject: [PATCH 054/122] Suppress documentation warnings when using external libraries (#1401) Fixes 1400 --- Tests/ASSnapshotTestCase.h | 4 ++++ Tests/ASTableViewTests.mm | 3 +++ Tests/ASTextKitTests.mm | 3 +++ 3 files changed, 10 insertions(+) diff --git a/Tests/ASSnapshotTestCase.h b/Tests/ASSnapshotTestCase.h index 0741aaab5..195e4b791 100644 --- a/Tests/ASSnapshotTestCase.h +++ b/Tests/ASSnapshotTestCase.h @@ -7,7 +7,11 @@ // Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 // +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdocumentation" #import +#pragma clang diagnostic pop + #import "ASDisplayNodeTestsHelper.h" @class ASDisplayNode; diff --git a/Tests/ASTableViewTests.mm b/Tests/ASTableViewTests.mm index 87e19329a..7295b20ba 100644 --- a/Tests/ASTableViewTests.mm +++ b/Tests/ASTableViewTests.mm @@ -8,7 +8,10 @@ // #import +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdocumentation" #import +#pragma clang diagnostic pop #import #import diff --git a/Tests/ASTextKitTests.mm b/Tests/ASTextKitTests.mm index 0d807824e..efd93cf80 100644 --- a/Tests/ASTextKitTests.mm +++ b/Tests/ASTextKitTests.mm @@ -10,7 +10,10 @@ #import #import +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdocumentation" #import +#pragma clang diagnostic pop #import From 45d90501c6709091230ecad184eac0aeaeb17b22 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Wed, 13 Mar 2019 11:58:08 -0700 Subject: [PATCH 055/122] Add layer-action support to nodes (#1396) * Add layer-action support to nodes, unify hierarchy notifications on it * Better pending state * Fix bool * Skip extra copy * Never run default actions * Continue the search --- Source/ASDisplayNode+Subclasses.h | 7 ++ Source/ASDisplayNode.h | 2 + Source/ASDisplayNode.mm | 19 ++--- Source/Details/UIView+ASConvenience.h | 1 + Source/Details/_ASDisplayLayer.mm | 7 ++ Source/Details/_ASDisplayView.mm | 23 +++--- Source/Private/ASDisplayNode+UIViewBridge.mm | 12 +++ Source/Private/ASDisplayNodeInternal.h | 2 +- Source/Private/_ASPendingState.mm | 86 +++++--------------- Tests/ASDisplayNodeTests.mm | 14 ++++ 10 files changed, 83 insertions(+), 90 deletions(-) diff --git a/Source/ASDisplayNode+Subclasses.h b/Source/ASDisplayNode+Subclasses.h index 2b80f2b5c..df84b577a 100644 --- a/Source/ASDisplayNode+Subclasses.h +++ b/Source/ASDisplayNode+Subclasses.h @@ -376,6 +376,13 @@ AS_CATEGORY_IMPLEMENTABLE */ @property (readonly) CGFloat contentsScaleForDisplay; +/** + * Called as part of actionForLayer:forKey:. Gives the node a chance to provide a custom action for its layer. + * + * The default implementation returns NSNull, indicating that no action should be taken. + */ +AS_CATEGORY_IMPLEMENTABLE +- (nullable id)layerActionForKey:(NSString *)event; #pragma mark - Touch handling /** @name Touch handling */ diff --git a/Source/ASDisplayNode.h b/Source/ASDisplayNode.h index 59fe93678..ce54e0a51 100644 --- a/Source/ASDisplayNode.h +++ b/Source/ASDisplayNode.h @@ -686,6 +686,8 @@ AS_EXTERN NSInteger const ASDefaultDrawingPriority; @property (getter=isExclusiveTouch) BOOL exclusiveTouch; // default=NO #endif +@property (nullable, copy) NSDictionary> *actions; // default = nil + /** * @abstract The node view's background color. * diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index b4034f291..fc541178f 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -64,11 +64,7 @@ static ASDisplayNodeNonFatalErrorBlock _nonFatalErrorBlock = nil; -// Forward declare CALayerDelegate protocol as the iOS 10 SDK moves CALayerDelegate from an informal delegate to a protocol. -// We have to forward declare the protocol as this place otherwise it will not compile compiling with an Base SDK < iOS 10 -@protocol CALayerDelegate; - -@interface ASDisplayNode () +@interface ASDisplayNode () /** * See ASDisplayNodeInternal.h for ivars */ @@ -107,9 +103,10 @@ BOOL ASDisplayNodeNeedsSpecialPropertiesHandling(BOOL isSynchronous, BOOL isLaye return result; } -void StubImplementationWithNoArgs(id receiver) {} -void StubImplementationWithSizeRange(id receiver, ASSizeRange sr) {} -void StubImplementationWithTwoInterfaceStates(id receiver, ASInterfaceState s0, ASInterfaceState s1) {} +void StubImplementationWithNoArgs(id receiver, SEL _cmd) {} +void StubImplementationWithSizeRange(id receiver, SEL _cmd, ASSizeRange sr) {} +void StubImplementationWithTwoInterfaceStates(id receiver, SEL _cmd, ASInterfaceState s0, ASInterfaceState s1) {} +id StubLayerActionImplementation(id receiver, SEL _cmd, NSString *key) { return (id)kCFNull; } /** * Returns ASDisplayNodeFlags for the given class/instance. instance MAY BE NIL. @@ -281,6 +278,8 @@ + (void)initialize auto interfaceStateType = std::string(@encode(ASInterfaceState)); auto type1 = "v@:" + interfaceStateType + interfaceStateType; class_addMethod(self, @selector(interfaceStateDidChange:fromState:), (IMP)StubImplementationWithTwoInterfaceStates, type1.c_str()); + + class_addMethod(self, @selector(layerActionForKey:), (IMP)StubLayerActionImplementation, "@@:@"); } } @@ -1804,7 +1803,6 @@ - (void)subnodeDisplayDidFinish:(ASDisplayNode *)subnode #pragma mark -// We are only the delegate for the layer when we are layer-backed, as UIView performs this function normally - (id)actionForLayer:(CALayer *)layer forKey:(NSString *)event { if (event == kCAOnOrderIn) { @@ -1813,8 +1811,7 @@ - (void)subnodeDisplayDidFinish:(ASDisplayNode *)subnode [self __exitHierarchy]; } - ASDisplayNodeAssert(_flags.layerBacked, @"We shouldn't get called back here unless we are layer-backed."); - return (id)kCFNull; + return [self layerActionForKey:event]; } #pragma mark - Error Handling diff --git a/Source/Details/UIView+ASConvenience.h b/Source/Details/UIView+ASConvenience.h index 30d58a07a..8c4b2f55c 100644 --- a/Source/Details/UIView+ASConvenience.h +++ b/Source/Details/UIView+ASConvenience.h @@ -42,6 +42,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) BOOL allowsGroupOpacity; @property (nonatomic) BOOL allowsEdgeAntialiasing; @property (nonatomic) unsigned int edgeAntialiasingMask; +@property (nonatomic, nullable, copy) NSDictionary> *actions; - (void)setNeedsDisplay; - (void)setNeedsLayout; diff --git a/Source/Details/_ASDisplayLayer.mm b/Source/Details/_ASDisplayLayer.mm index 96eda9e1f..6ced31685 100644 --- a/Source/Details/_ASDisplayLayer.mm +++ b/Source/Details/_ASDisplayLayer.mm @@ -115,6 +115,13 @@ - (void)setNeedsDisplay #pragma mark - ++ (id)defaultActionForKey:(NSString *)event +{ + // We never want to run one of CA's root default actions. So if we return nil from actionForLayer:forKey:, and let CA + // dig into the actions dictionary, and it doesn't find it there, it will check here and we need to stop the search. + return (id)kCFNull; +} + + (dispatch_queue_t)displayQueue { static dispatch_queue_t displayQueue = NULL; diff --git a/Source/Details/_ASDisplayView.mm b/Source/Details/_ASDisplayView.mm index 0bf1514e3..9d0af059c 100644 --- a/Source/Details/_ASDisplayView.mm +++ b/Source/Details/_ASDisplayView.mm @@ -153,22 +153,19 @@ - (NSString *)description #pragma mark - UIView Overrides -- (void)willMoveToWindow:(UIWindow *)newWindow +- (id)actionForLayer:(CALayer *)layer forKey:(NSString *)event { - ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. - BOOL visible = (newWindow != nil); - if (visible && !node.inHierarchy) { - [node __enterHierarchy]; - } -} + id uikitAction = [super actionForLayer:layer forKey:event]; -- (void)didMoveToWindow -{ - ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. - BOOL visible = (self.window != nil); - if (!visible && node.inHierarchy) { - [node __exitHierarchy]; + // Even though the UIKit action will take precedence, we still unconditionally forward to the node so that it can + // track events like kCAOnOrderIn. + id nodeAction = [_asyncdisplaykit_node actionForLayer:layer forKey:event]; + + // If UIKit specifies an action, that takes precedence. That's an animation block so it's explicit. + if (uikitAction && uikitAction != (id)kCFNull) { + return uikitAction; } + return nodeAction; } - (void)willMoveToSuperview:(UIView *)newSuperview diff --git a/Source/Private/ASDisplayNode+UIViewBridge.mm b/Source/Private/ASDisplayNode+UIViewBridge.mm index ebc63071c..4f7e93140 100644 --- a/Source/Private/ASDisplayNode+UIViewBridge.mm +++ b/Source/Private/ASDisplayNode+UIViewBridge.mm @@ -944,6 +944,18 @@ - (void)setInsetsLayoutMarginsFromSafeArea:(BOOL)insetsLayoutMarginsFromSafeArea } } +- (NSDictionary> *)actions +{ + _bridge_prologue_read; + return _getFromLayer(actions); +} + +- (void)setActions:(NSDictionary> *)actions +{ + _bridge_prologue_write; + _setToLayer(actions, actions); +} + - (void)safeAreaInsetsDidChange { ASDisplayNodeAssertMainThread(); diff --git a/Source/Private/ASDisplayNodeInternal.h b/Source/Private/ASDisplayNodeInternal.h index 8cc49441e..55cef7579 100644 --- a/Source/Private/ASDisplayNodeInternal.h +++ b/Source/Private/ASDisplayNodeInternal.h @@ -76,7 +76,7 @@ AS_EXTERN NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimest #define NUM_CLIP_CORNER_LAYERS 4 -@interface ASDisplayNode () <_ASTransitionContextCompletionDelegate> +@interface ASDisplayNode () <_ASTransitionContextCompletionDelegate, CALayerDelegate> { @package AS::RecursiveMutex __instanceLock__; diff --git a/Source/Private/_ASPendingState.mm b/Source/Private/_ASPendingState.mm index 784d43550..5dca0ce5c 100644 --- a/Source/Private/_ASPendingState.mm +++ b/Source/Private/_ASPendingState.mm @@ -86,8 +86,12 @@ int setLayoutMargins:1; int setPreservesSuperviewLayoutMargins:1; int setInsetsLayoutMarginsFromSafeArea:1; + int setActions:1; } ASPendingStateFlags; + +static constexpr ASPendingStateFlags kZeroFlags = {0}; + @implementation _ASPendingState { @package //Expose all ivars for ASDisplayNode to bypass getters for efficiency @@ -140,6 +144,7 @@ @implementation _ASPendingState CGPoint accessibilityActivationPoint; UIBezierPath *accessibilityPath; UISemanticContentAttribute semanticContentAttribute API_AVAILABLE(ios(9.0), tvos(9.0)); + NSDictionary> *actions; ASPendingStateFlags _flags; } @@ -209,6 +214,7 @@ ASDISPLAYNODE_INLINE void ASPendingStateApplyMetricsToLayer(_ASPendingState *sta @synthesize layoutMargins=layoutMargins; @synthesize preservesSuperviewLayoutMargins=preservesSuperviewLayoutMargins; @synthesize insetsLayoutMarginsFromSafeArea=insetsLayoutMarginsFromSafeArea; +@synthesize actions=actions; static CGColorRef blackColorRef = NULL; static UIColor *defaultTintColor = nil; @@ -586,6 +592,12 @@ - (void)setSemanticContentAttribute:(UISemanticContentAttribute)attribute API_AV _flags.setSemanticContentAttribute = YES; } +- (void)setActions:(NSDictionary> *)actionsArg +{ + actions = [actionsArg copy]; + _flags.setActions = YES; +} + - (BOOL)isAccessibilityElement { return isAccessibilityElement; @@ -917,6 +929,9 @@ - (void)applyToLayer:(CALayer *)layer if (flags.setOpaque) ASDisplayNodeAssert(layer.opaque == opaque, @"Didn't set opaque as desired"); + if (flags.setActions) + layer.actions = actions; + ASPendingStateApplyMetricsToLayer(self, layer); if (flags.needsLayout) @@ -936,7 +951,7 @@ - (void)applyToView:(UIView *)view withSpecialPropertiesHandling:(BOOL)specialPr because a different setter would be called. */ - CALayer *layer = view.layer; + unowned CALayer *layer = view.layer; ASPendingStateFlags flags = _flags; if (__shouldSetNeedsDisplay(layer)) { @@ -979,6 +994,9 @@ - (void)applyToView:(UIView *)view withSpecialPropertiesHandling:(BOOL)specialPr if (flags.setRasterizationScale) layer.rasterizationScale = rasterizationScale; + if (flags.setActions) + layer.actions = actions; + if (flags.setClipsToBounds) view.clipsToBounds = clipsToBounds; @@ -1272,7 +1290,7 @@ + (_ASPendingState *)pendingViewStateFromView:(UIView *)view - (void)clearChanges { - _flags = (ASPendingStateFlags){ 0 }; + _flags = kZeroFlags; } - (BOOL)hasSetNeedsLayout @@ -1287,69 +1305,7 @@ - (BOOL)hasSetNeedsDisplay - (BOOL)hasChanges { - ASPendingStateFlags flags = _flags; - - return (flags.setAnchorPoint - || flags.setPosition - || flags.setZPosition - || flags.setFrame - || flags.setBounds - || flags.setPosition - || flags.setTransform - || flags.setSublayerTransform - || flags.setContents - || flags.setContentsGravity - || flags.setContentsRect - || flags.setContentsCenter - || flags.setContentsScale - || flags.setRasterizationScale - || flags.setClipsToBounds - || flags.setBackgroundColor - || flags.setTintColor - || flags.setHidden - || flags.setAlpha - || flags.setCornerRadius - || flags.setContentMode - || flags.setUserInteractionEnabled - || flags.setExclusiveTouch - || flags.setShadowOpacity - || flags.setShadowOffset - || flags.setShadowRadius - || flags.setShadowColor - || flags.setBorderWidth - || flags.setBorderColor - || flags.setAutoresizingMask - || flags.setAutoresizesSubviews - || flags.setNeedsDisplayOnBoundsChange - || flags.setAllowsGroupOpacity - || flags.setAllowsEdgeAntialiasing - || flags.setEdgeAntialiasingMask - || flags.needsDisplay - || flags.needsLayout - || flags.setAsyncTransactionContainer - || flags.setOpaque - || flags.setSemanticContentAttribute - || flags.setLayoutMargins - || flags.setPreservesSuperviewLayoutMargins - || flags.setInsetsLayoutMarginsFromSafeArea - || flags.setIsAccessibilityElement - || flags.setAccessibilityLabel - || flags.setAccessibilityAttributedLabel - || flags.setAccessibilityHint - || flags.setAccessibilityAttributedHint - || flags.setAccessibilityValue - || flags.setAccessibilityAttributedValue - || flags.setAccessibilityTraits - || flags.setAccessibilityFrame - || flags.setAccessibilityLanguage - || flags.setAccessibilityElementsHidden - || flags.setAccessibilityViewIsModal - || flags.setShouldGroupAccessibilityChildren - || flags.setAccessibilityIdentifier - || flags.setAccessibilityNavigationStyle - || flags.setAccessibilityHeaderElements - || flags.setAccessibilityActivationPoint - || flags.setAccessibilityPath); + return memcmp(&_flags, &kZeroFlags, sizeof(ASPendingStateFlags)); } - (void)dealloc diff --git a/Tests/ASDisplayNodeTests.mm b/Tests/ASDisplayNodeTests.mm index f9ae561cb..e47646e33 100644 --- a/Tests/ASDisplayNodeTests.mm +++ b/Tests/ASDisplayNodeTests.mm @@ -9,6 +9,7 @@ #import #import +#import #import #import @@ -2697,4 +2698,17 @@ - (void)testCornerRoundingTypeClippingRoundedCornersIsUsingASDisplayNodeCornerLa } } +- (void)testLayerActionForKeyIsCalled +{ + UIWindow *window = [[UIWindow alloc] init]; + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + + id mockNode = OCMPartialMock(node); + OCMExpect([mockNode layerActionForKey:kCAOnOrderIn]); + [window.layer addSublayer:node.layer]; + OCMExpect([mockNode layerActionForKey:@"position"]); + node.layer.position = CGPointMake(10, 10); + OCMVerifyAll(mockNode); +} + @end From 602a1b2ad8e90934d58dd0c614d72e31061db551 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Sat, 16 Mar 2019 09:34:39 -0700 Subject: [PATCH 056/122] Avoid an unnecessary lock & unlock pair in ASMainSerialQueue (#1409) --- Source/Details/ASMainSerialQueue.mm | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Source/Details/ASMainSerialQueue.mm b/Source/Details/ASMainSerialQueue.mm index 06cb7c3b9..8b77c654b 100644 --- a/Source/Details/ASMainSerialQueue.mm +++ b/Source/Details/ASMainSerialQueue.mm @@ -40,14 +40,12 @@ - (NSUInteger)numberOfScheduledBlocks - (void)performBlockOnMainThread:(dispatch_block_t)block { - - AS::UniqueLock l(_serialQueueLock); - [_blocks addObject:block]; { - l.unlock(); - [self runBlocks]; - l.lock(); + AS::MutexLocker l(_serialQueueLock); + [_blocks addObject:block]; } + + [self runBlocks]; } - (void)runBlocks From ad0d4d6441ade740ae90b4fd1b0d88e9247a733f Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Sat, 16 Mar 2019 16:37:46 -0700 Subject: [PATCH 057/122] Add clang-format for a common source code format (#1365) * Add clang-format * Update format to be as close as our current style --- .clang-format | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++ format | 42 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 .clang-format create mode 100755 format diff --git a/.clang-format b/.clang-format new file mode 100644 index 000000000..5cb57c5bd --- /dev/null +++ b/.clang-format @@ -0,0 +1,53 @@ +BasedOnStyle: Google + +## Indentation +ColumnLimit: 120 +IndentWidth: 2 + +## Line Break +BreakBeforeBraces: Custom +BraceWrapping: + AfterClass: true + AfterControlStatement: false + AfterEnum: false + AfterFunction: true + AfterNamespace: true + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: true + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: false + +## ObjC +ObjCBinPackProtocolList: Always +ObjCSpaceAfterProperty: false + +## Comments +AlignTrailingComments: true + +## Arguments / Parameters + +# If false, a function call’s or function definition’s arguments will either +# all be on the same line or will have one line each. +BinPackArguments: false + +# If false, a function call’s or function definition’s parameters will either +# all be on the same line or will have one line each. +BinPackParameters: false + +# Allow putting all parameters of a function declaration onto the next line even +# if BinPackParameters is false. +# For example this should be ok: +# someFunction(foo, +# bar, +# baz); +AllowAllParametersOfDeclarationOnNextLine: true + +## Single Line +AllowShortBlocksOnASingleLine: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false + diff --git a/format b/format new file mode 100755 index 000000000..23f50803e --- /dev/null +++ b/format @@ -0,0 +1,42 @@ +#!/bin/bash +# +# Script uses clang-format to format all of the source files in a +# default format. +# + +# The absolute path of the directory containing this script. +DIR="$( cd "$( dirname "$0" )" && pwd)" +# Define top level project directory +PROJECT_DIR="${DIR}" + +METHOD="GIT" +if [ -n "$1" ]; then + METHOD=$1 +fi + +# Paths to format +MONITOR=() +MONITOR+=( "${PROJECT_DIR}/Source" ) + +if [ "$METHOD" == "ALL" ]; then + # Format all Source files + for FILE in $(find ${MONITOR[*]} -type f \( -iname "*.mm" -o -iname "*.m" -o -iname "*.h" \)) + do + clang-format -style=file -i "$FILE" & + done +elif [ "$METHOD" == "GIT" ]; then + # Gather all added or modified files from git and format them + CHANGED_FILES=$(git ls-files --other --modified --exclude-standard | grep ".*[\.m|\.mm|\.h|\.hpp]$") + for CHANGED_FILE in $CHANGED_FILES + do + clang-format -style=file -i "${PROJECT_DIR}/$CHANGED_FILE" & + done +else + echo "Usage: $0 [GIT|ALL]"; + echo "GIT: Only format added or modified files" + echo "ALL: Format all files" +fi + + +# Wait until all clang-format invoctations are done +wait \ No newline at end of file From 44846e88bb85c9f49fd15230301b0f284831fd3a Mon Sep 17 00:00:00 2001 From: Andrew Yates Date: Mon, 18 Mar 2019 13:32:01 -0700 Subject: [PATCH 058/122] Fix GIF Caching (#1405) --- Source/ASNetworkImageNode.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/ASNetworkImageNode.mm b/Source/ASNetworkImageNode.mm index e7910cd78..5bd87f62c 100644 --- a/Source/ASNetworkImageNode.mm +++ b/Source/ASNetworkImageNode.mm @@ -821,7 +821,7 @@ - (void)_lazilyLoadImageIfNecessary return; } - if ([imageContainer asdk_image] == nil && self->_downloader != nil) { + if ([imageContainer asdk_image] == nil && [imageContainer asdk_animatedImageData] == nil && self->_downloader != nil) { if (delegateWillLoadImageFromNetwork) { [delegate imageNodeWillLoadImageFromNetwork:self]; } From 1bbddabbfe8a2fb557ac321a197302c125815a80 Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 20 Mar 2019 15:09:21 -0700 Subject: [PATCH 059/122] Add support for reverse direction to yoga layouts (#1413) * Add support for reverse direction to yoga layouts * #ifdef YOGA yoga-specific additions * oops --- Source/Layout/ASStackLayoutDefines.h | 6 ++++++ Source/Layout/ASStackLayoutSpec.mm | 7 +++++++ Source/Layout/ASYogaUtilities.mm | 11 ++++++++++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/Source/Layout/ASStackLayoutDefines.h b/Source/Layout/ASStackLayoutDefines.h index 8a86be1b5..cd408af1f 100644 --- a/Source/Layout/ASStackLayoutDefines.h +++ b/Source/Layout/ASStackLayoutDefines.h @@ -15,6 +15,12 @@ typedef NS_ENUM(NSUInteger, ASStackLayoutDirection) { ASStackLayoutDirectionVertical, /** Children are stacked horizontally */ ASStackLayoutDirectionHorizontal, +#if YOGA + /** Children are stacked vertically, but in reverse. Only used by Yoga spec. */ + ASStackLayoutDirectionVerticalReverse, + /** Children are stacked horizontally, but in reverse. Only used by Yoga spec. */ + ASStackLayoutDirectionHorizontalReverse, +#endif }; /** If no children are flexible, how should this spec justify its children in the available space? */ diff --git a/Source/Layout/ASStackLayoutSpec.mm b/Source/Layout/ASStackLayoutSpec.mm index c7c9f8dd2..dffcbfb54 100644 --- a/Source/Layout/ASStackLayoutSpec.mm +++ b/Source/Layout/ASStackLayoutSpec.mm @@ -193,6 +193,13 @@ - (void)resolveVerticalAlignment case ASStackLayoutDirectionHorizontal: [result insertObject:@{ (id)kCFNull: @"horizontal" } atIndex:0]; break; +#if YOGA + case ASStackLayoutDirectionVerticalReverse: + case ASStackLayoutDirectionHorizontalReverse: + // Currently not handled. + ASDisplayNodeFailAssert(@"Reverse directions not implemented."); + break; +#endif } return result; diff --git a/Source/Layout/ASYogaUtilities.mm b/Source/Layout/ASYogaUtilities.mm index f0602ed4b..2411c06bf 100644 --- a/Source/Layout/ASYogaUtilities.mm +++ b/Source/Layout/ASYogaUtilities.mm @@ -96,7 +96,16 @@ YGAlign yogaAlignSelf(ASStackLayoutAlignSelf alignSelf) YGFlexDirection yogaFlexDirection(ASStackLayoutDirection direction) { - return direction == ASStackLayoutDirectionVertical ? YGFlexDirectionColumn : YGFlexDirectionRow; + switch (direction) { + case ASStackLayoutDirectionVertical: + return YGFlexDirectionColumn; + case ASStackLayoutDirectionVerticalReverse: + return YGFlexDirectionColumnReverse; + case ASStackLayoutDirectionHorizontal: + return YGFlexDirectionRow; + case ASStackLayoutDirectionHorizontalReverse: + return YGFlexDirectionRowReverse; + } } float yogaFloatForCGFloat(CGFloat value) From 66532c6472e4a349b477a7135fcd5ab7c6be0d14 Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Thu, 21 Mar 2019 07:41:02 -0700 Subject: [PATCH 060/122] Only set ASLayoutElementStyle delegate if Yoga is enabled (#1417) --- Source/ASDisplayNode+Layout.mm | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Source/ASDisplayNode+Layout.mm b/Source/ASDisplayNode+Layout.mm index 8800aa6fc..42f0f0d6d 100644 --- a/Source/ASDisplayNode+Layout.mm +++ b/Source/ASDisplayNode+Layout.mm @@ -61,7 +61,12 @@ - (ASLayoutElementStyle *)_locked_style { ASAssertLocked(__instanceLock__); if (_style == nil) { +#if YOGA + // In Yoga mode we use the delegate to inform the tree if properties changes _style = [[ASLayoutElementStyle alloc] initWithDelegate:self]; +#else + _style = [[ASLayoutElementStyle alloc] init]; +#endif } return _style; } From 4766a0b7f7a2766a65bfa3f1dac1ef772e550664 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Fri, 22 Mar 2019 06:40:18 -0700 Subject: [PATCH 061/122] Fix bug in ASRangeController that causes some cell nodes of a collection view which is about to becomes invisible to load their backing view/layer and render (#1418) --- Schemas/configuration.json | 3 ++- Source/ASExperimentalFeatures.h | 1 + Source/ASExperimentalFeatures.mm | 3 ++- Source/Details/ASRangeController.mm | 28 ++++++++++++++++------------ Tests/ASConfigurationTests.mm | 6 ++++-- 5 files changed, 25 insertions(+), 16 deletions(-) diff --git a/Schemas/configuration.json b/Schemas/configuration.json index 12957a12b..11aeca211 100644 --- a/Schemas/configuration.json +++ b/Schemas/configuration.json @@ -25,7 +25,8 @@ "exp_disable_a11y_cache", "exp_dispatch_apply", "exp_image_downloader_priority", - "exp_text_drawing" + "exp_text_drawing", + "exp_fix_range_controller" ] } } diff --git a/Source/ASExperimentalFeatures.h b/Source/ASExperimentalFeatures.h index c3d56eb7d..287c910bf 100644 --- a/Source/ASExperimentalFeatures.h +++ b/Source/ASExperimentalFeatures.h @@ -31,6 +31,7 @@ typedef NS_OPTIONS(NSUInteger, ASExperimentalFeatures) { ASExperimentalDispatchApply = 1 << 10, // exp_dispatch_apply ASExperimentalImageDownloaderPriority = 1 << 11, // exp_image_downloader_priority ASExperimentalTextDrawing = 1 << 12, // exp_text_drawing + ASExperimentalFixRangeController = 1 << 13, // exp_fix_range_controller ASExperimentalFeatureAll = 0xFFFFFFFF }; diff --git a/Source/ASExperimentalFeatures.mm b/Source/ASExperimentalFeatures.mm index 653db2c37..4dfe5ddde 100644 --- a/Source/ASExperimentalFeatures.mm +++ b/Source/ASExperimentalFeatures.mm @@ -24,7 +24,8 @@ @"exp_disable_a11y_cache", @"exp_dispatch_apply", @"exp_image_downloader_priority", - @"exp_text_drawing"])); + @"exp_text_drawing", + @"exp_fix_range_controller"])); if (flags == ASExperimentalFeatureAll) { return allNames; } diff --git a/Source/Details/ASRangeController.mm b/Source/Details/ASRangeController.mm index d16b281d5..5625fd59e 100644 --- a/Source/Details/ASRangeController.mm +++ b/Source/Details/ASRangeController.mm @@ -349,21 +349,25 @@ - (void)_updateVisibleNodeIndexPaths } } } else { - // If selfInterfaceState isn't visible, then visibleIndexPaths represents what /will/ be immediately visible at the - // instant we come onscreen. So, preload and display all of those things, but don't waste resources preloading yet. - // We handle this as a separate case to minimize set operations for offscreen preloading, including containsObject:. + // If selfInterfaceState isn't visible, then visibleIndexPaths represents either what /will/ be immediately visible at the + // instant we come onscreen, or what /will/ no longer be visible at the instant we come offscreen. + // So, preload and display all of those things, but don't waste resources preloading others. + // We handle this as a separate case to minimize set operations, including -containsObject:. + // + // DO NOT set Visible: even though these elements are in the visible range / "viewport", + // our overall container object is itself not yet, or no longer, visible. + // The moment it becomes visible, we will run the condition above. + + BOOL shouldUpdateInterfaceState = NO; + if (ASActivateExperimentalFeature(ASExperimentalFixRangeController)) { + shouldUpdateInterfaceState = [visibleIndexPaths containsObject:indexPath]; + } else { + shouldUpdateInterfaceState = [allCurrentIndexPaths containsObject:indexPath]; + } - if ([allCurrentIndexPaths containsObject:indexPath]) { - // DO NOT set Visible: even though these elements are in the visible range / "viewport", - // our overall container object is itself not visible yet. The moment it becomes visible, we will run the condition above - - // Set Layout, Preload + if (shouldUpdateInterfaceState) { interfaceState |= ASInterfaceStatePreload; - if (rangeMode != ASLayoutRangeModeLowMemory) { - // Add Display. - // We might be looking at an indexPath that was previously in-range, but now we need to clear it. - // In that case we'll just set it back to MeasureLayout. Only set Display | Preload if in allCurrentIndexPaths. interfaceState |= ASInterfaceStateDisplay; } } diff --git a/Tests/ASConfigurationTests.mm b/Tests/ASConfigurationTests.mm index 695292485..5738edc31 100644 --- a/Tests/ASConfigurationTests.mm +++ b/Tests/ASConfigurationTests.mm @@ -30,7 +30,8 @@ ASExperimentalDisableAccessibilityCache, ASExperimentalDispatchApply, ASExperimentalImageDownloaderPriority, - ASExperimentalTextDrawing + ASExperimentalTextDrawing, + ASExperimentalFixRangeController }; @interface ASConfigurationTests : ASTestCase @@ -55,7 +56,8 @@ + (NSArray *)names { @"exp_disable_a11y_cache", @"exp_dispatch_apply", @"exp_image_downloader_priority", - @"exp_text_drawing" + @"exp_text_drawing", + @"exp_fix_range_controller" ]; } From 30600cbbb00368f95631818f91be170040cb8c4a Mon Sep 17 00:00:00 2001 From: Tim Norman Date: Fri, 22 Mar 2019 11:30:40 -0700 Subject: [PATCH 062/122] Fix a bug with Yoga nodes whose position change and don't update. (#1408) --- Source/ASDisplayNode+Layout.mm | 10 ++++++++++ Source/ASDisplayNode+Yoga.h | 3 ++- Source/ASDisplayNode+Yoga.mm | 25 ++++++++++++++++++++----- Source/Private/ASDisplayNodeInternal.h | 1 + 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/Source/ASDisplayNode+Layout.mm b/Source/ASDisplayNode+Layout.mm index 42f0f0d6d..331c27a7d 100644 --- a/Source/ASDisplayNode+Layout.mm +++ b/Source/ASDisplayNode+Layout.mm @@ -402,9 +402,19 @@ - (void)_u_measureNodeWithBoundsIfNecessary:(CGRect)bounds // Use the last known constrainedSize passed from a parent during layout (if never, use bounds). NSUInteger version = _layoutVersion; ASSizeRange constrainedSize = [self _locked_constrainedSizeForLayoutPass]; +#if YOGA + // This flag indicates to the Texture+Yoga code that this next layout is intended to be + // displayed (vs. just for measurement). This will cause it to call setNeedsLayout on any nodes + // whose layout changes as a result of the Yoga recalculation. This is necessary because a + // change in one Yoga node can change the layout for any other node in the tree. + self.willApplyNextYogaCalculatedLayout = YES; +#endif ASLayout *layout = [self calculateLayoutThatFits:constrainedSize restrictedToSize:self.style.size relativeToParentSize:boundsSizeForLayout]; +#if YOGA + self.willApplyNextYogaCalculatedLayout = NO; +#endif nextLayout = ASDisplayNodeLayout(layout, constrainedSize, boundsSizeForLayout, version); // Now that the constrained size of pending layout might have been reused, the layout is useless // Release it and any orphaned subnodes it retains diff --git a/Source/ASDisplayNode+Yoga.h b/Source/ASDisplayNode+Yoga.h index 93cbf7a7c..ac6b92175 100644 --- a/Source/ASDisplayNode+Yoga.h +++ b/Source/ASDisplayNode+Yoga.h @@ -29,6 +29,7 @@ AS_EXTERN void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode * _Nullab @property BOOL yogaLayoutInProgress; // TODO: Make this atomic (lock). @property (nullable, nonatomic) ASLayout *yogaCalculatedLayout; +@property (nonatomic) BOOL willApplyNextYogaCalculatedLayout; // Will walk up the Yoga tree and returns the root node - (ASDisplayNode *)yogaRoot; @@ -54,7 +55,7 @@ AS_EXTERN void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode * _Nullab /// For internal usage only - (ASLayout *)calculateLayoutYoga:(ASSizeRange)constrainedSize; /// For internal usage only -- (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize; +- (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize willApply:(BOOL)willApply; /// For internal usage only - (void)invalidateCalculatedYogaLayout; /** diff --git a/Source/ASDisplayNode+Yoga.mm b/Source/ASDisplayNode+Yoga.mm index c52eeb679..aefb236e2 100644 --- a/Source/ASDisplayNode+Yoga.mm +++ b/Source/ASDisplayNode+Yoga.mm @@ -166,6 +166,14 @@ - (ASLayout *)yogaCalculatedLayout return _yogaCalculatedLayout; } +- (BOOL)willApplyNextYogaCalculatedLayout { + return _willApplyNextYogaCalculatedLayout; +} + +- (void)setWillApplyNextYogaCalculatedLayout:(BOOL)willApplyNextYogaCalculatedLayout { + _willApplyNextYogaCalculatedLayout = willApplyNextYogaCalculatedLayout; +} + - (void)setYogaLayoutInProgress:(BOOL)yogaLayoutInProgress { setFlag(YogaLayoutInProgress, yogaLayoutInProgress); @@ -194,7 +202,7 @@ - (ASLayout *)layoutForYogaNode return [ASLayout layoutWithLayoutElement:self size:size position:position sublayouts:nil]; } -- (void)setupYogaCalculatedLayout +- (void)setupYogaCalculatedLayoutAndSetNeedsLayoutForChangedNodes:(BOOL)setNeedsLayoutForChangedNodes { ASScopedLockSelfOrToRoot(); @@ -231,6 +239,13 @@ - (void)setupYogaCalculatedLayout layout = [layout filteredNodeLayoutTree]; if ([self.yogaCalculatedLayout isEqual:layout] == NO) { + if (setNeedsLayoutForChangedNodes && !self.willApplyNextYogaCalculatedLayout) { + // This flag will be set when this layout is intended for immediate display. In this case, we + // want to ensure that we call setNeedsLayout on any other nodes. Note that we skip any nodes + // whose willApplyNextYogaCalculatedLayout flags are set, as those are the nodes that are + // already being laid out. + [self setNeedsLayout]; + } self.yogaCalculatedLayout = layout; } else { layout = self.yogaCalculatedLayout; @@ -320,7 +335,7 @@ - (ASLayout *)calculateLayoutYoga:(ASSizeRange)constrainedSize if (self.yogaLayoutInProgress == NO) { ASYogaLog("Calculating yoga layout from root %@, %@", self, NSStringFromASSizeRange(constrainedSize)); - [self calculateLayoutFromYogaRoot:constrainedSize]; + [self calculateLayoutFromYogaRoot:constrainedSize willApply:self.willApplyNextYogaCalculatedLayout]; } else { ASYogaLog("Reusing existing yoga layout %@", _yogaCalculatedLayout); } @@ -337,7 +352,7 @@ - (ASLayout *)calculateLayoutYoga:(ASSizeRange)constrainedSize return [self calculateLayoutLayoutSpec:constrainedSize]; } -- (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize +- (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize willApply:(BOOL)willApply { ASScopedLockSet lockSet = [self lockToRootIfNeededForLayout]; ASDisplayNode *yogaRoot = self.yogaRoot; @@ -345,7 +360,7 @@ - (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize if (self != yogaRoot) { ASYogaLog("ESCALATING to Yoga root: %@", self); // TODO(appleguy): Consider how to get the constrainedSize for the yogaRoot when escalating manually. - [yogaRoot calculateLayoutFromYogaRoot:ASSizeRangeUnconstrained]; + [yogaRoot calculateLayoutFromYogaRoot:ASSizeRangeUnconstrained willApply:willApply]; return; } @@ -398,7 +413,7 @@ - (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize }); ASDisplayNodePerformBlockOnEveryYogaChild(self, ^(ASDisplayNode * _Nonnull node) { - [node setupYogaCalculatedLayout]; + [node setupYogaCalculatedLayoutAndSetNeedsLayoutForChangedNodes:willApply]; node.yogaLayoutInProgress = NO; }); diff --git a/Source/Private/ASDisplayNodeInternal.h b/Source/Private/ASDisplayNodeInternal.h index 55cef7579..b20470c6c 100644 --- a/Source/Private/ASDisplayNodeInternal.h +++ b/Source/Private/ASDisplayNodeInternal.h @@ -164,6 +164,7 @@ AS_EXTERN NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimest NSMutableArray *_yogaChildren; __weak ASDisplayNode *_yogaParent; ASLayout *_yogaCalculatedLayout; + BOOL _willApplyNextYogaCalculatedLayout; #endif // Automatically manages subnodes From 3415d1b3290e83cb9b0bf1d231521de53c48e3df Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Fri, 22 Mar 2019 12:41:49 -0700 Subject: [PATCH 063/122] Follow up on the ASRangeController fix in #1418 (#1419) The changes in #1418 is a bit too aggressive when it comes to nodes that are in display range. It forces those nodes to not preload. Also update the changes to avoid diluting the experiment data by triggering too broadly (i.e avoid triggering when the old and new implementations yield the same result leading to no behavior change). --- Source/Details/ASRangeController.mm | 31 +++++++++++++++++++---------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/Source/Details/ASRangeController.mm b/Source/Details/ASRangeController.mm index 5625fd59e..63a5562ad 100644 --- a/Source/Details/ASRangeController.mm +++ b/Source/Details/ASRangeController.mm @@ -351,25 +351,34 @@ - (void)_updateVisibleNodeIndexPaths } else { // If selfInterfaceState isn't visible, then visibleIndexPaths represents either what /will/ be immediately visible at the // instant we come onscreen, or what /will/ no longer be visible at the instant we come offscreen. - // So, preload and display all of those things, but don't waste resources preloading others. - // We handle this as a separate case to minimize set operations, including -containsObject:. + // So, preload and display all of those things, but don't waste resources displaying others. // // DO NOT set Visible: even though these elements are in the visible range / "viewport", // our overall container object is itself not yet, or no longer, visible. // The moment it becomes visible, we will run the condition above. - BOOL shouldUpdateInterfaceState = NO; - if (ASActivateExperimentalFeature(ASExperimentalFixRangeController)) { - shouldUpdateInterfaceState = [visibleIndexPaths containsObject:indexPath]; - } else { - shouldUpdateInterfaceState = [allCurrentIndexPaths containsObject:indexPath]; + ASInterfaceState interfaceStateBeforeFix = interfaceState; + if ([allCurrentIndexPaths containsObject:indexPath]) { + interfaceStateBeforeFix |= ASInterfaceStatePreload; + if (rangeMode != ASLayoutRangeModeLowMemory) { + interfaceStateBeforeFix |= ASInterfaceStateDisplay; + } } - - if (shouldUpdateInterfaceState) { - interfaceState |= ASInterfaceStatePreload; + + ASInterfaceState interfaceStateAfterFix = interfaceState; + if ([visibleIndexPaths containsObject:indexPath]) { + interfaceStateAfterFix |= ASInterfaceStatePreload; if (rangeMode != ASLayoutRangeModeLowMemory) { - interfaceState |= ASInterfaceStateDisplay; + interfaceStateAfterFix |= ASInterfaceStateDisplay; } + } else if ([displayIndexPaths containsObject:indexPath]) { + interfaceStateAfterFix |= ASInterfaceStatePreload; + } + + if (interfaceStateBeforeFix != interfaceStateAfterFix && ASActivateExperimentalFeature(ASExperimentalFixRangeController)) { + interfaceState = interfaceStateAfterFix; + } else { + interfaceState = interfaceStateBeforeFix; } } From c340fff87f7101990cdb48bf324fd4b834a7a9ed Mon Sep 17 00:00:00 2001 From: Michael Zuccarino Date: Fri, 22 Mar 2019 15:38:41 -0700 Subject: [PATCH 064/122] Dev docs for threading in Texture (#1319) * Getting started on adding threading by example * writing writing writing * more info * More threading * More more more * Threading * self lock * Some language fixes * Update docs/_docs/development/threading.md Co-Authored-By: mikezucc * fix image references --- docs/_docs/development/threading.md | 355 +++++++++++++++++- docs/static/images/development/threading1.png | Bin 0 -> 8935 bytes docs/static/images/development/threading2.png | Bin 0 -> 18233 bytes docs/static/images/development/threading3.png | Bin 0 -> 62412 bytes 4 files changed, 354 insertions(+), 1 deletion(-) create mode 100644 docs/static/images/development/threading1.png create mode 100644 docs/static/images/development/threading2.png create mode 100644 docs/static/images/development/threading3.png diff --git a/docs/_docs/development/threading.md b/docs/_docs/development/threading.md index 33499aeb2..8692387c8 100644 --- a/docs/_docs/development/threading.md +++ b/docs/_docs/development/threading.md @@ -4,4 +4,357 @@ layout: docs permalink: /development/threading.html --- -

👷👷‍♀️Under construction…

\ No newline at end of file +# Threading + +## At a glance + +The Texture philosophy is about efficient utilization of resources in order to provide a high frame rate user experience. In other words, an almost scientific approach to distributing work amongst threads keeps the Default Run Loop lean to allow user input events and to consume work scheduled on the main dispatch queue. + +There are a few conventions to follow: + +1. Invocations of the UIKit API must happen on the main thread via either `dispatch_get_main_queue()` or with `ASPerformBlockOnMainThread()` +2. Anything else should generally happen on a background thread, with prioritization + +## Run Loop, Threads, and Queues + +A thread is managed by the kernel to execute code. A `dispatch_queue_t` describes a context for an ordered list of blocks to be executed. A queue may utilize specific thread for execution determined by GCD and the kernel. + +A run loop is created per thread if required. Selectors and timers can be attached to a run loop. The main thread's run loop is iterated through automatically, background threads' run loops need to be looped explicitly, and may not necessarily exist without prior access. A thread has only one run loop. + +When using `dispatch_async` for a background queue, GCD (Grand Central Dispatch) will ask the kernel for a thread, where the kernel either creates one, picks an idle thread, or waits for one to become idle. These threads, once created, live in the thread pool. You should never have to call directly to a thread, they are abstracted to you by GCD. + +__Dispatch Queue Playgrounds__ + +`self.queue1 = dispatch_queue_create("multitask.1", DISPATCH_QUEUE_SERIAL);` + +This creates a `dispatch_queue_t`. This is an Objective-C object, managed by ARC. When you startup LLDB, you will notice that there is no designated thread for this queue. That is because a thread is only created if GCD asks the kernel for a thread when work is scheduled. + +``` +dispatch_async(self.queue1, ^{ + NSLog(@"I love robustly distributed work"); +}); +``` + +After performing the above operation, a thread is created to perform your work. A thread creation time is roughly 90 microseconds [according to Apple](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/CreatingThreads/CreatingThreads.html#//apple_ref/doc/uid/10000057i-CH15-SW7). After this has executed and you see the statement in the debugger, starting LLDB and running `(lldb) thread list` shows that there should be an additional background thread with no associated queue. This is because a thread is created and added to the pool, however the queue is detached from the thread until needed again. + +``` +dispatch_async(self.queue1, ^{ + while (true) { + NSLog(@"I love robustly distributed work"); + } +}); +``` + +Starting LLDB while the statements are printing and running `(lldb) thread list` shows a list of threads, one which has an associated queue, in this case our queue `multitask.1`. + +``` +dispatch_async(self.queue1, ^{ + printf("block 1\n"); +}); +dispatch_async(self.queue1, ^{ + printf("block 2\n"); +}); +``` + +For this example, we add some autocontinue breakpoints that run a command `thread info` on both of the print statements. Running the program shows the two block invocations are executed on the same thread. + +Let's see what kind of behavior GCD exhibits: + +``` +self.queueA = dispatch_queue_create("multitask.A", DISPATCH_QUEUE_SERIAL); +self.queueB = dispatch_queue_create("multitask.B", DISPATCH_QUEUE_SERIAL); + +dispatch_async(self.queueA, ^{ + printf("A: block 1\n"); +}); +dispatch_async(self.queueA, ^{ + printf("A: block 2\n"); +}); + +dispatch_async(self.queueB, ^{ + printf("B: block 1\n"); +}); +dispatch_async(self.queueB, ^{ + printf("B: block 2\n"); +}); +``` +outputs the following: +``` +thread #2: tid = 0x32a3e6, 0x000000010adf4457 ThreadingFun`__29-[ViewController viewDidLoad]_block_invoke(.block_descriptor=0x000000010adf6090) at ViewController.m:24, queue = 'multitask.A', stop reason = breakpoint 2.1 +thread #3: tid = 0x32a3e7, 0x000000010adf44b7 ThreadingFun`__29-[ViewController viewDidLoad]_block_invoke_3(.block_descriptor=0x000000010adf6110) at ViewController.m:31, queue = 'multitask.B', stop reason = breakpoint 4.1 + +A: block 1 +B: block 1 + +thread #2: tid = 0x32a3e6, 0x000000010adf4487 ThreadingFun`__29-[ViewController viewDidLoad]_block_invoke_2(.block_descriptor=0x000000010adf60d0) at ViewController.m:27, queue = 'multitask.A', stop reason = breakpoint 3.1 +thread #3: tid = 0x32a3e7, 0x000000010adf44e7 ThreadingFun`__29-[ViewController viewDidLoad]_block_invoke_4(.block_descriptor=0x000000010adf6150) at ViewController.m:34, queue = 'multitask.B', stop reason = breakpoint 5.1 + +A: block 2 +B: block 2 +``` + +So here we can see that GCD elects to create two new threads. It will reuse the thread that pertains to the queue. + +So this is when we dispatch work to a queue we created manually. Lets take a look at using `dispatch_get_global_queue` + +Without using LLDB breakpoints to execute `thread info`. Since running LLDB takes a non-deterministic amount of time, the block execution time may vary producing out of order statements. +``` +dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ + printf("block 1\n"); +}); + +dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ + printf("block 2\n"); +}); + +dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ + printf("block 3\n"); +}); + +dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ + printf("block 4\n"); +}); +``` +produces the following output +``` +block 1 +block 2 +block 3 +block 4 +``` +Starting the debugger at the end of the function should show 4 threads, each with the same queue attached. Now GCD is federating your work onto threads on demand. How many threads will it create? + +``` +for (int i=0; i < 100; i++) { + dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ + printf("block %i\n", i); + }); +} +``` +Starting the debugger approximately 500ms after the completion of the for loop shows that there are 64 threads in the thread pool. The behavior you would experience by the thread pool limit is a delay in block execution after the 64th long-running block has been queued. If you are performing a _lot_ of work, make sure to grab the correct priority queue `dispatch_get_global_queue(QOS_CLASS_xxxxxxx, 0)` for your needs. Otherwise you may denial-of-service any other blocks looking to be executed later on. + +An interesting dive into how cores fit into this picture can be found on Mike Ash's [Observing the A11 heterogenous cores](https://www.mikeash.com/pyblog/friday-qa-2017-11-10-observing-the-a11s-heterogenous-cores.html) + +Here we can do a fun test to understand the recycling behavior of GCD +``` +const int64_t kOneMillion = 1000 * 1000; +static mach_timebase_info_data_t s_timebase_info; + +static dispatch_once_t onceToken; +dispatch_once(&onceToken, ^{ + (void) mach_timebase_info(&s_timebase_info); +}); + +for (int i=0; i < 1000; i++) { + uint64_t start = mach_absolute_time(); + dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ + uint64_t end = mach_absolute_time(); + uint64_t elapsed = end - start; + uint64_t nanosec = elapsed * s_timebase_info.numer / s_timebase_info.denom; + printf("%llu ms block %i\n", nanosec / kOneMillion, i); + usleep(10000); + }); +} +``` +Looking at the logs, we can see how GCD reaches a maximum background thread count, varied by CPU. This example was run on the 8 core Macbook Pro +``` +1 ms block 62 +1 ms block 63 +10 ms block 65 +10 ms block 66 +10 ms block 64 +``` + +## ASMainSerialQueue + +The `ASMainSerialQueue` ensures that work can be performed serially on the main thread without being interrupted. The key difference between this and purely using `dispatch_async(dispatch_get_main_queue, block)` is that the main thread can be interrupted between execution of blocks in its queue, where as this interface will execute everything possible in its queue on the main thread before returning control back to the OS. + +This interface calls to `ASPerformBlockOnMainThread`. This interface will lock out other threads attempting to schedule work while it is popping the next block to be consumed on the main thread. New blocks scheduled while existing ones are executing are guaranteed to be executed during that run loop, i.e. before anything else even on main dispatch queue or the Default Run Loop are consumed. + +This should also be used a synchronization mechanism. Since the `ASMainSerialQueue` is serial, you can be sure that it will execute in order. An example would be to queue the following blocks: change view property -> trigger layout update -> animate. Remember that funny situations can occur since execution of work on `ASMainSerialQueue` can early execute blocks that were scheduled later than blocks sent using `dispatch_async(dispatch_get_main_queue())` if the `ASMainSerialQueue` is already consuming blocks. The execution time of the `[ASMainSerialQueue runBlocks]` is uncertain given that more work can be scheduled. + +This is really just a buffer to the main dispatch queue. It behaves the same, except this offers some more visibility onto how much work is scheduled. This interface guarantees that everything scheduled will execute in one operation serially on the main thread. + +## ASRunLoopQueue + +Even deallocation of UIKit objects on the main thread are optimized in Texture. Usually, synchronously, objects are retain counted and freed from memory when appropriate. This just isnt't good enough for a performant framework like Texture. So instead, for each run loop iteration on the main thread, a maximum set of objects can be designated to be freed. This is done by calling `void ASPerformMainThreadDeallocation(id _Nullable __strong * _Nonnull objectPtr)`. Behind the scenes, this is using `ASRunLoopQueue` ensuring that only a maximum amount of objects are deallocated, and that this occurs at the least demanding time on the run loop. Let's break down its initializer: + +``` +// Self is guaranteed to outlive the observer. Without the high cost of a weak pointer, +// __unsafe_unretained allows us to avoid flagging the memory cycle detector. +__unsafe_unretained __typeof__(self) weakSelf = self; +void (^handlerBlock) (CFRunLoopObserverRef observer, CFRunLoopActivity activity) = ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { + [weakSelf processQueue]; +}; +``` + +Why `__unsafe__unretained`? + +``` +_runLoopObserver = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, 0, handlerBlock); +CFRunLoopAddObserver(_runLoop, _runLoopObserver, kCFRunLoopCommonModes); +``` +This creates a run loop observer that can be added to the targeted run loop, in this case always the main thread's run loop. The [kCFRunLoopBeforeWaiting](https://developer.apple.com/documentation/corefoundation/cfrunloopactivity?language=objc) places the call to this observer just after the run loop has finished processing the other inputs and sources and just before it will be put to sleep. This ensures that high priority tasks such as responding to user events such as touches are handled first. + +However, there is one nuance to using `kCFRunLoopBeforeWaiting`, the handler will not be called if there were no inputs or sources to wake up the run loop again in a situation where the deallocation has to be batched. In order to wake the run loop for another cycle, we introduce a no-op source so that there is "work" to be performed for each iteration of the run loop and then invoke the Core Foundation API to iterate the run loop. + +``` +static void runLoopSourceCallback(void *info) { + // No-op +} + +... + +CFRunLoopSourceContext sourceContext = {}; +sourceContext.perform = runLoopSourceCallback; +_runLoopSource = CFRunLoopSourceCreate(NULL, 0, &sourceContext); +CFRunLoopAddSource(runloop, _runLoopSource, kCFRunLoopCommonModes); +``` + +At the end of each `processQueue` invocation, if we still have more objects to process (exceeded the deallocation batch size), then we use this at the end of the `processQueue` to begin another run loop iteration. + +``` +if (!isQueueDrained) { + CFRunLoopSourceSignal(_runLoopSource); + CFRunLoopWakeUp(_runLoop); +} +``` + +## Locks and Safety + +Carrying on from our last example, we have to make sure that during our processing of the queue for objects that need to be deallocated remains untampered to avoid bounds errors. The execution of this function must be synchronous. In this particular example, we can guarantee that the execution of this will always happen on a designated run loop's single thread, and therefore synchronously. However, we want to design robust software that won't deadlock or crash. + +__Method 1__ + +Looking at `-(void)processQueue`, we have the following locking convention: +``` +@interface ASRunLoopQueue () { + ASDN::RecursiveMutex _internalQueueLock; +} + +ASSynthesizeLockingMethodsWithMutex(_internalQueueLock) // Texture macro for creating the lock + +- (void)processQueue +{ + { + ASDN::MutexLocker l(_internalQueueLock); + + // ... array operations + } +} +``` + +`ASDN::MutexLocker l(_internalQueueLock);` will lock the recursive mutex. This prevents other threads other than the current thread from entering this routine again, which is fine since a thread serially executes. The lock will free itself once the stack frame is popped. + +Since this lock is also shared, it will prevent other routines from entering until this lock is released. This is mandatory for safe synchronization of shared objects between threads. + +__Method 2__ + +An alternative method is to manage the duration of the lock hold manually rather than using the runtime and scope. You must remember to unlock. + +``` +@implementation ASDeallocQueue { + ASDN::Mutex _lock; +} + +- (void)releaseObjectInBackground:(id _Nullable __strong *)objectPtr +{ + NSParameterAssert(objectPtr != NULL); // do I actually need to lock ? + // other conditions or non-shared tasks + _lock.lock(); + sharedObject.modify(newData); + _lock.unlock(); +} +``` + +__Method 3__ + +`ASThread` provides `ASLockScopeSelf()`. This is a convenience over `ASLockScopeUnowned()`. This will unlock itself once the scope in which the lock was created is released. Only use this when you are confident that the lock should remain until scope is complete. You can only have one lock defined for `self`, thus it will block all other branches. + +``` +#define ASLockScopeUnowned(nsLocking) \ + __unsafe_unretained id __lockToken __attribute__((cleanup(_ASLockScopeUnownedCleanup))) = nsLocking; \ + [__lockToken lock]; + +ASDISPLAYNODE_INLINE void _ASLockScopeCleanup(id __strong * const lockPtr) { + [*lockPtr unlock]; +} +``` + +Usage example +``` +- (ASImageNode *)imageNode +{ + ASLockScopeSelf(); + if (!_imageNode) { + _imageNode = [[ASImageNode alloc] init]; + [_imageNode setLayerBacked:YES]; + } + return _imageNode; +} +``` + +## Thread Contention + +Although locks are critical to guaranteeing safer multithreading synchronization, they must be used with some caution, as threads are slept if they can not hold the lock immediately. In order to sleep/wake a thread, many CPU cycles are consumed. Some `std::mutex` implementations are able to spinlock for a bit at first to combat the overhead of sleeping and waking a thread for failed lock holds. However, the performance loss here is a necessary sacrifice in order to attain thread safety. In order to make up for the potential loss in performance, the programmer should design as little opportunity for contention as possible, and to lock only the smallest amount of work. + +An interesting investigation can be found [here](https://stackoverflow.com/a/49712993/2584565) + +> Bottom line, a mutex is implemented with atomics. To synchronize atomics between cores an internal bus must be locked which freezes the corresponding cache line for several hundred clock cycles. + +## Other Threading Practices in Texture + +__Main Thread/ UIKit API__ + +API | Description | +--- | --- | +`ASDisplayNodeAssertMainThread();` | Place this at the start of the every function definition that performs work synchronously on the main thread. +`ASPerformBlockOnMainThread(block)` | If on main thread already, run block synchronously, otherwise use `dispatch_async(dispatch_get_main_queue(block))` +`ASPerformMainThreadDeallocation(&object)` | Schedule async deallocation of UIKit components +`ASPerformBackgroundDeallocation(&object)` | Schedule async deallocation of __non-UIKit__ objects +`ASPerformBlockOnBackgroundThread(block)` | Perform work on background + + +## Threading by example + +Let's take a look at collection view. UIKit calls (such as batch updates) can be invoked from the background due to network calls returning with fresh models. These models are then used to calculate a change set used by the collection view to perform a batch update. This batch update calls into UIKit API for insertions, deletions, and moves in one continuous operation that modifies the view hierarchy. As we know, all UIKit operations must occur on the main thread. How can we design the interface to get the most efficient distribution of work? + +So we have the following situation: +1. A data source that talks and responds to events from the network +2. A data source that calculates a change set for the collection view +2. A collection view that performs batch updates, calling into UIKit API +3. Batch updates must happen serially, as they use transformations rather than clearingAllData + +`ASDataController` + +This is our data source that talks to the network. It doesn't really in Texture, but we can pretend that this interface is receiving invocations from a network object. The network object invokes a completion block created by the `ASDataController` in a background thread context. This is correct behavior, because we want non-UI related work to happen off of the main thread as much as possible. + + Since the data source still needs to have usable data while the network and calculations are occurring, we keep two different data structures: `pendingMap` and `visibleMap`. The `visibleMap` is node map for display on the collection view and the `pendingMap` is an ephemeral for calculating the change set. The change set is the exact least amount of work required to shuffle the collection view items from the old set to the new set. + +__Editing Transaction Queue__ + +The Editing Transaction Queue is an example of using a background queue to schedule work that may be long running in a way that won't block the application from receiving callbacks from the main thread's Run Loop or other main thread only work such as calls to UIKit API. + +This is a `dispatch_queue_t` that is privately held by each UICollectionView. The work scheduled gets performed on a background thread. + +__Describing the Batch Update flow__ + + +![Threading1](/docs/static/images/development/threading1.png) + +Starting with a simple example, let's say everything was on the main thread. The app would then appear unresponsive to the user until the entire flow finished. This is because the main thread's run loop (which receives operating system events for input) and the main thread dispatch queue compete for service time on the main thread. Execution goes back and forth between the run loop and the main dispatch queue while work is present in either. Now you can see that the work your code wants to schedule competes for time with system and user input events. + +Let's identify the items that are not critical for execution on the main thread and dispatch those as blocks onto a background thread. + + +![Threading2](/docs/static/images/development/threading2.png) + +Ok, this is starting to look better. The main thread now only performs the minimal amount of work. This means the app will feel responsive to the user as it is able to consume Run Loop events with little interruption. However, a lot of the background work is still being done serially in one long running operation. If the network is the longest running task in this sequence, you would want to make sure they get fired off as early as possible. Let's also introduce another condition, we have a NSTimer running which injects a "hint" cell into the collection view. This is to demonstrate how the editing transaction queue is consumed serially. + +The reason we must design the work to consume the editing transaction queue serially alongside the main queue is change sets must represent a transformation of the current data set in the collection view. This means that each time a `performBatchUpdate` is intended to be performed, it _must_ calculate using the latest data set used for display in the collection view. In this following image, this means that change sets can only be calculated from `A -> B`, and then when `performBatchUpdate` task is fully finished, another calculation can be done from `B -> C`. Consider data sets `B` and `C` coming in right after the other. If the queues consumed concurrently, then you would get a change set for `A -> B` and `A -> C`. The collection view will crash if a change set is requested for `A -> C`, while a `performBatchUpdate` is updating the collection view to reflect `A -> B` with a `A -> C` operation is queued. All this means that we create a pseudo-lock on the `editingTransactionQueue` by consuming it one operation at a time, either when the queue is empty or when an operation has finished as called by the completed `performBatchUpdate` invocation. + + +![Threading3](/docs/static/images/development/threading3.png) + +One small note is these diagrams are missing a representation of the main dispatch queue. Remember this is separate from the main thread. It is a data structure operated by GCD similarly to ASMainSerialQueue which handles execution to the main thread. All the work that goes from the background threads to the main thread is first put onto the main dispatch queue when using `dispatch_async(dispatch_get_main_queue())`. +. diff --git a/docs/static/images/development/threading1.png b/docs/static/images/development/threading1.png new file mode 100644 index 0000000000000000000000000000000000000000..a1ed3e787e0c59ad52cfa2117086660afa51b0a1 GIT binary patch literal 8935 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 ZXruAHdrPNpbN;}x&&JBZ5^e6A@L&8_=D+{| literal 0 HcmV?d00001 diff --git a/docs/static/images/development/threading2.png b/docs/static/images/development/threading2.png new file mode 100644 index 0000000000000000000000000000000000000000..26056d001d5703136ca7defef20c31be25a3ed49 GIT binary patch literal 18233 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
bg>ruL7L4ma6xB&78IE_ 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 literal 0 HcmV?d00001 From 6aaf0dcb1a47c69016c0c9d886a5982643a7aad0 Mon Sep 17 00:00:00 2001 From: Michael Zuccarino Date: Fri, 22 Mar 2019 15:41:35 -0700 Subject: [PATCH 065/122] Removed locked jekyll version (#1322) --- docs/Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Gemfile b/docs/Gemfile index dec8af500..f6816300a 100755 --- a/docs/Gemfile +++ b/docs/Gemfile @@ -2,4 +2,4 @@ source 'https://rubygems.org' gem 'github-pages' gem 'rouge', '~>1.7' -gem 'jekyll', '~>3.6.3' +gem 'jekyll' From d34a182c5f3984787a5c4c6df5e7df41391846d0 Mon Sep 17 00:00:00 2001 From: Dmitry Gridnev Date: Mon, 25 Mar 2019 23:59:31 +0300 Subject: [PATCH 066/122] showcase.md updated (#1421) --- docs/showcase.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/showcase.md b/docs/showcase.md index 57f9ca127..9d54ed247 100755 --- a/docs/showcase.md +++ b/docs/showcase.md @@ -271,6 +271,12 @@ permalink: /showcase.html
Bluebird + + +
+
+ App in the Air + From b6a6b0fa8460f18d87c8e8fbcb862be7fed2d50a Mon Sep 17 00:00:00 2001 From: Michael Zuccarino Date: Mon, 25 Mar 2019 14:11:04 -0700 Subject: [PATCH 067/122] Experiment with disabling ASViewController background dealloc (#1420) --- Schemas/configuration.json | 3 ++- Source/ASExperimentalFeatures.h | 1 + Source/ASExperimentalFeatures.mm | 3 ++- Source/ASViewController.mm | 5 +++++ Tests/ASConfigurationTests.mm | 6 ++++-- 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Schemas/configuration.json b/Schemas/configuration.json index 11aeca211..b66510699 100644 --- a/Schemas/configuration.json +++ b/Schemas/configuration.json @@ -26,7 +26,8 @@ "exp_dispatch_apply", "exp_image_downloader_priority", "exp_text_drawing", - "exp_fix_range_controller" + "exp_fix_range_controller", + "exp_oom_bg_dealloc_disable" ] } } diff --git a/Source/ASExperimentalFeatures.h b/Source/ASExperimentalFeatures.h index 287c910bf..f5650be83 100644 --- a/Source/ASExperimentalFeatures.h +++ b/Source/ASExperimentalFeatures.h @@ -32,6 +32,7 @@ typedef NS_OPTIONS(NSUInteger, ASExperimentalFeatures) { ASExperimentalImageDownloaderPriority = 1 << 11, // exp_image_downloader_priority ASExperimentalTextDrawing = 1 << 12, // exp_text_drawing ASExperimentalFixRangeController = 1 << 13, // exp_fix_range_controller + ASExperimentalOOMBackgroundDeallocDisable = 1 << 14, // exp_oom_bg_dealloc_disable ASExperimentalFeatureAll = 0xFFFFFFFF }; diff --git a/Source/ASExperimentalFeatures.mm b/Source/ASExperimentalFeatures.mm index 4dfe5ddde..0badb9fcf 100644 --- a/Source/ASExperimentalFeatures.mm +++ b/Source/ASExperimentalFeatures.mm @@ -25,7 +25,8 @@ @"exp_dispatch_apply", @"exp_image_downloader_priority", @"exp_text_drawing", - @"exp_fix_range_controller"])); + @"exp_fix_range_controller", + @"exp_oom_bg_dealloc_disable"])); if (flags == ASExperimentalFeatureAll) { return allNames; } diff --git a/Source/ASViewController.mm b/Source/ASViewController.mm index d27f47238..a1151e00d 100644 --- a/Source/ASViewController.mm +++ b/Source/ASViewController.mm @@ -15,6 +15,8 @@ #import #import #import +#import +#import @implementation ASViewController { @@ -98,6 +100,9 @@ - (void)_initializeInstance - (void)dealloc { + if (ASActivateExperimentalFeature(ASExperimentalOOMBackgroundDeallocDisable)) { + return; + } ASPerformBackgroundDeallocation(&_node); } diff --git a/Tests/ASConfigurationTests.mm b/Tests/ASConfigurationTests.mm index 5738edc31..aa0e3a49d 100644 --- a/Tests/ASConfigurationTests.mm +++ b/Tests/ASConfigurationTests.mm @@ -31,7 +31,8 @@ ASExperimentalDispatchApply, ASExperimentalImageDownloaderPriority, ASExperimentalTextDrawing, - ASExperimentalFixRangeController + ASExperimentalFixRangeController, + ASExperimentalOOMBackgroundDeallocDisable }; @interface ASConfigurationTests : ASTestCase @@ -57,7 +58,8 @@ + (NSArray *)names { @"exp_dispatch_apply", @"exp_image_downloader_priority", @"exp_text_drawing", - @"exp_fix_range_controller" + @"exp_fix_range_controller", + @"exp_oom_bg_dealloc_disable" ]; } From a631803806e03aaf774201ba721e79619677f248 Mon Sep 17 00:00:00 2001 From: Michael Zuccarino Date: Wed, 27 Mar 2019 11:23:27 -0700 Subject: [PATCH 068/122] [Experiment] Dont forget about these collection view background deallocs (#1424) * Dont forget about these collection view background deallocs * Also Table view for completeness --- Source/ASCollectionView.mm | 6 ++++-- Source/ASTableView.mm | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Source/ASCollectionView.mm b/Source/ASCollectionView.mm index 4ea247095..d8dc9fdfe 100644 --- a/Source/ASCollectionView.mm +++ b/Source/ASCollectionView.mm @@ -334,8 +334,10 @@ - (void)dealloc } // Data controller & range controller may own a ton of nodes, let's deallocate those off-main. - ASPerformBackgroundDeallocation(&_dataController); - ASPerformBackgroundDeallocation(&_rangeController); + if (ASActivateExperimentalFeature(ASExperimentalOOMBackgroundDeallocDisable) == NO) { + ASPerformBackgroundDeallocation(&_dataController); + ASPerformBackgroundDeallocation(&_rangeController); + } } #pragma mark - diff --git a/Source/ASTableView.mm b/Source/ASTableView.mm index 34b78eb64..9cae6878b 100644 --- a/Source/ASTableView.mm +++ b/Source/ASTableView.mm @@ -373,8 +373,10 @@ - (void)dealloc } // Data controller & range controller may own a ton of nodes, let's deallocate those off-main - ASPerformBackgroundDeallocation(&_dataController); - ASPerformBackgroundDeallocation(&_rangeController); + if (ASActivateExperimentalFeature(ASExperimentalOOMBackgroundDeallocDisable) == NO) { + ASPerformBackgroundDeallocation(&_dataController); + ASPerformBackgroundDeallocation(&_rangeController); + } } #pragma mark - From 6b81f5a7608aadcc498f0e9ce343e1b3f8872c4e Mon Sep 17 00:00:00 2001 From: Greg Bolsinga Date: Thu, 28 Mar 2019 18:37:06 -0700 Subject: [PATCH 069/122] Fix retain cycle with transaction operations (#1429) Add unit tests that help find cycles. `-testWeakWithSingleOperation` fails without the code fix applied. --- AsyncDisplayKit.xcodeproj/project.pbxproj | 4 + .../_ASAsyncTransactionContainer.mm | 2 +- Tests/ASTransactionTests.mm | 84 +++++++++++++++++++ 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 Tests/ASTransactionTests.mm diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index c3617ea67..2d9476e5f 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -455,6 +455,7 @@ CCEDDDD9200C518800FFCD0A /* ASConfigurationTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = CCEDDDD8200C518800FFCD0A /* ASConfigurationTests.mm */; }; CCF18FF41D2575E300DF5895 /* NSIndexSet+ASHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = CC4981BA1D1C7F65004E13CC /* NSIndexSet+ASHelpers.h */; settings = {ATTRIBUTES = (Private, ); }; }; CCF1FF5E20C4785000AAD8FC /* ASLocking.h in Headers */ = {isa = PBXBuildFile; fileRef = CCF1FF5D20C4785000AAD8FC /* ASLocking.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D933F041224AD17F00FF495E /* ASTransactionTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = D933F040224AD17F00FF495E /* ASTransactionTests.mm */; }; DB55C2671C641AE4004EDCF5 /* ASContextTransitioning.h in Headers */ = {isa = PBXBuildFile; fileRef = DB55C2651C641AE4004EDCF5 /* ASContextTransitioning.h */; settings = {ATTRIBUTES = (Public, ); }; }; DB7121BCD50849C498C886FB /* libPods-AsyncDisplayKitTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */; }; DB78412E1C6BCE1600A9E2B4 /* _ASTransitionContext.mm in Sources */ = {isa = PBXBuildFile; fileRef = DB55C2601C6408D6004EDCF5 /* _ASTransitionContext.mm */; }; @@ -1000,6 +1001,7 @@ D3779BCFF841AD3EB56537ED /* Pods-AsyncDisplayKitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.release.xcconfig"; sourceTree = ""; }; D785F6601A74327E00291744 /* ASScrollNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASScrollNode.h; sourceTree = ""; }; D785F6611A74327E00291744 /* ASScrollNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASScrollNode.mm; sourceTree = ""; }; + D933F040224AD17F00FF495E /* ASTransactionTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTransactionTests.mm; sourceTree = ""; }; DB55C25F1C6408D6004EDCF5 /* _ASTransitionContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = _ASTransitionContext.h; path = ../_ASTransitionContext.h; sourceTree = ""; }; DB55C2601C6408D6004EDCF5 /* _ASTransitionContext.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = _ASTransitionContext.mm; path = ../_ASTransitionContext.mm; sourceTree = ""; }; DB55C2651C641AE4004EDCF5 /* ASContextTransitioning.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASContextTransitioning.h; sourceTree = ""; }; @@ -1389,6 +1391,7 @@ 83A7D95D1D446A6E00BF333E /* ASWeakMapTests.mm */, CC3B208D1C3F7D0A00798563 /* ASWeakSetTests.mm */, 695BE2541DC1245C008E6EA5 /* ASWrapperSpecSnapshotTests.mm */, + D933F040224AD17F00FF495E /* ASTransactionTests.mm */, 057D02C01AC0A66700C7AC3C /* AsyncDisplayKitTestHost */, CC583ABF1EF9BAB400134156 /* Common */, 058D09C6195D04C000B7D73C /* Supporting Files */, @@ -2292,6 +2295,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + D933F041224AD17F00FF495E /* ASTransactionTests.mm in Sources */, CCEDDDD9200C518800FFCD0A /* ASConfigurationTests.mm in Sources */, AE440175210FB7CF00B36DA2 /* ASTextKitFontSizeAdjusterTests.mm in Sources */, E51B78BF1F028ABF00E32604 /* ASLayoutFlatteningTests.mm in Sources */, diff --git a/Source/Details/Transactions/_ASAsyncTransactionContainer.mm b/Source/Details/Transactions/_ASAsyncTransactionContainer.mm index ed44231ce..48dc6be44 100644 --- a/Source/Details/Transactions/_ASAsyncTransactionContainer.mm +++ b/Source/Details/Transactions/_ASAsyncTransactionContainer.mm @@ -59,7 +59,7 @@ - (_ASAsyncTransaction *)asyncdisplaykit_asyncTransaction if (self == nil) { return; } - [transactions removeObject:completedTransaction]; + [self.asyncdisplaykit_asyncLayerTransactions removeObject:completedTransaction]; [self asyncdisplaykit_asyncTransactionContainerDidCompleteTransaction:completedTransaction]; }]; [transactions addObject:transaction]; diff --git a/Tests/ASTransactionTests.mm b/Tests/ASTransactionTests.mm new file mode 100644 index 000000000..370f05135 --- /dev/null +++ b/Tests/ASTransactionTests.mm @@ -0,0 +1,84 @@ +// +// ASTransactionTests.m +// AsyncDisplayKitTests +// +// Created by Greg Bolsinga on 3/26/19. +// Copyright © 2019 Pinterest. All rights reserved. +// + +#import "ASTestCase.h" +#import + +@interface ASTransactionTests : ASTestCase + +@end + +@implementation ASTransactionTests + +- (void)testWeak +{ + __weak _ASAsyncTransaction* weakTransaction = nil; + @autoreleasepool { + CALayer *layer = [[CALayer alloc] init]; + _ASAsyncTransaction *transaction = layer.asyncdisplaykit_asyncTransaction; + + weakTransaction = transaction; + layer = nil; + } + + // held by main transaction group + XCTAssertNotNil(weakTransaction); + + // run so that transaction group drains. + static NSTimeInterval delay = 0.1; + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:delay]]; + + XCTAssertNil(weakTransaction); +} + +- (void)testWeakWhenCancelled +{ + __weak _ASAsyncTransaction* weakTransaction = nil; + @autoreleasepool { + CALayer *layer = [[CALayer alloc] init]; + _ASAsyncTransaction *transaction = layer.asyncdisplaykit_asyncTransaction; + + weakTransaction = transaction; + + [layer asyncdisplaykit_cancelAsyncTransactions]; + layer = nil; + } + + XCTAssertNil(weakTransaction); +} + +- (void)testWeakWithSingleOperation +{ + __weak _ASAsyncTransaction* weakTransaction = nil; + @autoreleasepool { + CALayer *layer = [[CALayer alloc] init]; + _ASAsyncTransaction *transaction = layer.asyncdisplaykit_asyncTransaction; + + [transaction addOperationWithBlock:^id _Nullable{ + return nil; + } priority:1 + queue:dispatch_get_main_queue() + completion:^(id _Nullable value, BOOL canceled) { + ; + }]; + + weakTransaction = transaction; + layer = nil; + } + + // held by main transaction group + XCTAssertNotNil(weakTransaction); + + // run so that transaction group drains. + static NSTimeInterval delay = 0.1; + [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:delay]]; + + XCTAssertNil(weakTransaction); +} + +@end From 7d105ece701ab9cfcd93c2da4a5cff5625ca625d Mon Sep 17 00:00:00 2001 From: Jacob Farkas Date: Thu, 28 Mar 2019 18:41:42 -0700 Subject: [PATCH 070/122] Replace +load initializers with __attribute__((constructor)) functions (#1425) * Replace +load initializers with __attribute__((constructor)) functions * Updating the documentation for textureDidInitialize, per @nguyenhuy --- Source/ASConfigurationDelegate.h | 4 ++-- Source/ASDisplayNode.mm | 2 +- Source/Private/ASTipsController.mm | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Source/ASConfigurationDelegate.h b/Source/ASConfigurationDelegate.h index 127ec3eb1..fde3950cb 100644 --- a/Source/ASConfigurationDelegate.h +++ b/Source/ASConfigurationDelegate.h @@ -27,8 +27,8 @@ NS_ASSUME_NONNULL_BEGIN /** * Texture framework initialized. This method is called synchronously * on the main thread from ASInitializeFrameworkMainThread if you defined - * AS_INITIALIZE_FRAMEWORK_MANUALLY or from the default initialization point - * (currently +load) otherwise. + * AS_INITIALIZE_FRAMEWORK_MANUALLY or otherwise from the default initialization point + * (currently a static constructor, called before main()). */ - (void)textureDidInitialize; diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index fc541178f..7bc5716de 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -284,7 +284,7 @@ + (void)initialize } #if !AS_INITIALIZE_FRAMEWORK_MANUALLY -+ (void)load +__attribute__((constructor)) static void ASLoadFrameworkInitializer(void) { ASInitializeFrameworkMainThread(); } diff --git a/Source/Private/ASTipsController.mm b/Source/Private/ASTipsController.mm index acc592130..e30a064d6 100644 --- a/Source/Private/ASTipsController.mm +++ b/Source/Private/ASTipsController.mm @@ -37,9 +37,9 @@ @implementation ASTipsController #pragma mark - Singleton -+ (void)load +__attribute__((constructor)) static void ASLoadTipsControllerNotification(void) { - [NSNotificationCenter.defaultCenter addObserver:self.shared + [NSNotificationCenter.defaultCenter addObserver:ASTipsController.shared selector:@selector(windowDidBecomeVisibleWithNotification:) name:UIWindowDidBecomeVisibleNotification object:nil]; From 2234c47828e19d07a2881923afa737a1607b805a Mon Sep 17 00:00:00 2001 From: Max Wang Date: Thu, 28 Mar 2019 18:42:16 -0700 Subject: [PATCH 071/122] Fix dealloc on bg (#1410) * fix SIMULATE_WEB_RESPONSE not imported #449 * Fix to make rangeMode update in right time * remove uncessary assert * Fix collection cell editing bug for iOS 9 & 10 * Revert "Fix collection cell editing bug for iOS 9 & 10" This reverts commit 06e18a10596622ff8a68835c95a23986d7bf61ea. * Fix dealloc long gesture in background --- Source/ASTextNode.mm | 8 -------- Source/ASTextNode2.mm | 6 ------ 2 files changed, 14 deletions(-) diff --git a/Source/ASTextNode.mm b/Source/ASTextNode.mm index e872a0929..71138045e 100644 --- a/Source/ASTextNode.mm +++ b/Source/ASTextNode.mm @@ -261,14 +261,6 @@ - (instancetype)init - (void)dealloc { CGColorRelease(_shadowColor); - - // TODO: This may not be necessary post-iOS-9 when most UIKit assign APIs - // were changed to weak. - if (_longPressGestureRecognizer) { - _longPressGestureRecognizer.delegate = nil; - [_longPressGestureRecognizer removeTarget:nil action:NULL]; - [self.view removeGestureRecognizer:_longPressGestureRecognizer]; - } } #pragma mark - Description diff --git a/Source/ASTextNode2.mm b/Source/ASTextNode2.mm index 7fa844ba1..2eb0ad5aa 100644 --- a/Source/ASTextNode2.mm +++ b/Source/ASTextNode2.mm @@ -227,12 +227,6 @@ - (instancetype)init - (void)dealloc { CGColorRelease(_shadowColor); - - if (_longPressGestureRecognizer) { - _longPressGestureRecognizer.delegate = nil; - [_longPressGestureRecognizer removeTarget:nil action:NULL]; - [self.view removeGestureRecognizer:_longPressGestureRecognizer]; - } } #pragma mark - Description From c47d6d55f965760433e59ce47999d663bce5027e Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Thu, 28 Mar 2019 18:42:33 -0700 Subject: [PATCH 072/122] Add support for clipping only specific corners, add unit tests (#1415) * Add support for clipping only specific corners, add unit tests * Remove some cleanup to make the diff smaller * Fix --- Source/ASDisplayNode.h | 8 ++ Source/ASDisplayNode.mm | 91 ++++++++++-------- Source/Details/UIView+ASConvenience.h | 1 + Source/Private/ASDisplayNode+AsyncDisplay.mm | 10 +- Source/Private/ASDisplayNode+UIViewBridge.mm | 40 +++++++- Source/Private/ASDisplayNodeInternal.h | 12 ++- Source/Private/_ASPendingState.mm | 14 +++ Tests/ASDisplayNodeSnapshotTests.mm | 43 +++++++++ .../testClippingCornerRounding@2x.png | Bin 0 -> 1851 bytes .../testClippingCornerRounding_15@2x.png | Bin 0 -> 2553 bytes .../testClippingCornerRounding_1@2x.png | Bin 0 -> 1851 bytes .../testClippingCornerRounding_3@2x.png | Bin 0 -> 2121 bytes .../testClippingCornerRounding_7@2x.png | Bin 0 -> 2397 bytes .../testPrecompositedCornerRounding@2x.png | Bin 0 -> 2145 bytes .../testPrecompositedCornerRounding_15@2x.png | Bin 0 -> 3119 bytes .../testPrecompositedCornerRounding_1@2x.png | Bin 0 -> 2145 bytes .../testPrecompositedCornerRounding_3@2x.png | Bin 0 -> 2409 bytes .../testPrecompositedCornerRounding_7@2x.png | Bin 0 -> 2825 bytes 18 files changed, 174 insertions(+), 45 deletions(-) create mode 100644 Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testClippingCornerRounding@2x.png create mode 100644 Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testClippingCornerRounding_15@2x.png create mode 100644 Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testClippingCornerRounding_1@2x.png create mode 100644 Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testClippingCornerRounding_3@2x.png create mode 100644 Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testClippingCornerRounding_7@2x.png create mode 100644 Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding@2x.png create mode 100644 Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding_15@2x.png create mode 100644 Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding_1@2x.png create mode 100644 Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding_3@2x.png create mode 100644 Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding_7@2x.png diff --git a/Source/ASDisplayNode.h b/Source/ASDisplayNode.h index ce54e0a51..161146a4d 100644 --- a/Source/ASDisplayNode.h +++ b/Source/ASDisplayNode.h @@ -666,6 +666,14 @@ AS_EXTERN NSInteger const ASDefaultDrawingPriority; */ @property CGFloat cornerRadius; // default=0.0 +/** @abstract Which corners to mask when rounding corners. + * + * @note This option cannot be changed when using iOS < 11 + * and using ASCornerRoundingTypeDefaultSlowCALayer. Use a different corner rounding type to implement not-all-corners + * rounding in prior versions of iOS. + */ +@property CACornerMask maskedCorners; // default=all corners. + @property BOOL clipsToBounds; // default==NO @property (getter=isHidden) BOOL hidden; // default==NO @property (getter=isOpaque) BOOL opaque; // default==YES diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index 7bc5716de..83e333a01 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -324,6 +324,7 @@ - (void)_initializeInstance _contentsScaleForDisplay = ASScreenScale(); _drawingPriority = ASDefaultTransactionPriority; + _maskedCorners = kASCACornerAllCorners; _primitiveTraitCollection = ASPrimitiveTraitCollectionMakeDefault(); @@ -1526,17 +1527,17 @@ - (void)recursivelySetNeedsDisplayAtScale:(CGFloat)contentsScale - (void)_layoutClipCornersIfNeeded { ASDisplayNodeAssertMainThread(); - if (_clipCornerLayers[0] == nil) { + if (_clipCornerLayers[0] == nil && _clipCornerLayers[1] == nil && _clipCornerLayers[2] == nil && + _clipCornerLayers[3] == nil) { return; } - + CGSize boundsSize = self.bounds.size; for (int idx = 0; idx < NUM_CLIP_CORNER_LAYERS; idx++) { BOOL isTop = (idx == 0 || idx == 1); - BOOL isRight = (idx == 1 || idx == 2); + BOOL isRight = (idx == 1 || idx == 3); if (_clipCornerLayers[idx]) { - // Note the Core Animation coordinates are reversed for y; 0 is at the bottom. - _clipCornerLayers[idx].position = CGPointMake(isRight ? boundsSize.width : 0.0, isTop ? boundsSize.height : 0.0); + _clipCornerLayers[idx].position = CGPointMake(isRight ? boundsSize.width : 0.0, isTop ? 0.0 : boundsSize.height); [_layer addSublayer:_clipCornerLayers[idx]]; } } @@ -1546,78 +1547,87 @@ - (void)_updateClipCornerLayerContentsWithRadius:(CGFloat)radius backgroundColor { ASPerformBlockOnMainThread(^{ for (int idx = 0; idx < NUM_CLIP_CORNER_LAYERS; idx++) { - // Layers are, in order: Top Left, Top Right, Bottom Right, Bottom Left. + // Skip corners that aren't clipped (we have already set up & torn down layers based on maskedCorners.) + if (_clipCornerLayers[idx] == nil) { + continue; + } + + // Layers are, in order: Top Left, Top Right, Bottom Left, Bottom Right, which mirrors CACornerMask. // anchorPoint is Bottom Left at 0,0 and Top Right at 1,1. BOOL isTop = (idx == 0 || idx == 1); - BOOL isRight = (idx == 1 || idx == 2); - + BOOL isRight = (idx == 1 || idx == 3); + CGSize size = CGSizeMake(radius + 1, radius + 1); ASGraphicsBeginImageContextWithOptions(size, NO, self.contentsScaleForDisplay); - + CGContextRef ctx = UIGraphicsGetCurrentContext(); if (isRight == YES) { CGContextTranslateCTM(ctx, -radius + 1, 0); } - if (isTop == YES) { + if (isTop == NO) { CGContextTranslateCTM(ctx, 0, -radius + 1); } + UIBezierPath *roundedRect = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, radius * 2, radius * 2) cornerRadius:radius]; [roundedRect setUsesEvenOddFillRule:YES]; [roundedRect appendPath:[UIBezierPath bezierPathWithRect:CGRectMake(-1, -1, radius * 2 + 1, radius * 2 + 1)]]; [backgroundColor setFill]; [roundedRect fill]; - + // No lock needed, as _clipCornerLayers is only modified on the main thread. - CALayer *clipCornerLayer = _clipCornerLayers[idx]; + unowned CALayer *clipCornerLayer = _clipCornerLayers[idx]; clipCornerLayer.contents = (id)(ASGraphicsGetImageAndEndCurrentContext().CGImage); clipCornerLayer.bounds = CGRectMake(0.0, 0.0, size.width, size.height); - clipCornerLayer.anchorPoint = CGPointMake(isRight ? 1.0 : 0.0, isTop ? 1.0 : 0.0); + clipCornerLayer.anchorPoint = CGPointMake(isRight ? 1.0 : 0.0, isTop ? 0.0 : 1.0); } [self _layoutClipCornersIfNeeded]; }); } -- (void)_setClipCornerLayersVisible:(BOOL)visible +- (void)_setClipCornerLayersVisible:(CACornerMask)visibleCornerLayers { ASPerformBlockOnMainThread(^{ ASDisplayNodeAssertMainThread(); - if (visible) { - for (int idx = 0; idx < NUM_CLIP_CORNER_LAYERS; idx++) { - if (_clipCornerLayers[idx] == nil) { - static ASDisplayNodeCornerLayerDelegate *clipCornerLayers; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - clipCornerLayers = [[ASDisplayNodeCornerLayerDelegate alloc] init]; - }); - _clipCornerLayers[idx] = [[CALayer alloc] init]; - _clipCornerLayers[idx].zPosition = 99999; - _clipCornerLayers[idx].delegate = clipCornerLayers; - } - } - [self _updateClipCornerLayerContentsWithRadius:_cornerRadius backgroundColor:self.backgroundColor]; - } else { - for (int idx = 0; idx < NUM_CLIP_CORNER_LAYERS; idx++) { + for (int idx = 0; idx < NUM_CLIP_CORNER_LAYERS; idx++) { + BOOL visible = (0 != (visibleCornerLayers & (1 << idx))); + if (visible == (_clipCornerLayers[idx] != nil)) { + continue; + } else if (visible) { + static ASDisplayNodeCornerLayerDelegate *clipCornerLayers; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + clipCornerLayers = [[ASDisplayNodeCornerLayerDelegate alloc] init]; + }); + _clipCornerLayers[idx] = [[CALayer alloc] init]; + _clipCornerLayers[idx].zPosition = 99999; + _clipCornerLayers[idx].delegate = clipCornerLayers; + } else { [_clipCornerLayers[idx] removeFromSuperlayer]; _clipCornerLayers[idx] = nil; } } + [self _updateClipCornerLayerContentsWithRadius:_cornerRadius backgroundColor:self.backgroundColor]; }); } -- (void)updateCornerRoundingWithType:(ASCornerRoundingType)newRoundingType cornerRadius:(CGFloat)newCornerRadius +- (void)updateCornerRoundingWithType:(ASCornerRoundingType)newRoundingType + cornerRadius:(CGFloat)newCornerRadius + maskedCorners:(CACornerMask)newMaskedCorners { __instanceLock__.lock(); CGFloat oldCornerRadius = _cornerRadius; ASCornerRoundingType oldRoundingType = _cornerRoundingType; + CACornerMask oldMaskedCorners = _maskedCorners; _cornerRadius = newCornerRadius; _cornerRoundingType = newRoundingType; + _maskedCorners = newMaskedCorners; __instanceLock__.unlock(); - + ASPerformBlockOnMainThread(^{ ASDisplayNodeAssertMainThread(); - if (oldRoundingType != newRoundingType || oldCornerRadius != newCornerRadius) { + if (oldRoundingType != newRoundingType || oldCornerRadius != newCornerRadius || oldMaskedCorners != newMaskedCorners) { if (oldRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { if (newRoundingType == ASCornerRoundingTypePrecomposited) { self.layerCornerRadius = 0.0; @@ -1629,14 +1639,16 @@ - (void)updateCornerRoundingWithType:(ASCornerRoundingType)newRoundingType corne } else if (newRoundingType == ASCornerRoundingTypeClipping) { self.layerCornerRadius = 0.0; - [self _setClipCornerLayersVisible:YES]; + [self _setClipCornerLayersVisible:newMaskedCorners]; } else if (newRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { self.layerCornerRadius = newCornerRadius; + self.layerMaskedCorners = newMaskedCorners; } } else if (oldRoundingType == ASCornerRoundingTypePrecomposited) { if (newRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { self.layerCornerRadius = newCornerRadius; + self.layerMaskedCorners = newMaskedCorners; [self setNeedsDisplay]; } else if (newRoundingType == ASCornerRoundingTypePrecomposited) { @@ -1645,22 +1657,23 @@ - (void)updateCornerRoundingWithType:(ASCornerRoundingType)newRoundingType corne [self setNeedsDisplay]; } else if (newRoundingType == ASCornerRoundingTypeClipping) { - [self _setClipCornerLayersVisible:YES]; + [self _setClipCornerLayersVisible:newMaskedCorners]; [self setNeedsDisplay]; } } else if (oldRoundingType == ASCornerRoundingTypeClipping) { if (newRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { self.layerCornerRadius = newCornerRadius; - [self _setClipCornerLayersVisible:NO]; + [self _setClipCornerLayersVisible:kNilOptions]; } else if (newRoundingType == ASCornerRoundingTypePrecomposited) { - [self _setClipCornerLayersVisible:NO]; + [self _setClipCornerLayersVisible:kNilOptions]; [self displayImmediately]; } else if (newRoundingType == ASCornerRoundingTypeClipping) { - // Clip corners already exist, but the radius has changed. - [self _updateClipCornerLayerContentsWithRadius:newCornerRadius backgroundColor:self.backgroundColor]; + // Clip corners already exist, but the radius and/or maskedCorners have changed. + // This method will add & remove them, and subsequently redraw them. + [self _setClipCornerLayersVisible:newMaskedCorners]; } } } diff --git a/Source/Details/UIView+ASConvenience.h b/Source/Details/UIView+ASConvenience.h index 8c4b2f55c..19fd57695 100644 --- a/Source/Details/UIView+ASConvenience.h +++ b/Source/Details/UIView+ASConvenience.h @@ -22,6 +22,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) CGFloat zPosition; @property (nonatomic) CGPoint anchorPoint; @property (nonatomic) CGFloat cornerRadius; +@property (nonatomic) CACornerMask maskedCorners API_AVAILABLE(ios(11), tvos(11)); @property (nullable, nonatomic) id contents; @property (nonatomic, copy) NSString *contentsGravity; @property (nonatomic) CGRect contentsRect; diff --git a/Source/Private/ASDisplayNode+AsyncDisplay.mm b/Source/Private/ASDisplayNode+AsyncDisplay.mm index 31662bd70..68bf4107a 100644 --- a/Source/Private/ASDisplayNode+AsyncDisplay.mm +++ b/Source/Private/ASDisplayNode+AsyncDisplay.mm @@ -287,13 +287,15 @@ - (void)__willDisplayNodeContentWithRenderingContext:(CGContextRef)context drawP ASCornerRoundingType cornerRoundingType = _cornerRoundingType; CGFloat cornerRadius = _cornerRadius; ASDisplayNodeContextModifier willDisplayNodeContentWithRenderingContext = _willDisplayNodeContentWithRenderingContext; + CACornerMask maskedCorners = _maskedCorners; __instanceLock__.unlock(); if (cornerRoundingType == ASCornerRoundingTypePrecomposited && cornerRadius > 0.0) { ASDisplayNodeAssert(context == UIGraphicsGetCurrentContext(), @"context is expected to be pushed on UIGraphics stack %@", self); // TODO: This clip path should be removed if we are rasterizing. CGRect boundingBox = CGContextGetClipBoundingBox(context); - [[UIBezierPath bezierPathWithRoundedRect:boundingBox cornerRadius:cornerRadius] addClip]; + CGSize radii = CGSizeMake(cornerRadius, cornerRadius); + [[UIBezierPath bezierPathWithRoundedRect:boundingBox byRoundingCorners:maskedCorners cornerRadii:radii] addClip]; } if (willDisplayNodeContentWithRenderingContext) { @@ -313,6 +315,7 @@ - (void)__didDisplayNodeContentWithRenderingContext:(CGContextRef)context image: CGFloat cornerRadius = _cornerRadius; CGFloat contentsScale = _contentsScaleForDisplay; ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext = _didDisplayNodeContentWithRenderingContext; + CACornerMask maskedCorners = _maskedCorners; __instanceLock__.unlock(); if (context != NULL) { @@ -338,7 +341,10 @@ - (void)__didDisplayNodeContentWithRenderingContext:(CGContextRef)context image: ASDisplayNodeAssert(UIGraphicsGetCurrentContext(), @"context is expected to be pushed on UIGraphics stack %@", self); UIBezierPath *roundedHole = [UIBezierPath bezierPathWithRect:bounds]; - [roundedHole appendPath:[UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:cornerRadius * contentsScale]]; + CGSize radii = CGSizeMake(cornerRadius * contentsScale, cornerRadius * contentsScale); + [roundedHole appendPath:[UIBezierPath bezierPathWithRoundedRect:bounds + byRoundingCorners:maskedCorners + cornerRadii:radii]]; roundedHole.usesEvenOddFillRule = YES; UIBezierPath *roundedPath = nil; diff --git a/Source/Private/ASDisplayNode+UIViewBridge.mm b/Source/Private/ASDisplayNode+UIViewBridge.mm index 4f7e93140..45828d7bc 100644 --- a/Source/Private/ASDisplayNode+UIViewBridge.mm +++ b/Source/Private/ASDisplayNode+UIViewBridge.mm @@ -181,7 +181,9 @@ - (CGFloat)cornerRadius - (void)setCornerRadius:(CGFloat)newCornerRadius { - [self updateCornerRoundingWithType:self.cornerRoundingType cornerRadius:newCornerRadius]; + [self updateCornerRoundingWithType:self.cornerRoundingType + cornerRadius:newCornerRadius + maskedCorners:self.maskedCorners]; } - (ASCornerRoundingType)cornerRoundingType @@ -192,7 +194,20 @@ - (ASCornerRoundingType)cornerRoundingType - (void)setCornerRoundingType:(ASCornerRoundingType)newRoundingType { - [self updateCornerRoundingWithType:newRoundingType cornerRadius:self.cornerRadius]; + [self updateCornerRoundingWithType:newRoundingType cornerRadius:self.cornerRadius maskedCorners:self.maskedCorners]; +} + +- (CACornerMask)maskedCorners +{ + AS::MutexLocker l(__instanceLock__); + return _maskedCorners; +} + +- (void)setMaskedCorners:(CACornerMask)newMaskedCorners +{ + [self updateCornerRoundingWithType:self.cornerRoundingType + cornerRadius:self.cornerRadius + maskedCorners:newMaskedCorners]; } - (NSString *)contentsGravity @@ -983,6 +998,27 @@ - (void)setLayerCornerRadius:(CGFloat)newLayerCornerRadius _setToLayer(cornerRadius, newLayerCornerRadius); } +- (CACornerMask)layerMaskedCorners +{ + _bridge_prologue_read; + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { + return _getFromLayer(maskedCorners); + } else { + return kASCACornerAllCorners; + } +} + +- (void)setLayerMaskedCorners:(CACornerMask)newLayerMaskedCorners +{ + _bridge_prologue_write; + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { + _setToLayer(maskedCorners, newLayerMaskedCorners); + } else { + ASDisplayNodeAssert(newLayerMaskedCorners == kASCACornerAllCorners, + @"Cannot change maskedCorners property in iOS < 11 while using DefaultSlowCALayer rounding."); + } +} + - (BOOL)_locked_insetsLayoutMarginsFromSafeArea { ASAssertLocked(__instanceLock__); diff --git a/Source/Private/ASDisplayNodeInternal.h b/Source/Private/ASDisplayNodeInternal.h index b20470c6c..879639f16 100644 --- a/Source/Private/ASDisplayNodeInternal.h +++ b/Source/Private/ASDisplayNodeInternal.h @@ -73,6 +73,8 @@ AS_EXTERN NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimest #define VISIBILITY_NOTIFICATIONS_DISABLED_BITS 4 #define TIME_DISPLAYNODE_OPS 0 // If you're using this information frequently, try: (DEBUG || PROFILE) +static constexpr CACornerMask kASCACornerAllCorners = + kCALayerMinXMinYCorner | kCALayerMaxXMinYCorner | kCALayerMinXMaxYCorner | kCALayerMaxXMaxYCorner; #define NUM_CLIP_CORNER_LAYERS 4 @@ -216,6 +218,7 @@ AS_EXTERN NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimest CGFloat _cornerRadius; ASCornerRoundingType _cornerRoundingType; CALayer *_clipCornerLayers[NUM_CLIP_CORNER_LAYERS]; + CACornerMask _maskedCorners; ASDisplayNodeContextModifier _willDisplayNodeContentWithRenderingContext; ASDisplayNodeContextModifier _didDisplayNodeContentWithRenderingContext; @@ -336,8 +339,10 @@ AS_EXTERN NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimest /// Display the node's view/layer immediately on the current thread, bypassing the background thread rendering. Will be deprecated. - (void)displayImmediately; -/// Refreshes any precomposited or drawn clip corners, setting up state as required to transition radius or rounding type. -- (void)updateCornerRoundingWithType:(ASCornerRoundingType)newRoundingType cornerRadius:(CGFloat)newCornerRadius; +/// Refreshes any precomposited or drawn clip corners, setting up state as required to transition corner config. +- (void)updateCornerRoundingWithType:(ASCornerRoundingType)newRoundingType + cornerRadius:(CGFloat)newCornerRadius + maskedCorners:(CACornerMask)newMaskedCorners; /// Alternative initialiser for backing with a custom view class. Supports asynchronous display with _ASDisplayView subclasses. - (instancetype)initWithViewClass:(Class)viewClass; @@ -397,6 +402,9 @@ AS_EXTERN NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimest @property (nonatomic) CGFloat layerCornerRadius; +/// NOTE: Changing this to non-default under iOS < 11 will make an assertion (for the end user to see.) +@property (nonatomic) CACornerMask layerMaskedCorners; + - (BOOL)_locked_insetsLayoutMarginsFromSafeArea; @end diff --git a/Source/Private/_ASPendingState.mm b/Source/Private/_ASPendingState.mm index 5dca0ce5c..422ee6239 100644 --- a/Source/Private/_ASPendingState.mm +++ b/Source/Private/_ASPendingState.mm @@ -87,6 +87,7 @@ int setPreservesSuperviewLayoutMargins:1; int setInsetsLayoutMarginsFromSafeArea:1; int setActions:1; + int setMaskedCorners : 1; } ASPendingStateFlags; @@ -215,6 +216,7 @@ ASDISPLAYNODE_INLINE void ASPendingStateApplyMetricsToLayer(_ASPendingState *sta @synthesize preservesSuperviewLayoutMargins=preservesSuperviewLayoutMargins; @synthesize insetsLayoutMarginsFromSafeArea=insetsLayoutMarginsFromSafeArea; @synthesize actions=actions; +@synthesize maskedCorners = maskedCorners; static CGColorRef blackColorRef = NULL; static UIColor *defaultTintColor = nil; @@ -416,6 +418,12 @@ - (void)setCornerRadius:(CGFloat)newCornerRadius _flags.setCornerRadius = YES; } +- (void)setMaskedCorners:(CACornerMask)newMaskedCorners +{ + maskedCorners = newMaskedCorners; + _flags.setMaskedCorners = YES; +} + - (void)setContentMode:(UIViewContentMode)newContentMode { contentMode = newContentMode; @@ -890,6 +898,12 @@ - (void)applyToLayer:(CALayer *)layer if (flags.setCornerRadius) layer.cornerRadius = cornerRadius; + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { + if (flags.setMaskedCorners) { + layer.maskedCorners = maskedCorners; + } + } + if (flags.setContentMode) layer.contentsGravity = ASDisplayNodeCAContentsGravityFromUIContentMode(contentMode); diff --git a/Tests/ASDisplayNodeSnapshotTests.mm b/Tests/ASDisplayNodeSnapshotTests.mm index 489727e3b..ae1bce744 100644 --- a/Tests/ASDisplayNodeSnapshotTests.mm +++ b/Tests/ASDisplayNodeSnapshotTests.mm @@ -8,6 +8,7 @@ #import "ASSnapshotTestCase.h" #import +#import @interface ASDisplayNodeSnapshotTests : ASSnapshotTestCase @@ -33,4 +34,46 @@ - (void)testBasicHierarchySnapshotTesting ASSnapshotVerifyNode(node, nil); } +NS_INLINE UIImage *BlueImageMake(CGRect bounds) +{ + UIGraphicsBeginImageContextWithOptions(bounds.size, YES, 0); + [[UIColor blueColor] setFill]; + UIRectFill(bounds); + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return image; +} + +- (void)testPrecompositedCornerRounding +{ + for (CACornerMask c = 1; c <= kASCACornerAllCorners; c |= (c << 1)) { + auto node = [[ASImageNode alloc] init]; + auto bounds = CGRectMake(0, 0, 100, 100); + node.image = BlueImageMake(bounds); + node.frame = bounds; + node.cornerRoundingType = ASCornerRoundingTypePrecomposited; + node.backgroundColor = UIColor.greenColor; + node.maskedCorners = c; + node.cornerRadius = 15; + ASSnapshotVerifyNode(node, ([NSString stringWithFormat:@"%d", (int)c])); + } +} + +- (void)testClippingCornerRounding +{ + for (CACornerMask c = 1; c <= kASCACornerAllCorners; c |= (c << 1)) { + auto node = [[ASImageNode alloc] init]; + auto bounds = CGRectMake(0, 0, 100, 100); + node.image = BlueImageMake(bounds); + node.frame = bounds; + node.cornerRoundingType = ASCornerRoundingTypeClipping; + node.backgroundColor = UIColor.greenColor; + node.maskedCorners = c; + node.cornerRadius = 15; + // A layout pass is required, because that's where we lay out the clip layers. + [node.layer layoutIfNeeded]; + ASSnapshotVerifyNode(node, ([NSString stringWithFormat:@"%d", (int)c])); + } +} + @end diff --git a/Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testClippingCornerRounding@2x.png b/Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testClippingCornerRounding@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..aa72ff691a5134e6091a069c7c26b43efdb7742e GIT binary patch literal 1851 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yF%}28J29*~C-V}>;VkfoEM{O3 zQw3p0&mff#KtY*I7yl3-%>=|CkOIUSP#VZ)zMkrS$rVU1_H=O!sbG9_$2Kb@P^Rr; zb4OEetHzC;Jgob*91EDk@*Fo5F^4rpZ#c!qa`2S?h146IU9G(fI%J-oS20lezVF*{e_rle+2gxL4D9uj`0ricHgEoMffKi9@4Z`n@%GF63)ZvC zF8^6DOW?%o+k0c{-*0$ceB)f!=gqg2CY+yMy?a0Vw`Us+XUlKqWv;B;zWwuk*)M*55d#C>6Q=l;u<6RA2c{H$!I6J{^?K^;1-f$P(QFyLCi&g>7hdb z!yJa=O%Vc(94*WT84H+XxRV@Nlmz%#*bi_P=yb3Ob~soFjB223C@tpu!^*($|NnB;>cMjv2 zC}PPWw@ofNbX3G}W~q>HTyi< z0FbjISUE~M|4TuoC42Q1ZoZ@esgAZ50B%2OR&v=JLU5r10E${)3TWr3`BCzSx^Cq_ zl5{CaeObPeI{LLs&S25h;6VunuC=o=Cq)83jM=BSIU&0iEfF9hD#_xSfn{5b9avo@ z%%g z;e=J8bVrCMonxnP<2ln+4&umFu{}UMjUu-uGMtNbj;bpd&z&AOp*Als zL}wmBWRpV+QF=<}$KNRwSy{K&6m{Qg8 zdm4IgkH+>2RsWIUYdddzpl;oNkYm1(p2ZW)?fbWWZW4I*_H9s93BNU}!3e5w>cbSg zE1j|-4%>REE~ElEN~Gp~b(G1O1D}_4#rvM*L>izy)RmaAQM@0n&M(|8r&KIC^IDA@ zf@1mfE$X9unIwgMc2vSW;v*;Nex~-s=Gs~8>10oCcPuWZax;@Qj9o7DYNhwBXpXx3 z?j^VE9X@_GC_N9zzHVQW9V$0}WPmzS@Ef?@7c3yo{RrQ`C&2GI}* z2>t<8ae;Iy`;k5H8bn%FuL1r;-0Jbv=-{F`xwAfP^gPh{Q|#YcKo)Sl{?qq;OUBFQ zKcu&G#e-BJ**At=u~G~mq~G+uZ~N!ze2Asug&w+=3VbocTpe}OQ*eidx?`#AmVIN* zP*MKJHZRE+8WjmQXw-uv=ui|CPE!z?oz0*-P^G~E2DJTrt)pnEr*t6z17zOpn|n>a zOM^lo43H3vQC0pOwPbMYU-($g!Pc75L%%rYMNd>y9&d4YiMFmU z3^{cZvEt?$Q%>o7UW+&t=2_?sFu#KKY&F7Tj<5rrig4vqXegtV=0yv+fFY%@&t2*} z$v{J4NNH&I?aS3V%VLru*c^d0f+3iD6VI_OAFFJ#L4fJNcEDs#NTpN4HoHeMF9o^- ze0qHH;;}Ft>ysl;2HMCIiAE_9>nooZTy{Z#&HrIU=~xrXDrS_;5nyQ;=(f*Oj`P6{ zs*da~qh%SNbKr&B;ay2DDlOuOevp4S;hB)b|sa6>-2VjqqoZl&(bJ&BvI3M zC6TIRwy;@yb{UHt|0JaZa^?Rg=^qp7{xtzt{?!EGua;-k27F`G7&`x5%;6%|`O^|uJOQvc4Nd!Pu2tAooj8|% z+s0rP2BVBUmW9rujkX&kBMvq7Ci|6}N+mxR*E?$3UD!vz#a{XmPWZGIomB^I%@F~NpPYbR7+8KY7z|*+z&-_QKgou9t0(1;MVigw zulTZhX>Q2TDM9KpJPWqxzp#(1*`@AI@(>YhqnHfi(?t#MbN$J54SAU^vrt1w_9!>_ z;p9xob$`a;R88rQ?dIz5n@vX}v~fV2{m8WN#HdGjILUn#vd&-s zG~EIolnuwEEolitIJ( zXzf1m>`WX>6fDsAql5iYr3Q@p+3f9fkxboY`&%Z0_g%`oerbCk?|dFbRPi}45Wg|i t(n@$pEXEpo6ZDdg)oOKhyjBau0TvQmc-L2xBPD4Du(KvwaV;VkfoEM{O3 zQw3p0&mff#KtY*I7yl3-%>=|CkOIUSP#VZ)zMkrS$rVU1_H=O!sbG9_$2Kb@P^Rr; zb4OEetHzC;Jgob*91EDk@*Fo5F^4rpZ#c!qa`2S?h146IU9G(fI%J-oS20lezVF*{e_rle+2gxL4D9uj`0ricHgEoMffKi9@4Z`n@%GF63)ZvC zF8^6DOW?%o+k0c{-*0$ceB)f!=gqg2CY+yMy?a0Vw`Us+XUlKqWv;B;zWwuk*)M*55d#C>6Q=l;u<6RA2c{H$!I6J{^?K^;1-f$P(QFyLCi&g>7hdb z!yJa=O%Vc(94*WT84H+XxRV@Nlmz%#*bi_P=yb3Ob~soFjB223C@tpu!^*($|Nn;VkfoEM{O3 zQw3p0&mff#KtY*I7yl3-%>=|CkOIUSP#VZ)dHU$0C{T`Nqo<2wNCo4YJHA~pp%TYF zItwuS$#giS={X-<@5!PlUGPMV#Xax9#0O$VMa)eLgzcGj@uW8?GOIT!6`iT>_9|Vq zdY)f--qMBl<=g$r!^1<1{yqP1qh9;^o>cO@j6Yvb$=t8CI4(Z_{Ku1jE${s*?K7{R z{dlW;@t!~DpDo@ecbT2-xa^JZOJ%gCpITY8T1NZ2pJ`a_Bl~+Z9^#VL$cJr0?H`x|usqCY(9@D^J#A{|i#=xfZ4YHT*Qb8}&ks9GtImXV$7}7YSN)mXaK-Y=g2V%jXTPf4j!oh8 zh;4nEr2bH#ua|jh>)WZF-VU#VTnh{y95~l_%BA;d|E_uBGCUR=iWeAdWLa`gaFJdW zJ3BMq8<`b8A!gII`!c4nvvKQegTe~DWM4f Dv8;>H literal 0 HcmV?d00001 diff --git a/Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testClippingCornerRounding_7@2x.png b/Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testClippingCornerRounding_7@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..45d842d099ee65661b6464d2a63456c8563cdb83 GIT binary patch literal 2397 zcmeHH`#aMM7~gDcX=W}VePxP>S?G^C6dc<93i#P zMa{iV?w3>Q=!h9YDwi2Y#GHAa^TYWYdU~Jd^M2m<{XFk4pC3M{uFeh;;)>!B2t|8vAEF#2i zg$SYX9}8#r%+#xWLJeL|vLkxLL%)nUrTKa)bnp1 z&*sYM#lje6GFBpNR5(Kgrp`DN+njfsLCSCHSL%g1C0%!AFrg2iy^kWCGd2494qV?b~Laa;a-FhXUd8@Z3wieJD zaW*_fowDK-OxL>1#jnpfL2z5Vl&&xfBhRNZzR$eSEW)V^4W*o-X^t`2yt;HT_XsB6 zE2a#<$@ldbc7hf?lFI$OLD=34c`2&}!wK%>{E8uc)+%>msh`IisVc>$>iEB)$38A@ z>3zDI+`1i+>9zBS5{WCGi;p>&_i$!0w5#&q@=Q25wz>4uDb4!ygL&&`KInij^Wl=6 z))vCOC$m<6f!RB3pxXA2nbn8a_b#n2uMs$cmqGp`L7NFK)BXfMa)7~0F2jH1qtx!YKU)4T(fBU|hI~ zlHzNCk3afD{+?_Z#&1{&Gi+z4$e{k5Fuas@!)sy8EY0I5*_6rTYm8L7MgajO8jy`O zfgY~HDiENDJB|uFQD;Te6s1qp)sd3Tj#Or+pG_M*;bWG?QFf|WWPM=qcM|eE{o=0I z<61*$^fV~rG)4i5LaJYO|8*bVGQ$?b2*m1Oi!qsZJ`T&i^_IDe{hvHw^F#g~ZK2G9 zU(22T4tF>Gybw;EG9yO0qHdxva6k?CITYnmkF?2ZZ%O&-8d-&^8r7Vo7S4RG35~BA$*EJ-6zkMuHM0^U z(8Y_WFUXaTD9?m?PMo0Blrd-aeq}? zlx24teK?OUK@3YbZ+dn;*s4ivimjczjf#>AIli*Y5g<0qBG`JaNIf-P?Xq7vW&CrB z0(|3q?EYIE@kph7+-9g0{I>SWJdU(UJR@~)Xv>&!k`$I3&lW8-G$?8K3M1Z8dKZCt;Hl*v;NwncAC-l8;z1gBvPK-K9k|eOndh5>GX!1$y$kuT1xtW9s zQS0rCY${dfm1{Ks7T#!LR-OhwHRTBQyF>P~i_IOKMmM}3OBztE&mQ5hzUrm+lm;e9( literal 0 HcmV?d00001 diff --git a/Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding@2x.png b/Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0b4a6b425fb624d9988018c2ad4d02ef2e30a3a3 GIT binary patch literal 2145 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yF%}28J29*~C-V}>;VkfoEM{O3 zQw3p0&mff#KtY*I7yl3-%>=|CkOIUSP#VZ)`TKEiFHoN4q^FBxNCo4Yx%acr7D^nS zKhtpbq$f_xVjd)^iU{tWrKDS4r1*26kH~*aFTw}qrA3kR;Jr%I}RKo02 zYn!#i{Z^NJT_v+)?IpVfn|`*Q`em-Adss``Zne(eWtS|LT{2h~WWO-TJhHN5)dz*< zJvHnp*A>g4GmX8Tr| z>_1JvAg)>L|iVPDtE?qu*lCfi_gU+j3 zp{6^xS8w{^9@w6ycjW9*2T{ki=!&ge48@EQUw)qvSn_Adp9^Of@t>{XnA6=T+4QCS zgPOvJr%n2YYL{^u-CDBguV>4`nS!0gh6cV9Rv$0AJLNY+g$Ua+mY+LUe7eaiCZ1Y& zW@h_UO-H#m&usf`8kx)1%vsLz@?B!#MzQ1jKD}A3aw1~!scFwy=BXDH^-V4IoG^WM z$Sn7Wm^DW?uDAKE#d+zr>y64oYR@Yyr?XgA^&UMMEn|Ip)8R=xL8 zZONiPlAlfA3$i#SEYM*jszs zB558+lY&8lfrEvcg`jl*%vyh27DoXI0|^BgWtm$yw)ZMO>r~)iLN?-x^QaN9;2glP z@MQZ3EZhG7KfZM3G6n`_PEeut_NpP{0Rx^R2R^9ZRjLsDz#D6{YSRC`-)rr7EH=mp zbTA)mEMS<^uz6kAck6CtfyM#_83y$S5na~a8~<%}U^#@4J*_kgTe~DWM4fkPVYX literal 0 HcmV?d00001 diff --git a/Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding_15@2x.png b/Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding_15@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ba850b21eb92e06d780f6cc7ca4988a7e99fdac8 GIT binary patch literal 3119 zcmeH}iBnVA5{GXRAP^ymFae$>m@q0R49K8{h{)lh4#eF+4`s0fH4 zD1w5hED8z;VPBPXfUpK7FhN1qfNaX132O1)fAH#6y;Iev@BN+ob)D{Ws_Wjcv@n%{ zZG!;-AVV}Wu@d=)4U?1*&Dm#j9*P`@Ze?l&Yce57w(J z?+bAHLYPi$-}C9k!mE4twVhiCe=&OIY^Rg*onJbB5*n}2v(n&&qP|vce~-Iw;P@6ci{bNNiOWZ*52ANC_;JPAgvw$)k1pzhJKL9Aim{#a zac$~_I+=q#{qy=#9o;7e=(h7zy}6$|7jI|Ad04tAt(2@cn`ksO=DM&Kc4oP6*Hy#480+!}=-|c=16*)w)vkr$UA&Ij8BJt!o@)Guv6{0%h(Kdwg!|73ph#C)?xUqpJ}~N{#+;e;CZ0w8+h&b=OdRs-TJ~Wq2d!xW1{fGh$$y4ZS=_L|$ab!8d{<_B6gdks+W#=Xm(=>z zEn9b?W_UQdi7CfSdx=I21-(=dWQR`LRTB-uPFn0s%O}Jxjk*QM_?zL%NP^Hzkg06B5Vji4Ka+NX}V=6LM&y9X&nI59J?5Q72 z4noMGGZLDzdvwJmm{rU|$d1ac;u8^I2vkCc<6{uwaOOv6<R9`3+ABsexCDDKd zz=FkugquZbfpAzIqz;e-Sh)G5>I-4U29lC+F*u+SY8>8BY4oJ--X@}Qpp^3{Bpino z6N7@Gp>kEJMniTuD+lvUBsTHFXj^3s5(y52FdWcTrLj zRL!gc@x^Ev$R+FRU5av>$P*`p<++#p#IA<&4fp^JtV6!Os%GMLOKKCb%HOiKWUUA` zGWebwdaubkO8G$Dzmd#$;p?kv(wn)#QOZJC%r=KjB&loCC}bUVZ8Nw3iE`LMb07d9 z6zWCo7BzqIwvFa4NbY}57Pa<-#3drGC;SilvtdpdmOIBIVK>hD{*HWXc5FPv9v_e< zWNzDg@oI`+x-zUg^61IiA51NYhQBGGtTJ2~4H`k<8ZZ+Q1*!7j$DKiqdmMsql$fsrprP<&Ws7vxmIIrD zh3n~5rS$x7ci|hs+4I5cX2D}F#TpTuk-s#z6M!ZK1AYsFnI>W!^U4x zhoIB-`9n(@>PlqkMhC@?R3%35)zB>jRG_SFl(Ne5-)Wzz2FQ(ly>d2wJKUwrAL3iq zM#NqwOIJK_9m}j_8e(ZFV^u{V>dE&{X)S&H7pYSdoG!(p2KlWkkSdQ@dlYGRbcAUF zMyea%{b^@gLO5rhm3t9EOQiMIu?kl1-dX{@D^|el!>})MP@+Z_5qaZ;dScxV&%Lmc*?;2!4Egfb|9y_BKVxyZpWzZfXZ@ zuS+MXdt8fMPeH0EAsqR!Pw$7WuI$|LRrb0cb2N!An6;E2Z)d00tQP>|Vs&e0EmR>b z?bEFVGw~GRUU?+ADu@ej2zE>%EcdtLqq1M^pM`VWTzGXf!`9}t))|ArFRG+z^mUN< z(*2S5AcE#s-Fk?)9>0T3(yo9~WTN{9d<=7%mv{~G3srK>y`>iW0(2D2z>IAQ_z!!uInJm8B2Z;pjNWp6a;J)P5$7d$U%N47q z(=RvaLbUA-##JVHmxwIf7g<$Cg5TU8(GN>)6pN8U`Ml_Pq+gj_izhF?J_biPNsm_t zT*Rc8E<5<_mW>;f`$-@cm+$HO%7Jr&gDBFi>4;&gw+KBK4U9(b(RY~+XC(#4&6u6Y z3gL81#UDBE?ApHP5DXL)1kQ-UsSH0uW118@VkdUI$k!kBlT9A6Hg%YBTAzv8b&Te% z_1?O*5V0B?;hDERO--|7mBmwZ7@I#&+EM0AR1tQMo$`vp85z5jpnB4UdGjBl1s@Z< zc1~9K{xVhuGZx4nkDd_ZK%X!Mm(M&pFwqhN594N{y(>;7|8#cH#^c2rsUm2g8bfw-uy~_!A0SiK#w$NQ&OfmwH*)*;!z8e93%U%e*lY+ BX|DhP literal 0 HcmV?d00001 diff --git a/Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding_1@2x.png b/Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding_1@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0b4a6b425fb624d9988018c2ad4d02ef2e30a3a3 GIT binary patch literal 2145 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yF%}28J29*~C-V}>;VkfoEM{O3 zQw3p0&mff#KtY*I7yl3-%>=|CkOIUSP#VZ)`TKEiFHoN4q^FBxNCo4Yx%acr7D^nS zKhtpbq$f_xVjd)^iU{tWrKDS4r1*26kH~*aFTw}qrA3kR;Jr%I}RKo02 zYn!#i{Z^NJT_v+)?IpVfn|`*Q`em-Adss``Zne(eWtS|LT{2h~WWO-TJhHN5)dz*< zJvHnp*A>g4GmX8Tr| z>_1JvAg)>L|iVPDtE?qu*lCfi_gU+j3 zp{6^xS8w{^9@w6ycjW9*2T{ki=!&ge48@EQUw)qvSn_Adp9^Of@t>{XnA6=T+4QCS zgPOvJr%n2YYL{^u-CDBguV>4`nS!0gh6cV9Rv$0AJLNY+g$Ua+mY+LUe7eaiCZ1Y& zW@h_UO-H#m&usf`8kx)1%vsLz@?B!#MzQ1jKD}A3aw1~!scFwy=BXDH^-V4IoG^WM z$Sn7Wm^DW?uDAKE#d+zr>y64oYR@Yyr?XgA^&UMMEn|Ip)8R=xL8 zZONiPlAlfA3$i#SEYM*jszs zB558+lY&8lfrEvcg`jl*%vyh27DoXI0|^BgWtm$yw)ZMO>r~)iLN?-x^QaN9;2glP z@MQZ3EZhG7KfZM3G6n`_PEeut_NpP{0Rx^R2R^9ZRjLsDz#D6{YSRC`-)rr7EH=mp zbTA)mEMS<^uz6kAck6CtfyM#_83y$S5na~a8~<%}U^#@4J*_kgTe~DWM4fkPVYX literal 0 HcmV?d00001 diff --git a/Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding_3@2x.png b/Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding_3@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4282e29d394f52c6649c124e3175dc81bbdd54a9 GIT binary patch literal 2409 zcmeHHi&N5B6#fAwiio5`IR%9F(3Uo}heE7LNUMxS<>FegX}guL4VI+Ud=Y3ytC>B_ z%CObc%u3C)5FY_)Dq2dZk)ef~nU>@$QVVf$*Rj8$na<32zk9zk-^@8@?wr52(Y-8T zi(mi%SkkD|uzUs}n%}ZvqlgZp zZewVpPVx7*6$Y(uW1MnyvQNsh9!-QN z=T$4Rkppy?eR*T6A+cJq__Ng$KbNHmb)?hYO7<%2#1l=~R@Q4Oy-&+pedFDwHR_n) zkHn$O$1D48>TX@QxHjYw@geW&7;{Ke7E-dmZ`$)SH$y!#$rhfk)J8wnYTdeB4{(_h z6|2)u{Gg!=Yf!L0(!{a!sz-!(uDUDg-e&gM;)*R5KOebJHR3V>&m3ug_F^PWo$kYK zYpLpvuh-u5=;|}Py{m~87kET5OJA}&pzKR`D_BAAA~i(uAJ{)XPw~X~vxeB%h99tR zu2#kCD%35i?x*ai*tZs9AusaMq)KD8st3Jf?_t$EF;OaUKiJOFvBgoUDhzWlGOXzt zG}sp$5q(>ov!DI$1(*4OqmH^i0$;IEyNg-HUk^SN>JZh*p(4SbF8mgl|RjkU=%lXGQ)CtrvlM6qjsp= zLnEdG6aL$KZ+2l=i#s42cfNNW^15D+Q^<#6+5#kap;z0F`r4uDo~sKffKZMj;e@Y8 zy_us>UEo-s`0d#IMEJ3nS$o_N{rVIURuP@PkNko9$Yu<+fJnrNfBIf8C|X>ICYsAj z;aM66t@m*Ir%2wXxG5)qjDexB(IZN!q*2E!3TaSuRx>vvM`4t8mt&%QTE@eRNV@y4 zU#B$*Yy`O_Xsh11l7-qL{$P-tMo!pHcg`$*s-Q|dsx2k2^y|b9Y3a}cL7nP$L*RQn zq?^^~HYDBUiUZ%D7z*shSdC1Gr{Y86wWeoqGzFsn)AIc}VQy@6Tm~1rJVjorvdL{p z?T{`d$6QLaz;hp(`7Dvf`|4(AUm4 z6+I=*GZn}LcOgrp?UJo2UI63glE+IhDWLdFM514Gw}R$0#%S9rrl0uv&q+Z+TGg zooM@6J@|IV1G`zhE#i)))2!~=d*EKDq|TbSN7)Q2;{5i!y(Yk6T%Lajdi_8;LhvfY zzW)sX>S@IrV!Vk;C-<(_hsh8Olut%-a2&u`0%J1YW7<4}k3^!)&;T6}!cHGa@=kRD z+2IIC0w4nD;gLBW(=bo6nHjpN#To4LEcReB$Z1SdcQ5Sh7Vo84#$ve0G4oQ3vi^@le>znYnWU}A z*Vlis6m}*V^mzDOZwQqAH0<=Ey>k{(`yl0r0d;Jyx36LxLC|6d08CH2pIl*F{t)w- z<*w~;0m+=R-8OeN?D2&aByhA yL9-*7vtbx=(PC%i(0KkLz!aOd*W` literal 0 HcmV?d00001 diff --git a/Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding_7@2x.png b/Tests/ReferenceImages_iOS_10/ASDisplayNodeSnapshotTests/testPrecompositedCornerRounding_7@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..46ae9327a1e21a8493869f453fb153ff8bbd83a1 GIT binary patch literal 2825 zcmeHHdo+l2Ns<~TkEHQdsWI-jx8>e{@1J+AyVu&^{r!H{`s}s${(RSt*++L! zRa&P60DvluN^zFQ<`qXP%Fm+Tif_mR%-`983>3G436PRl{i zo3fW7k8ruK*za=R^drjeiVAE=%2xnT=%i70FoI#9vIgT09@L3;x9nzI-n5x;{v-MPZr}1dv!h`E+zk0P=zleA_$DGP6^}6pdx@`a5KXqVckrjQdT*B>?NVW}H z`S>%-`h5Eh1kak@;AJhopJ{?ndPOU7!fGLBVE8%fYO!#S@NbdVD`vKQRY{-edG&TC zaUj`|)!kMx7~UwUvU@Ws`}jm0D!6MG#4LH|+lyjlJQ4c3PXvms1!wSo|GpF2@PpuP${ z=BGe!(JPdv6P&|+IB?5@HSyM;Iqz#bx4!%9<@{v7eJAnX%Vp0g=AxF8lY#}qf;AOx z{>+e9zJDG(w`phNOhhJUKV7uPI>bhjiVyRR8mQ8KlNv_b)L3D5XE}nyC>HfIy>lZj zyMdLn23olRGm7J15AGi>e52>9+l%!j6`LqQ|dQacsW?NVkM?a;kTT0ix4C{LRV zMAt$#Q1r+wl{t%AaGM(ye4JB1t8(F8*3oU)57M|yd z$g%CqsUn+&x{;AY5nq#^dmmIA4!f@{DdK%{uceWq_R}rWOJ256%Iu!1mc5s56>LmQ zM&0E%^gV2Hn>0lX`ZjNysy<`|!Rx+Gxee;6&3qLsg?oid6t6(E7VL*_Mjp4kx3R#m zRDZltTy068W^P;jhmb3IgTfL&(_c4U5%a)~lm~nMU>(jpLPsH3%GLfhHn|B@ zE!+{L-4e)wP}pWz){(w)rBl6keRTnQBKzh5U+;7|JSWAV+@?%5u}klyTQuVjyOWE_ zU3|8b*dgJ)ovhqmQcvj{KNWv#lFq8n4M5?g&m=vSdTR6VEJtq)Wt^`QAwBeAI@)2o z+ezc5p{y`9P4M0|QPE2q1x0=h{|Q3Bx~FV!92|jCBz6Rn;tE|%y%2X*Q5!5evB=)p z*=C-~3O?Y^2(XU^{f!H?JXHziPn~VVQ1hq1xt06RwX(w0?mQ*hhLD zsl47Qc8zyEALI1M-SaV*@B{aJ*!#@j2j0zjth)IJZZ+&v)nC@2VSTjYIjo3%{B=*I z%f>IYcLGoaKFL`AdoRL=F#tByZYm3*T6b)=ebfFT7NLjYk#KB?4aiGiEZ(tO#-#Fa zI1mj2bRb$ODe^ouehbV1BI1ZZCO|(Gc*|~C$)1EpW06>3BhUVPb2a&Pi5w}@y0F^Y|HL@l zQN;)VSYEbG*dSm2$o!S%F3Eg%y7ib&O?GM87RMOfH0K;j8lj6Ngnka%O}lMoCquA= zLw3`-#!1PXyGG_hSAys#V`tIug=EhcK4X4wt864bq1j%IthO6_H7Z@IvcJr6Z}
    lRePW3jb>*kw1~`xn0ttwwbd$`c zelsK)B8rIsh{9$N+VUPurjyV}6o}%zuPMn>8uPPK=Ht*e< znqXogyD-0wo7qZ+kWq*PM2(;Lf$^U{N)yGHbEN+)q4fVH$c6atr%=w=g}Z>nUdQ6l zoa>z54p??^-=m`-v>e&0ifB5g>FqpQxcG&s9HFtI@i=J$U^@|=dZmo`29I&=6`d{g zPEMc_FdMsW54+9{a9Wu^z$rumCOSVoyC6yUDVVyVUmoH+Yc;1YSE{k1`gLrkodT$d zJz!~faJ^sISzILgJt>l$IULg&nJ$vRb(81Q}lmtym1mB3*1~UT>7cnL%d9<7o5LHxx$O zjTr33u|!+LJd9zb_uiywW1CwIDjw;iSZ$_>Hod@1$~W9%6caWTxt6LlGKsG$-wAad zPun3+nP|$uz7^L~$T1os6$C9Q+0i8EbAeANsG!llHVBg1L%m;&-f8C_w$174e7-Bj zwBxWoep#JrnLM;1Wq!$Ud`!v;Y0KaqPjl>iL56zwb!FSPnuJSCt|jJ!<;4$2@ Date: Sat, 30 Mar 2019 01:45:55 +0800 Subject: [PATCH 073/122] [ASImageNode]fix incorrect backing size calculation (#1189) * fix backing size for image node which content mode is scaleAspectFit * chore: update comments and naming * add change log * Update CHANGELOG.md * Update CHANGELOG.md Co-Authored-By: junjielu <348649634@qq.com> * add unit test for backing size calculation * correct license --- AsyncDisplayKit.xcodeproj/project.pbxproj | 5 ++ Source/Private/ASImageNode+CGExtras.mm | 47 ++++++------- Tests/ASImageNodeBackingSizeTests.mm | 80 +++++++++++++++++++++++ 3 files changed, 109 insertions(+), 23 deletions(-) create mode 100644 Tests/ASImageNodeBackingSizeTests.mm diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 2d9476e5f..7b7c708fc 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -101,6 +101,7 @@ 3917EBD41E9C2FC400D04A01 /* _ASCollectionReusableView.h in Headers */ = {isa = PBXBuildFile; fileRef = 3917EBD21E9C2FC400D04A01 /* _ASCollectionReusableView.h */; settings = {ATTRIBUTES = (Private, ); }; }; 3917EBD51E9C2FC400D04A01 /* _ASCollectionReusableView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3917EBD31E9C2FC400D04A01 /* _ASCollectionReusableView.mm */; }; 3C9C128519E616EF00E942A0 /* ASTableViewTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3C9C128419E616EF00E942A0 /* ASTableViewTests.mm */; }; + 471D04B1224CB98600649215 /* ASImageNodeBackingSizeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 471D04B0224CB98600649215 /* ASImageNodeBackingSizeTests.mm */; }; 4E9127691F64157600499623 /* ASRunLoopQueueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4E9127681F64157600499623 /* ASRunLoopQueueTests.mm */; }; 509E68601B3AED8E009B9150 /* ASScrollDirection.mm in Sources */ = {isa = PBXBuildFile; fileRef = 205F0E111B371BD7007741D0 /* ASScrollDirection.mm */; }; 509E68611B3AEDA0009B9150 /* ASAbstractLayoutController.h in Headers */ = {isa = PBXBuildFile; fileRef = 205F0E171B37339C007741D0 /* ASAbstractLayoutController.h */; }; @@ -685,6 +686,7 @@ 4640521B1A3F83C40061C0BA /* ASTableLayoutController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTableLayoutController.h; sourceTree = ""; }; 4640521C1A3F83C40061C0BA /* ASTableLayoutController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTableLayoutController.mm; sourceTree = ""; }; 4640521D1A3F83C40061C0BA /* ASLayoutController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutController.h; sourceTree = ""; }; + 471D04B0224CB98600649215 /* ASImageNodeBackingSizeTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASImageNodeBackingSizeTests.mm; sourceTree = ""; }; 4E9127681F64157600499623 /* ASRunLoopQueueTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASRunLoopQueueTests.mm; sourceTree = ""; }; 68355B2E1CB5799E001D4E68 /* ASImageNode+AnimatedImage.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASImageNode+AnimatedImage.mm"; sourceTree = ""; }; 68355B361CB57A5A001D4E68 /* ASPINRemoteImageDownloader.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASPINRemoteImageDownloader.mm; sourceTree = ""; }; @@ -1341,6 +1343,7 @@ 058D0A30195D057000B7D73C /* ASDisplayNodeTestsHelper.h */, 058D0A31195D057000B7D73C /* ASDisplayNodeTestsHelper.mm */, 697B31591CFE4B410049936F /* ASEditableTextNodeTests.mm */, + 471D04B0224CB98600649215 /* ASImageNodeBackingSizeTests.mm */, 056D21541ABCEF50001107EF /* ASImageNodeSnapshotTests.mm */, ACF6ED551B178DC700DA7C62 /* ASInsetLayoutSpecSnapshotTests.mm */, CCE4F9B21F0D60AC00062E4E /* ASIntegerMapTests.mm */, @@ -2215,6 +2218,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, Base, ); @@ -2323,6 +2327,7 @@ 058D0A3A195D057000B7D73C /* ASDisplayNodeTests.mm in Sources */, 9644CFE02193777C00213478 /* ASThrashUtility.m in Sources */, 696FCB311D6E46050093471E /* ASBackgroundLayoutSpecSnapshotTests.mm in Sources */, + 471D04B1224CB98600649215 /* ASImageNodeBackingSizeTests.mm in Sources */, CC583AD81EF9BDC300134156 /* OCMockObject+ASAdditions.mm in Sources */, 69FEE53D1D95A9AF0086F066 /* ASLayoutElementStyleTests.mm in Sources */, 4E9127691F64157600499623 /* ASRunLoopQueueTests.mm in Sources */, diff --git a/Source/Private/ASImageNode+CGExtras.mm b/Source/Private/ASImageNode+CGExtras.mm index c4f9fdcbc..db9871815 100644 --- a/Source/Private/ASImageNode+CGExtras.mm +++ b/Source/Private/ASImageNode+CGExtras.mm @@ -53,16 +53,17 @@ void ASCroppedImageBackingSizeAndDrawRectInBounds(CGSize sourceImageSize, // Per the API contract as commented in the header, we will adjust input parameters (destinationWidth, destinationHeight) to ensure that the image is not upscaled on the CPU. CGFloat boundsAspectRatio = (CGFloat)destinationWidth / (CGFloat)destinationHeight; - CGSize scaledSizeForImage = sourceImageSize; + CGSize minimumDestinationSize = sourceImageSize; BOOL cropToRectDimensions = !CGRectIsEmpty(cropRect); + // Given image size and container ratio, calculate minimum container size for the image under different contentMode if (cropToRectDimensions) { - scaledSizeForImage = CGSizeMake(boundsSize.width / cropRect.size.width, boundsSize.height / cropRect.size.height); + minimumDestinationSize = CGSizeMake(boundsSize.width / cropRect.size.width, boundsSize.height / cropRect.size.height); } else { if (contentMode == UIViewContentModeScaleAspectFill) - scaledSizeForImage = _ASSizeFillWithAspectRatio(boundsAspectRatio, sourceImageSize); + minimumDestinationSize = _ASSizeFitWithAspectRatio(boundsAspectRatio, sourceImageSize); else if (contentMode == UIViewContentModeScaleAspectFit) - scaledSizeForImage = _ASSizeFitWithAspectRatio(boundsAspectRatio, sourceImageSize); + minimumDestinationSize = _ASSizeFillWithAspectRatio(boundsAspectRatio, sourceImageSize); } // If fitting the desired aspect ratio to the image size actually results in a larger buffer, use the input values. @@ -70,9 +71,9 @@ void ASCroppedImageBackingSizeAndDrawRectInBounds(CGSize sourceImageSize, if (CGSizeEqualToSize(CGSizeZero, forcedSize) == NO) { destinationWidth = (size_t)round(forcedSize.width); destinationHeight = (size_t)round(forcedSize.height); - } else if (forceUpscaling == NO && (scaledSizeForImage.width * scaledSizeForImage.height) < (destinationWidth * destinationHeight)) { - destinationWidth = (size_t)round(scaledSizeForImage.width); - destinationHeight = (size_t)round(scaledSizeForImage.height); + } else if (forceUpscaling == NO && (minimumDestinationSize.width * minimumDestinationSize.height) < (destinationWidth * destinationHeight)) { + destinationWidth = (size_t)round(minimumDestinationSize.width); + destinationHeight = (size_t)round(minimumDestinationSize.height); if (destinationWidth == 0 || destinationHeight == 0) { *outBackingSize = CGSizeZero; *outDrawRect = CGRectZero; @@ -82,39 +83,39 @@ void ASCroppedImageBackingSizeAndDrawRectInBounds(CGSize sourceImageSize, // Figure out the scaled size within the destination bounds. CGFloat sourceImageAspectRatio = sourceImageSize.width / sourceImageSize.height; - CGSize scaledSizeForDestination = CGSizeMake(destinationWidth, destinationHeight); + CGSize scaledSizeForImage = CGSizeMake(destinationWidth, destinationHeight); if (cropToRectDimensions) { - scaledSizeForDestination = CGSizeMake(boundsSize.width / cropRect.size.width, boundsSize.height / cropRect.size.height); + scaledSizeForImage = CGSizeMake(boundsSize.width / cropRect.size.width, boundsSize.height / cropRect.size.height); } else { if (contentMode == UIViewContentModeScaleAspectFill) - scaledSizeForDestination = _ASSizeFillWithAspectRatio(sourceImageAspectRatio, scaledSizeForDestination); + scaledSizeForImage = _ASSizeFillWithAspectRatio(sourceImageAspectRatio, scaledSizeForImage); else if (contentMode == UIViewContentModeScaleAspectFit) - scaledSizeForDestination = _ASSizeFitWithAspectRatio(sourceImageAspectRatio, scaledSizeForDestination); + scaledSizeForImage = _ASSizeFitWithAspectRatio(sourceImageAspectRatio, scaledSizeForImage); } // Figure out the rectangle into which to draw the image. CGRect drawRect = CGRectZero; if (cropToRectDimensions) { - drawRect = CGRectMake(-cropRect.origin.x * scaledSizeForDestination.width, - -cropRect.origin.y * scaledSizeForDestination.height, - scaledSizeForDestination.width, - scaledSizeForDestination.height); + drawRect = CGRectMake(-cropRect.origin.x * scaledSizeForImage.width, + -cropRect.origin.y * scaledSizeForImage.height, + scaledSizeForImage.width, + scaledSizeForImage.height); } else { // We want to obey the origin of cropRect in aspect-fill mode. if (contentMode == UIViewContentModeScaleAspectFill) { - drawRect = CGRectMake(((destinationWidth - scaledSizeForDestination.width) * cropRect.origin.x), - ((destinationHeight - scaledSizeForDestination.height) * cropRect.origin.y), - scaledSizeForDestination.width, - scaledSizeForDestination.height); + drawRect = CGRectMake(((destinationWidth - scaledSizeForImage.width) * cropRect.origin.x), + ((destinationHeight - scaledSizeForImage.height) * cropRect.origin.y), + scaledSizeForImage.width, + scaledSizeForImage.height); } // And otherwise just center it. else { - drawRect = CGRectMake(((destinationWidth - scaledSizeForDestination.width) / 2.0), - ((destinationHeight - scaledSizeForDestination.height) / 2.0), - scaledSizeForDestination.width, - scaledSizeForDestination.height); + drawRect = CGRectMake(((destinationWidth - scaledSizeForImage.width) / 2.0), + ((destinationHeight - scaledSizeForImage.height) / 2.0), + scaledSizeForImage.width, + scaledSizeForImage.height); } } diff --git a/Tests/ASImageNodeBackingSizeTests.mm b/Tests/ASImageNodeBackingSizeTests.mm new file mode 100644 index 000000000..96d838414 --- /dev/null +++ b/Tests/ASImageNodeBackingSizeTests.mm @@ -0,0 +1,80 @@ +// +// ASImageNodeBackingSizeTests.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 + +#import +#import + +static CGSize _FitSizeWithAspectRatio(CGFloat imageRatio, CGSize backingSize); +static CGSize _FillSizeWithAspectRatio(CGFloat imageRatio, CGSize backingSize); + +static CGSize _FitSizeWithAspectRatio(CGFloat imageRatio, CGSize backingSize) +{ + CGFloat backingRatio = backingSize.width / backingSize.height; + // fit size should be constrained in backing size + if (imageRatio > backingRatio) { + return CGSizeMake(backingSize.width, backingSize.width / imageRatio); + } else { + return CGSizeMake(backingSize.height * imageRatio, backingSize.height); + } +} + +static CGSize _FillSizeWithAspectRatio(CGFloat imageRatio, CGSize backingSize) +{ + CGFloat backingRatio = backingSize.width / backingSize.height; + // backing size should be constrained in fill size + if (imageRatio > backingRatio) { + return CGSizeMake(backingSize.height * imageRatio, backingSize.height); + } else { + return CGSizeMake(backingSize.width, backingSize.width / imageRatio); + } +} + +@interface ASImageNodeBackingSizeTests : XCTestCase + +@end + +@implementation ASImageNodeBackingSizeTests + +- (void)setUp { + // Put setup code here. This method is called before the invocation of each test method in the class. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the class. +} + +// ScaleAspectFit mode calculation. +- (void)testScaleAspectFitModeBackingSizeCalculation { + CGSize imageSize = CGSizeMake(100, 100); + CGSize boundsSize = CGSizeMake(200, 400); + + CGSize backingSize = CGSizeZero; + CGRect imageDrawRect = CGRectZero; + + ASCroppedImageBackingSizeAndDrawRectInBounds(imageSize, boundsSize, UIViewContentModeScaleAspectFit, CGRectZero, false, CGSizeZero, &backingSize, &imageDrawRect); + CGSize backingSizeShouldBe = _FillSizeWithAspectRatio(boundsSize.width / boundsSize.height, imageSize); + CGSize drawRectSizeShouldBe = _FitSizeWithAspectRatio(imageSize.width / imageSize.height, backingSizeShouldBe); + XCTAssertTrue(CGSizeEqualToSize(backingSizeShouldBe, backingSize)); + XCTAssertTrue(CGSizeEqualToSize(drawRectSizeShouldBe, imageDrawRect.size)); +} + +// ScaleAspectFill mode calculation. +- (void)testScaleAspectFillModeBackingSizeCalculation { + CGSize imageSize = CGSizeMake(100, 100); + CGSize boundsSize = CGSizeMake(200, 400); + + CGSize backingSize = CGSizeZero; + CGRect imageDrawRect = CGRectZero; + + ASCroppedImageBackingSizeAndDrawRectInBounds(imageSize, boundsSize, UIViewContentModeScaleAspectFill, CGRectZero, false, CGSizeZero, &backingSize, &imageDrawRect); + CGSize backingSizeShouldBe = _FitSizeWithAspectRatio(boundsSize.width / boundsSize.height, imageSize); + CGSize drawRectSizeShouldBe = _FillSizeWithAspectRatio(imageSize.width / imageSize.height, backingSizeShouldBe); + XCTAssertTrue(CGSizeEqualToSize(backingSizeShouldBe, backingSize)); + XCTAssertTrue(CGSizeEqualToSize(drawRectSizeShouldBe, imageDrawRect.size)); +} + +@end From 976fd43fef620e1f777ef5f341ac1cb60e57b4a9 Mon Sep 17 00:00:00 2001 From: Adlai Holler Date: Fri, 29 Mar 2019 11:16:35 -0700 Subject: [PATCH 074/122] Have image nodes draw into opaque contexts automatically if possible (#1432) * Have image nodes draw into opaque contexts if the image is opaque and it fills the context * Call backingSize once --- Source/ASImageNode.mm | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/Source/ASImageNode.mm b/Source/ASImageNode.mm index 063617b5a..27e8e05f2 100644 --- a/Source/ASImageNode.mm +++ b/Source/ASImageNode.mm @@ -473,9 +473,17 @@ + (UIImage *)createContentsForkey:(ASImageNodeContentsKey *)key drawParameters:( return nil; } + // If the image is opaque, and the draw rect contains the bounds rect, we can use an opaque context. + UIImage *image = key.image; + const CGRect imageDrawRect = key.imageDrawRect; + const CGRect contextBounds = { CGPointZero, key.backingSize }; + const BOOL imageIsOpaque = ASImageAlphaInfoIsOpaque(CGImageGetAlphaInfo(image.CGImage)); + const BOOL imageFillsContext = CGRectContainsRect(imageDrawRect, contextBounds); + const BOOL contextIsOpaque = (imageIsOpaque && imageFillsContext) || key.isOpaque; + // Use contentsScale of 1.0 and do the contentsScale handling in boundsSizeInPixels so ASCroppedImageBackingSizeAndDrawRectInBounds // will do its rounding on pixel instead of point boundaries - ASGraphicsBeginImageContextWithOptions(key.backingSize, key.isOpaque, 1.0); + ASGraphicsBeginImageContextWithOptions(contextBounds.size, contextIsOpaque, 1.0); BOOL contextIsClean = YES; @@ -503,13 +511,12 @@ + (UIImage *)createContentsForkey:(ASImageNodeContentsKey *)key drawParameters:( // upon removal of the object from the set when the operation completes. // Another option is to have ASDisplayNode+AsyncDisplay coordinate these cases, and share the decoded buffer. // Details tracked in https://github.com/facebook/AsyncDisplayKit/issues/1068 - - UIImage *image = key.image; - BOOL canUseCopy = (contextIsClean || ASImageAlphaInfoIsOpaque(CGImageGetAlphaInfo(image.CGImage))); + + BOOL canUseCopy = (contextIsClean || imageIsOpaque); CGBlendMode blendMode = canUseCopy ? kCGBlendModeCopy : kCGBlendModeNormal; @synchronized(image) { - [image drawInRect:key.imageDrawRect blendMode:blendMode alpha:1]; + [image drawInRect:imageDrawRect blendMode:blendMode alpha:1]; } if (context && key.didDisplayNodeContentWithRenderingContext) { From c34573375c7a603ecb6a34addd7557742e939edf Mon Sep 17 00:00:00 2001 From: dirtmelon <0xffdirtmelon@gmail.com> Date: Mon, 1 Apr 2019 23:52:43 +0800 Subject: [PATCH 075/122] Fix typo in batch-fetching-api.md (#1437) --- docs/_docs/batch-fetching-api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/_docs/batch-fetching-api.md b/docs/_docs/batch-fetching-api.md index 5fd904469..916eb6666 100755 --- a/docs/_docs/batch-fetching-api.md +++ b/docs/_docs/batch-fetching-api.md @@ -109,7 +109,7 @@ This is where you should actually fetch data, be it from a web API or some local
     - (void)tableNode:(ASTableNode *)tableNode willBeginBatchFetchWithContext:(ASBatchContext *)context 
     {
    -  // Fetch data most of the time asynchronoulsy from an API or local database
    +  // Fetch data most of the time asynchronously from an API or local database
       NSArray *newPhotos = [SomeSource getNewPhotos];
     
       // Insert data into table or collection node
    @@ -124,7 +124,7 @@ This is where you should actually fetch data, be it from a web API or some local