[ASDisplayNode] Add locking to view and layer in ASDisplayNode (#3179)

* Add locking to view and layer in ASDisplayNode

* Another approach

* Some more

* Address comments
This commit is contained in:
Michael Schneider 2017-03-15 09:05:38 -07:00 committed by GitHub
parent bf41847665
commit ef2ed54d0b

View File

@ -575,7 +575,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
- (BOOL)_locked_shouldLoadViewOrLayer - (BOOL)_locked_shouldLoadViewOrLayer
{ {
return !(_hierarchyState & ASHierarchyStateRasterized); return !_flags.isDeallocating && !(_hierarchyState & ASHierarchyStateRasterized);
} }
- (UIView *)_locked_viewToLoad - (UIView *)_locked_viewToLoad
@ -633,61 +633,39 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
return layer; return layer;
} }
- (void)_loadViewOrLayerIsLayerBacked:(BOOL)isLayerBacked - (void)_locked_loadViewOrLayerIsLayerBacked:(BOOL)isLayerBacked
{ {
{ if (isLayerBacked) {
ASDN::MutexLocker l(__instanceLock__); TIME_SCOPED(_debugTimeToCreateView);
_layer = [self _locked_layerToLoad];
static int ASLayerDelegateAssociationKey;
if (_flags.isDeallocating) { /**
return; * CALayer's .delegate property is documented to be weak, but the implementation is actually assign.
} * Because our layer may survive longer than the node (e.g. if someone else retains it, or if the node
* begins deallocation on a background thread and it waiting for the -dealloc call to reach main), the only
if (![self _locked_shouldLoadViewOrLayer]) { * way to avoid a dangling pointer is to use a weak proxy.
return; */
} ASWeakProxy *instance = [ASWeakProxy weakProxyWithTarget:self];
_layer.delegate = (id<CALayerDelegate>)instance;
if (isLayerBacked) { objc_setAssociatedObject(_layer, &ASLayerDelegateAssociationKey, instance, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
TIME_SCOPED(_debugTimeToCreateView); } else {
_layer = [self _locked_layerToLoad]; TIME_SCOPED(_debugTimeToCreateView);
static int ASLayerDelegateAssociationKey; _view = [self _locked_viewToLoad];
_view.asyncdisplaykit_node = self;
/** _layer = _view.layer;
* CALayer's .delegate property is documented to be weak, but the implementation is actually assign.
* Because our layer may survive longer than the node (e.g. if someone else retains it, or if the node
* begins deallocation on a background thread and it waiting for the -dealloc call to reach main), the only
* way to avoid a dangling pointer is to use a weak proxy.
*/
ASWeakProxy *instance = [ASWeakProxy weakProxyWithTarget:self];
_layer.delegate = (id<CALayerDelegate>)instance;
objc_setAssociatedObject(_layer, &ASLayerDelegateAssociationKey, instance, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
} else {
TIME_SCOPED(_debugTimeToCreateView);
_view = [self _locked_viewToLoad];
_view.asyncdisplaykit_node = self;
_layer = _view.layer;
}
_layer.asyncdisplaykit_node = self;
self._locked_asyncLayer.asyncDelegate = self;
} }
_layer.asyncdisplaykit_node = self;
{ self._locked_asyncLayer.asyncDelegate = self;
TIME_SCOPED(_debugTimeToApplyPendingState);
[self _applyPendingStateToViewOrLayer];
}
{
TIME_SCOPED(_debugTimeToAddSubnodeViews);
[self _addSubnodeViewsAndLayers];
}
{
TIME_SCOPED(_debugTimeForDidLoad);
[self _didLoad];
}
} }
- (void)_didLoad - (void)_didLoad
{ {
ASDisplayNodeAssertMainThread();
ASDisplayNodeAssertLockUnownedByCurrentThread(__instanceLock__);
ASDisplayNodeLogEvent(self, @"didLoad"); ASDisplayNodeLogEvent(self, @"didLoad");
TIME_SCOPED(_debugTimeForDidLoad);
[self didLoad]; [self didLoad];
@ -729,14 +707,47 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
- (UIView *)view - (UIView *)view
{ {
ASDN::MutexLocker l(__instanceLock__);
ASDisplayNodeAssert(!_flags.layerBacked, @"Call to -view undefined on layer-backed nodes"); ASDisplayNodeAssert(!_flags.layerBacked, @"Call to -view undefined on layer-backed nodes");
if (_flags.layerBacked) { BOOL isLayerBacked = _flags.layerBacked;
if (isLayerBacked) {
return nil; return nil;
} }
if (_view == nil) { if (_view != nil) {
ASDisplayNodeAssertMainThread(); return _view;
[self _loadViewOrLayerIsLayerBacked:NO]; }
if (![self _locked_shouldLoadViewOrLayer]) {
return nil;
}
// Loading a view needs to happen on the main thread
ASDisplayNodeAssertMainThread();
[self _locked_loadViewOrLayerIsLayerBacked:isLayerBacked];
// 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.
if (_pendingViewState.hasSetNeedsLayout) {
// Need to unlock before calling setNeedsLayout to avoid deadlocks.
// MutexUnlocker will re-lock at the end of scope.
ASDN::MutexUnlocker u(__instanceLock__);
[self __setNeedsLayout];
}
[self _locked_applyPendingStateToViewOrLayer];
{
// The following methods should not be called with a lock
ASDN::MutexUnlocker u(__instanceLock__);
// No need for the lock as accessing the subviews or layers are always happening on main
[self _addSubnodeViewsAndLayers];
// A subclass hook should never be called with a lock
[self _didLoad];
} }
return _view; return _view;
@ -744,14 +755,47 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
- (CALayer *)layer - (CALayer *)layer
{ {
if (_layer == nil) { ASDN::MutexLocker l(__instanceLock__);
ASDisplayNodeAssertMainThread(); if (_layer != nil) {
return _layer;
}
if (!_flags.layerBacked) { BOOL isLayerBacked = _flags.layerBacked;
return self.view.layer; if (!isLayerBacked) {
} // No need for the lock and call the view explicitly in case it needs to be loaded first
ASDN::MutexUnlocker u(__instanceLock__);
return self.view.layer;
}
[self _loadViewOrLayerIsLayerBacked:YES]; if (![self _locked_shouldLoadViewOrLayer]) {
return nil;
}
// Loading a layer needs to happen on the main thread
ASDisplayNodeAssertMainThread();
[self _locked_loadViewOrLayerIsLayerBacked:isLayerBacked];
// 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.
if (_pendingViewState.hasSetNeedsLayout) {
// Need to unlock before calling setNeedsLayout to avoid deadlocks.
// MutexUnlocker will re-lock at the end of scope.
ASDN::MutexUnlocker u(__instanceLock__);
[self __setNeedsLayout];
}
[self _locked_applyPendingStateToViewOrLayer];
{
// The following methods should not be called with a lock
ASDN::MutexUnlocker u(__instanceLock__);
// No need for the lock as accessing the subviews or layers are always happening on main
[self _addSubnodeViewsAndLayers];
// A subclass hook should never be called with a lock
[self _didLoad];
} }
return _layer; return _layer;
@ -2793,6 +2837,8 @@ ASDISPLAYNODE_INLINE BOOL nodeIsInRasterizedTree(ASDisplayNode *node) {
{ {
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
TIME_SCOPED(_debugTimeToAddSubnodeViews);
for (ASDisplayNode *node in self.subnodes) { for (ASDisplayNode *node in self.subnodes) {
[self _addSubnodeSubviewOrSublayer:node]; [self _addSubnodeSubviewOrSublayer:node];
} }
@ -3761,17 +3807,16 @@ ASDISPLAYNODE_INLINE BOOL nodeIsInRasterizedTree(ASDisplayNode *node) {
#pragma mark - Pending View State #pragma mark - Pending View State
- (void)_applyPendingStateToViewOrLayer - (void)_locked_applyPendingStateToViewOrLayer
{ {
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
ASDisplayNodeAssert(self.nodeLoaded, @"must have a view or layer"); ASDisplayNodeAssert(self.nodeLoaded, @"must have a view or layer");
TIME_SCOPED(_debugTimeToApplyPendingState);
// If no view/layer properties were set before the view/layer were created, _pendingViewState will be nil and the default values // If no view/layer properties were set before the view/layer were created, _pendingViewState will be nil and the default values
// for the view/layer are still valid. // for the view/layer are still valid.
[self _locked_applyPendingViewState];
[self applyPendingViewState];
__instanceLock__.lock();
if (_flags.displaySuspended) { if (_flags.displaySuspended) {
self._locked_asyncLayer.displaySuspended = YES; self._locked_asyncLayer.displaySuspended = YES;
@ -3779,26 +3824,32 @@ ASDISPLAYNODE_INLINE BOOL nodeIsInRasterizedTree(ASDisplayNode *node) {
if (!_flags.displaysAsynchronously) { if (!_flags.displaysAsynchronously) {
self._locked_asyncLayer.displaysAsynchronously = NO; self._locked_asyncLayer.displaysAsynchronously = NO;
} }
__instanceLock__.unlock();
} }
- (void)applyPendingViewState - (void)applyPendingViewState
{ {
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
ASDN::MutexLocker l(__instanceLock__); ASDisplayNodeAssertLockUnownedByCurrentThread(__instanceLock__);
ASDN::MutexLocker l(__instanceLock__);
// FIXME: Ideally we'd call this as soon as the node receives -setNeedsLayout // 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 // but automatic subnode management would require us to modify the node tree
// in the background on a loaded node, which isn't currently supported. // in the background on a loaded node, which isn't currently supported.
if (_pendingViewState.hasSetNeedsLayout) { if (_pendingViewState.hasSetNeedsLayout) {
//Need to unlock before calling setNeedsLayout to avoid deadlocks. // Need to unlock before calling setNeedsLayout to avoid deadlocks.
//MutexUnlocker will re-lock at the end of scope. // MutexUnlocker will re-lock at the end of scope.
ASDN::MutexUnlocker u(__instanceLock__); ASDN::MutexUnlocker u(__instanceLock__);
[self __setNeedsLayout]; [self __setNeedsLayout];
} }
if (self.layerBacked) { [self _locked_applyPendingViewState];
}
- (void)_locked_applyPendingViewState
{
ASDisplayNodeAssertMainThread();
if (_flags.layerBacked) {
[_pendingViewState applyToLayer:self.layer]; [_pendingViewState applyToLayer:self.layer];
} else { } else {
BOOL specialPropertiesHandling = ASDisplayNodeNeedsSpecialPropertiesHandlingForFlags(_flags); BOOL specialPropertiesHandling = ASDisplayNodeNeedsSpecialPropertiesHandlingForFlags(_flags);