-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adds support for having multiple interface state delegates. #979
Conversation
Hopefully in a performant way.
* The defualt state, ASInterfaceStateNone, means that the element is not predicted to be onscreen soon and | ||
* preloading should not be performed. Swift: use [] for the default behavior. | ||
*/ | ||
typedef NS_OPTIONS(NSUInteger, ASInterfaceState) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just moved, no changes.
ASInterfaceStateInHierarchy = ASInterfaceStateMeasureLayout | ASInterfaceStatePreload | ASInterfaceStateDisplay | ASInterfaceStateVisible, | ||
}; | ||
|
||
@protocol ASInterfaceStateDelegate <NSObject> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only change here is that these are now all optional.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also added hierarchyDisplayDidFinish
🚫 CI failed with log |
_interfaceDidEnterPreloadDelegates = [NSHashTable weakObjectsHashTable]; | ||
_interfaceDidExitPreloadDelegates = [NSHashTable weakObjectsHashTable]; | ||
_interfaceNodeDidLayoutDelegates = [NSHashTable weakObjectsHashTable]; | ||
_interfaceNodeDidLoadDelegates = [NSHashTable weakObjectsHashTable]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Rather than separate tables, I think it's fine to use
class_respondsToSelector
before each call. That call actually runs through the same message-send machinery as a normal call, plus it warms the impcache. It's plenty fast. - Let's use
-[NSHashTable initWithOptions:NSHashTableWeakMemory | NSHashTableObjectPointerPersonality capacity:0]
(0 means default.)
That'll let you remove this intermediary object and it won't be slower by any amount that matters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On second thought, using -respondsToSelector:
is only a tiny bit slower and allows for proxying, so that's probably the way to go. Still fast.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should have to create all these up front...since we would only have to use the respondsToSelector: once per class (not even once per delegate), we should get a significant win by relying on just one object tracking the delegates, and we could use a simple bitfield / NS_OPTIONS to cache the ASInterfaceStateDelegateMethodsImplemented (similar to the flags in ASCollection that cache what the delegate / datasource implement, but just an NS_OPTIONS).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In recent versions of Objective-C, respondsToSelector:
is truly very fast. It amortizes to 1 additional objc_msgSend, 3 static function invocations, and one inline-assembly routine. It also has the added benefit of checking-from and storing-to the ObjC impcache, so the subsequent message-send is optimized.
Writing our own bitfield-by-class would amount to us reimplementing that portion of the impcache, without the benefit of direct access to the class method table. And even accessing our table – say it were in a singleton registry object – would require retaining-releasing it unless we coded around that, which would easily subsume the entire rest of the lookup. The added boilerplate is also nontrivial.
So there is a win available here, but I believe it's small. Let's avoid these calls only if we can do it in a simple and tremendously fast & scalable way. If we can't, then we shouldn't worry about it – I believe we can absolutely achieve far superior optimizations by using resources for other parts of the framework (such as transfer-collections or more judicious applications of __unsafe_unretained
).
Implementations:
Source/ASDisplayNode.mm
Outdated
@@ -543,8 +543,8 @@ - (void)_didLoad | |||
for (ASDisplayNodeDidLoadBlock block in onDidLoadBlocks) { | |||
block(self); | |||
} | |||
|
|||
[_interfaceStateDelegate nodeDidLoad]; | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: No trailing white spaces.
Source/ASNodeController+Beta.m
Outdated
@@ -47,6 +47,11 @@ - (instancetype)init | |||
return self; | |||
} | |||
|
|||
- (void)dealloc | |||
{ | |||
[_weakNode removeInterfaceStateDelegate:self]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-removeInterfaceStateDelegate:
needs to be called on main. and objects are de-allocated off main by default (code here) But since node controller is an associated object of display node, if the node is de-allocated off main, the node controller will be as well. And since NSHashTable
is not thread-safe, we have to dispatch to main here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@garrettmoon I still think that we should dispatch to main here, no?
Source/ASDisplayNode.mm
Outdated
// Not a fan of lazy loading, but this method won't get called very often and avoiding | ||
// the overhead of creating this is probably worth it. | ||
if (_interfaceStateDelegateManager == nil) { | ||
[[ASDisplayNodeInterfaceDelegateManager alloc] init]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing an assign to _interfaceStateDelegateManager
?
Source/ASDisplayNode.h
Outdated
@@ -292,6 +262,24 @@ AS_EXTERN NSInteger const ASDefaultDrawingPriority; | |||
*/ | |||
@property (readonly) ASInterfaceState interfaceState; | |||
|
|||
/** | |||
* @abstract Adds a delegate to recieve notifications on interfaceState changes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Typo: "recieve"
Source/ASDisplayNode.h
Outdated
- (void)addInterfaceStateDelegate:(id <ASInterfaceStateDelegate>)interfaceStateDelegate; | ||
|
||
/** | ||
* @abstract Removes a delegate from recieving notifications on interfaceState changes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Typo: "recieving"
This could be pretty useful! Could you share some background on the use case? I've found ASNodeController quite valuable in the cases I've come across, but maybe it doesn't work for the case you have. I think we can design an ideal solution if we have the same / general use case in mind to know where we need to scale. It seems maybe more generalized than needed to track an arbitrary number of delegates. |
@appleguy We have two use cases, both related to tracking performance. It'd be ideal if there were separate managers for each of these which could monitor the states. |
🚫 CI failed with log |
Source/ASDisplayNode.mm
Outdated
@@ -433,6 +433,9 @@ - (void)dealloc | |||
|
|||
// TODO: Remove this? If supernode isn't already nil, this method isn't dealloc-safe anyway. | |||
[self _setSupernode:nil]; | |||
|
|||
ASPerformMainThreadDeallocation(&_interfaceStateDelegates); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is necessary. If the node is gone, _interfaceStateDelegates
can be safely deallocated as well, especially because it only stores weak references.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, sorry, I thought the comment about the dealloc was here, now I get what you mean, thank you :)
Source/ASNodeController+Beta.m
Outdated
@@ -47,6 +47,11 @@ - (instancetype)init | |||
return self; | |||
} | |||
|
|||
- (void)dealloc | |||
{ | |||
[_weakNode removeInterfaceStateDelegate:self]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@garrettmoon I still think that we should dispatch to main here, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me.
…roup#979) * Adds support for having multiple interface state delegates. Hopefully in a performant way. * Switch to respondsToSelector for int del instead of separate object * Add CHANGELOG * Make ASDisplayNode+InterfaceState.h public * Huy's comments * Don't even bother removing since it's a weak hash table.
Hopefully in a performant way.