Skip to content

Commit ef2ed54

Browse files
authored
[ASDisplayNode] Add locking to view and layer in ASDisplayNode (facebookarchive#3179)
* Add locking to view and layer in ASDisplayNode * Another approach * Some more * Address comments
1 parent bf41847 commit ef2ed54

1 file changed

Lines changed: 122 additions & 71 deletions

File tree

Source/ASDisplayNode.mm

Lines changed: 122 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,7 @@ - (void)__unloadNode
575575

576576
- (BOOL)_locked_shouldLoadViewOrLayer
577577
{
578-
return !(_hierarchyState & ASHierarchyStateRasterized);
578+
return !_flags.isDeallocating && !(_hierarchyState & ASHierarchyStateRasterized);
579579
}
580580

581581
- (UIView *)_locked_viewToLoad
@@ -633,61 +633,39 @@ - (CALayer *)_locked_layerToLoad
633633
return layer;
634634
}
635635

636-
- (void)_loadViewOrLayerIsLayerBacked:(BOOL)isLayerBacked
636+
- (void)_locked_loadViewOrLayerIsLayerBacked:(BOOL)isLayerBacked
637637
{
638-
{
639-
ASDN::MutexLocker l(__instanceLock__);
640-
641-
if (_flags.isDeallocating) {
642-
return;
643-
}
644-
645-
if (![self _locked_shouldLoadViewOrLayer]) {
646-
return;
647-
}
648-
649-
if (isLayerBacked) {
650-
TIME_SCOPED(_debugTimeToCreateView);
651-
_layer = [self _locked_layerToLoad];
652-
static int ASLayerDelegateAssociationKey;
653-
654-
/**
655-
* CALayer's .delegate property is documented to be weak, but the implementation is actually assign.
656-
* Because our layer may survive longer than the node (e.g. if someone else retains it, or if the node
657-
* begins deallocation on a background thread and it waiting for the -dealloc call to reach main), the only
658-
* way to avoid a dangling pointer is to use a weak proxy.
659-
*/
660-
ASWeakProxy *instance = [ASWeakProxy weakProxyWithTarget:self];
661-
_layer.delegate = (id<CALayerDelegate>)instance;
662-
objc_setAssociatedObject(_layer, &ASLayerDelegateAssociationKey, instance, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
663-
} else {
664-
TIME_SCOPED(_debugTimeToCreateView);
665-
_view = [self _locked_viewToLoad];
666-
_view.asyncdisplaykit_node = self;
667-
_layer = _view.layer;
668-
}
669-
_layer.asyncdisplaykit_node = self;
670-
671-
self._locked_asyncLayer.asyncDelegate = self;
672-
}
673-
674-
{
675-
TIME_SCOPED(_debugTimeToApplyPendingState);
676-
[self _applyPendingStateToViewOrLayer];
677-
}
678-
{
679-
TIME_SCOPED(_debugTimeToAddSubnodeViews);
680-
[self _addSubnodeViewsAndLayers];
681-
}
682-
{
683-
TIME_SCOPED(_debugTimeForDidLoad);
684-
[self _didLoad];
638+
if (isLayerBacked) {
639+
TIME_SCOPED(_debugTimeToCreateView);
640+
_layer = [self _locked_layerToLoad];
641+
static int ASLayerDelegateAssociationKey;
642+
643+
/**
644+
* CALayer's .delegate property is documented to be weak, but the implementation is actually assign.
645+
* Because our layer may survive longer than the node (e.g. if someone else retains it, or if the node
646+
* begins deallocation on a background thread and it waiting for the -dealloc call to reach main), the only
647+
* way to avoid a dangling pointer is to use a weak proxy.
648+
*/
649+
ASWeakProxy *instance = [ASWeakProxy weakProxyWithTarget:self];
650+
_layer.delegate = (id<CALayerDelegate>)instance;
651+
objc_setAssociatedObject(_layer, &ASLayerDelegateAssociationKey, instance, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
652+
} else {
653+
TIME_SCOPED(_debugTimeToCreateView);
654+
_view = [self _locked_viewToLoad];
655+
_view.asyncdisplaykit_node = self;
656+
_layer = _view.layer;
685657
}
658+
_layer.asyncdisplaykit_node = self;
659+
660+
self._locked_asyncLayer.asyncDelegate = self;
686661
}
687662

688663
- (void)_didLoad
689664
{
665+
ASDisplayNodeAssertMainThread();
666+
ASDisplayNodeAssertLockUnownedByCurrentThread(__instanceLock__);
690667
ASDisplayNodeLogEvent(self, @"didLoad");
668+
TIME_SCOPED(_debugTimeForDidLoad);
691669

692670
[self didLoad];
693671

@@ -729,29 +707,95 @@ - (BOOL)_locked_isNodeLoaded
729707

730708
- (UIView *)view
731709
{
710+
ASDN::MutexLocker l(__instanceLock__);
711+
732712
ASDisplayNodeAssert(!_flags.layerBacked, @"Call to -view undefined on layer-backed nodes");
733-
if (_flags.layerBacked) {
713+
BOOL isLayerBacked = _flags.layerBacked;
714+
if (isLayerBacked) {
715+
return nil;
716+
}
717+
718+
if (_view != nil) {
719+
return _view;
720+
}
721+
722+
if (![self _locked_shouldLoadViewOrLayer]) {
734723
return nil;
735724
}
725+
726+
// Loading a view needs to happen on the main thread
727+
ASDisplayNodeAssertMainThread();
728+
[self _locked_loadViewOrLayerIsLayerBacked:isLayerBacked];
729+
730+
// FIXME: Ideally we'd call this as soon as the node receives -setNeedsLayout
731+
// but automatic subnode management would require us to modify the node tree
732+
// in the background on a loaded node, which isn't currently supported.
733+
if (_pendingViewState.hasSetNeedsLayout) {
734+
// Need to unlock before calling setNeedsLayout to avoid deadlocks.
735+
// MutexUnlocker will re-lock at the end of scope.
736+
ASDN::MutexUnlocker u(__instanceLock__);
737+
[self __setNeedsLayout];
738+
}
739+
740+
[self _locked_applyPendingStateToViewOrLayer];
741+
742+
{
743+
// The following methods should not be called with a lock
744+
ASDN::MutexUnlocker u(__instanceLock__);
736745

737-
if (_view == nil) {
738-
ASDisplayNodeAssertMainThread();
739-
[self _loadViewOrLayerIsLayerBacked:NO];
746+
// No need for the lock as accessing the subviews or layers are always happening on main
747+
[self _addSubnodeViewsAndLayers];
748+
749+
// A subclass hook should never be called with a lock
750+
[self _didLoad];
740751
}
741752

742753
return _view;
743754
}
744755

745756
- (CALayer *)layer
746757
{
747-
if (_layer == nil) {
748-
ASDisplayNodeAssertMainThread();
749-
750-
if (!_flags.layerBacked) {
751-
return self.view.layer;
752-
}
758+
ASDN::MutexLocker l(__instanceLock__);
759+
if (_layer != nil) {
760+
return _layer;
761+
}
762+
763+
BOOL isLayerBacked = _flags.layerBacked;
764+
if (!isLayerBacked) {
765+
// No need for the lock and call the view explicitly in case it needs to be loaded first
766+
ASDN::MutexUnlocker u(__instanceLock__);
767+
return self.view.layer;
768+
}
769+
770+
if (![self _locked_shouldLoadViewOrLayer]) {
771+
return nil;
772+
}
773+
774+
// Loading a layer needs to happen on the main thread
775+
ASDisplayNodeAssertMainThread();
776+
[self _locked_loadViewOrLayerIsLayerBacked:isLayerBacked];
777+
778+
// FIXME: Ideally we'd call this as soon as the node receives -setNeedsLayout
779+
// but automatic subnode management would require us to modify the node tree
780+
// in the background on a loaded node, which isn't currently supported.
781+
if (_pendingViewState.hasSetNeedsLayout) {
782+
// Need to unlock before calling setNeedsLayout to avoid deadlocks.
783+
// MutexUnlocker will re-lock at the end of scope.
784+
ASDN::MutexUnlocker u(__instanceLock__);
785+
[self __setNeedsLayout];
786+
}
787+
788+
[self _locked_applyPendingStateToViewOrLayer];
789+
790+
{
791+
// The following methods should not be called with a lock
792+
ASDN::MutexUnlocker u(__instanceLock__);
793+
794+
// No need for the lock as accessing the subviews or layers are always happening on main
795+
[self _addSubnodeViewsAndLayers];
753796

754-
[self _loadViewOrLayerIsLayerBacked:YES];
797+
// A subclass hook should never be called with a lock
798+
[self _didLoad];
755799
}
756800

757801
return _layer;
@@ -2793,6 +2837,8 @@ - (void)_addSubnodeViewsAndLayers
27932837
{
27942838
ASDisplayNodeAssertMainThread();
27952839

2840+
TIME_SCOPED(_debugTimeToAddSubnodeViews);
2841+
27962842
for (ASDisplayNode *node in self.subnodes) {
27972843
[self _addSubnodeSubviewOrSublayer:node];
27982844
}
@@ -3761,44 +3807,49 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
37613807

37623808
#pragma mark - Pending View State
37633809

3764-
- (void)_applyPendingStateToViewOrLayer
3810+
- (void)_locked_applyPendingStateToViewOrLayer
37653811
{
37663812
ASDisplayNodeAssertMainThread();
37673813
ASDisplayNodeAssert(self.nodeLoaded, @"must have a view or layer");
37683814

3815+
TIME_SCOPED(_debugTimeToApplyPendingState);
3816+
37693817
// If no view/layer properties were set before the view/layer were created, _pendingViewState will be nil and the default values
37703818
// for the view/layer are still valid.
3771-
3772-
[self applyPendingViewState];
3773-
3774-
__instanceLock__.lock();
3819+
[self _locked_applyPendingViewState];
37753820

37763821
if (_flags.displaySuspended) {
37773822
self._locked_asyncLayer.displaySuspended = YES;
37783823
}
37793824
if (!_flags.displaysAsynchronously) {
37803825
self._locked_asyncLayer.displaysAsynchronously = NO;
37813826
}
3782-
3783-
__instanceLock__.unlock();
37843827
}
37853828

37863829
- (void)applyPendingViewState
37873830
{
37883831
ASDisplayNodeAssertMainThread();
3832+
ASDisplayNodeAssertLockUnownedByCurrentThread(__instanceLock__);
3833+
37893834
ASDN::MutexLocker l(__instanceLock__);
3790-
37913835
// FIXME: Ideally we'd call this as soon as the node receives -setNeedsLayout
37923836
// but automatic subnode management would require us to modify the node tree
37933837
// in the background on a loaded node, which isn't currently supported.
37943838
if (_pendingViewState.hasSetNeedsLayout) {
3795-
//Need to unlock before calling setNeedsLayout to avoid deadlocks.
3796-
//MutexUnlocker will re-lock at the end of scope.
3839+
// Need to unlock before calling setNeedsLayout to avoid deadlocks.
3840+
// MutexUnlocker will re-lock at the end of scope.
37973841
ASDN::MutexUnlocker u(__instanceLock__);
37983842
[self __setNeedsLayout];
37993843
}
3844+
3845+
[self _locked_applyPendingViewState];
3846+
}
38003847

3801-
if (self.layerBacked) {
3848+
- (void)_locked_applyPendingViewState
3849+
{
3850+
ASDisplayNodeAssertMainThread();
3851+
3852+
if (_flags.layerBacked) {
38023853
[_pendingViewState applyToLayer:self.layer];
38033854
} else {
38043855
BOOL specialPropertiesHandling = ASDisplayNodeNeedsSpecialPropertiesHandlingForFlags(_flags);

0 commit comments

Comments
 (0)