Skip to content

Native Animated - Restore default values when removing props on iOS #11819

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

Conversation

janicduplessis
Copy link
Contributor

@janicduplessis janicduplessis commented Jan 10, 2017

This fixes a bug that causes properties to keep stale values because they were not restored to their default after being removed when their value was controlled by native animated.

To fix this we restore default values in disconnectFromView by updating views with null values for all props that we modified previously. However this causes another issue where we lose any props that were set by the normal process because NativeAnimated operations are always executed after UIManager operatations. To fix this I added a way to hook into UIManager view updating process to be able to execute NativeAnimated operations either before or after updating native views.

In the case of disconnecting we want to do it before updating views so that it does: Value changed by native animated -> value restored to default -> (optional) value updated by normal prop.

This PR also depends on #10658.

Test plan
Tested that this fixed a particular bug in an app that uses ex-navigation + native animations where a navbar would not get it's opacity restored to 1 when an opacity prop driven by native animated was removed. Also tested that other animations still worked properly as well as the examples in UIExplorer.

@facebook-github-bot
Copy link
Contributor

By analyzing the blame information on this pull request, we identified @ryangomba and @buba447 to be potential reviewers.

@facebook-github-bot facebook-github-bot added GH Review: review-needed CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. labels Jan 10, 2017
@janicduplessis janicduplessis force-pushed the native-animations-default-values branch from 566cde7 to 46b073f Compare January 12, 2017 05:01
@janicduplessis janicduplessis force-pushed the native-animations-default-values branch from 46b073f to c31d6ff Compare January 27, 2017 21:00
@janicduplessis
Copy link
Contributor Author

#10658 landed so this is ready for review, cc @ryangomba @ide @javache

@@ -33,6 +45,17 @@ - (void)connectToView:(NSNumber *)viewTag

- (void)disconnectFromView:(NSNumber *)viewTag
{
// Restore the default value for all props that were modified by this node.
for (NSString *key in [_propsDictionary allKeys]) {
[_propsDictionary setObject:[NSNull null] forKey:key];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use modern syntax: _propsDictionary[key] = [NSNull null]

@@ -33,6 +45,17 @@ - (void)connectToView:(NSNumber *)viewTag

- (void)disconnectFromView:(NSNumber *)viewTag
{
// Restore the default value for all props that were modified by this node.
for (NSString *key in [_propsDictionary allKeys]) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allKeys is documented as a property, write _propsDictionary.allKeys: https://developer.apple.com/reference/foundation/nsdictionary/1409150-allkeys?language=objc


} else if ([parentNode isKindOfClass:[RCTValueAnimatedNode class]]) {
NSString *property = [self propertyNameForParentTag:parentTag];
CGFloat value = [(RCTValueAnimatedNode *)parentNode value];
[props setObject:@(value) forKey:property];
[_propsDictionary setObject:@(value) forKey:property];
}

}];

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there are props that were previously set, but now are no longer set -- that is, if the old value of _propsDictionary has keys that wouldn't be in props (in the old code) -- should we set those entries in _propsDictionary to [NSNull null]?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A props node always keep the same props, if the props change we create a new one. See https://github.com/facebook/react-native/blob/master/Libraries/Animated/src/AnimatedImplementation.js#L1777 which is called from componentWillReceiveProps.

[nodesManager connectAnimatedNodeToView:nodeTag viewTag:viewTag viewName:viewName];
}];
}

RCT_EXPORT_METHOD(disconnectAnimatedNodeFromView:(nonnull NSNumber *)nodeTag
viewTag:(nonnull NSNumber *)viewTag)
{
[_operations addObject:^(RCTNativeAnimatedNodesManager *nodesManager) {
// Disconnecting a view also restores it's default values so we have to make
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's -> its

NSMutableArray<AnimatedOperation> *_preOperations;
// Lock used for _operations and _preOperations. This is needed since operations
// are added from the UIManager queue and executed from the main queue.
NSLock *_operationsLock;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to come up with a solution that doesn't use the lock on each operation to further ensure the code is correct, plus perhaps better performance due to less contention.

My concern is that we may process half a JS frame's worth of operations, rendering an inconsistent state to the user, regardless of whether the JS thread runs faster than the UI thread or vice versa. I believe we should treat all the operations in a given JS frame as one atomic set -- the code should be structured in a way that makes it impossible to do otherwise.

Here is one idea: we split _operations into two arrays (same for _preOperations): NSMutableArray<AnimOp> *_operations and NSMutableArray<NSArray<AnimOp> *> *_operationLists. The first array is accessible only from the UIManager queue and the second only from the native main queue (mostly).

The UIManager queue still writes to _operations the same way. When we know it has written all of its operations for the current JS frame, we run:

// On the UI queue
[_operationsLock lock];
// Add both operation sets to be accessible from the main queue
[_preOperationsLists addObject[_preOperations];
[_operationsLists addObject:_operations];
// We are on the UIManager queue at the end of the frame, this is safe:
_preOperations = [NSMutableArray new];
_operations = [NSMutableArray new];
[_operationsLock unlock];

There's a little trickiness on the main queue to also use the above lock (plus probably more efficient solutions than what I proposed).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, working on a fix.

@@ -723,6 +725,16 @@ - (void)_amendPendingUIBlocksWithStylePropagationUpdateForShadowView:(RCTShadowV
}
}

- (void)addUIManagerObserver:(id<RCTUIManagerObserver>)observer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@javache any reservations about adding this API?

@janicduplessis janicduplessis force-pushed the native-animations-default-values branch from cb68c83 to 0a15f9a Compare February 17, 2017 22:48
@janicduplessis
Copy link
Contributor Author

@ide Updated, this adds a staged queue that we update only at the end of the batch. This avoid having to use locking when adding operations and makes sure operations are batched properly.

@cychiuae
Copy link

Any chance this PR will be included in 0.42?

@janicduplessis
Copy link
Contributor Author

Would love to, ping @ide

- (void)batchDidComplete
{
NSArray *operations = _operations;
_operations = [NSMutableArray new];
[_operationsLock lock];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add an assertion or a comment about which queue this runs on. It's important that this runs on the the method queue (RCTGetUIManagerQueue in practice) because we don't want [self addOperationBlock:] and this method to run concurrently. I think it might run on the JS queue..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK it will run on the same queue as the module specifies, will double check.

_stagedOperations = [NSMutableArray new];
_preOperations = [NSMutableArray new];
_stagedPreOperations = [NSMutableArray new];
_operationsLock = [NSLock new];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about naming this _stagedOperationsLock since it's only (I believe) the staged operation arrays that are accessed from different queues (UIManager queue and main queue)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea you are right

_stagedOperations = [NSMutableArray new];
[_operationsLock unlock];

[operations enumerateObjectsUsingBlock:^(AnimatedOperation operation, NSUInteger i, BOOL *stop) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use for in instead of enumerateObjectsUsingBlock? Same above too.

@ide
Copy link
Contributor

ide commented Feb 24, 2017

I realized there might be a potential bug with the locking, imagine this:

  1. UIManager notifies observers that it willFlushUIBlocks. We acquire the operations lock, safely get the preOperations, release the lock, and execute the preOperations.
  2. The JS frame happens to finish and batchDidComplete is called. We acquire the operations lock, add new operations to the preOperations and operations arrays, and release the lock.
  3. UIManager flushes its UI blocks. Then it notifies observers that it didFlushUIBlocks. We acquire the operations lock, get all the operations (including the ones that were just added in step 2), release the lock, and execute the operations.

The problem is that we've run the preOperations for frame 1 but the operations for frames 1 and 2. Maybe this doesn't ever happen but I think it can because the UIManager does its work on the main queue while batchDidComplete is called from the JS thread.


// Copy the observers set to avoid having to lock for too long while running ui blocks.
[_uiManagerObserversLock lock];
NSSet *observers = [[NSSet alloc] initWithSet:_uiManagerObservers];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just do [_uiManagerObservers copy]

@@ -1183,6 +1201,16 @@ - (void)flushUIBlocks
RCTProfileBeginFlowEvent();
dispatch_async(dispatch_get_main_queue(), ^{
RCTProfileEndFlowEvent();

// Copy the observers set to avoid having to lock for too long while running ui blocks.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More importantly this avoids needing to think about concurrent modification of the observers set while the observers run (i.e. when an observer adds or removes other observers)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea this actually avoids a deadlock too haha, will update the comment a bit.

}];
[self->_nodesManager updateAnimations];
});
[self.bridge.uiManager addUIManagerObserver:batch];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should go back to your first approach of adding just one observer. The issue here is that the observers are stored in an NSSet, which doesn't preserve order. So if batchDidComplete is called twice, the observer set might store the OperationBatch objects in the wrong order.

So I think it would be better to have one NSMutableArray<OperationBatch *> in RCTNativeAnimatedModule and make the module be the only observer.

btw I think this batch approach is good, this is what we do for GL calls in exgl.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call about set ordering, didnt think about that, I'll try the approach you suggested.

@@ -723,6 +727,20 @@ - (void)_amendPendingUIBlocksWithStylePropagationUpdateForShadowView:(RCTShadowV
}
}

- (void)addUIManagerObserver:(id<RCTUIManagerObserver>)observer
{
[_uiManagerObserversLock lock];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would also be safe to remove the lock and replace it with dispatch_async(main queue) since the main queue is a serial queue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's preferable that adding observers is synchronous, with the current impl that adds an observer for each batch this is needed otherwise the observer could be added after the UI operations are completed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The order of operations that currently touch the lock (addUIManagerObserver, removeUIManagerObserver, the code within flushUIBlocks) should still happen in the same order with dispatch_async, which keeps the order of blocks (it uses FIFO). I believe that "otherwise the observer could be added after the UI operations are completed" is about as equally likely with the locks as with dispatch_async.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The case Im worried about is:
UIManager batchDidComplete -> dispatch async the ui operations block
NativeAnimatedModule batchDidComplete -> dispatch async addObserver.

In this case the observer would get added after operations are executed. It's preferable not to assume the order in which the batchDidComplete methods get executed in.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still have a similar bug with the locks:

  1. [UIManager batchDidComplete] -> dispatch_async to the main queue to run the UI blocks
  2. Main thread starts running and acquires the observers lock
  3. [NativeAnimatedModule batchDidComplete] -> acquires the observers lock

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea I see, so now it's a race instead of happening all the time. Anyway if I make the change to use one observer this locking won't be needed anymore.

@@ -224,6 +224,8 @@ @implementation RCTUIManager
NSDictionary *_componentDataByName;

NSMutableSet<id<RCTComponent>> *_bridgeTransactionListeners;
NSMutableSet<id<RCTUIManagerObserver>> *_uiManagerObservers;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should hold weak references -- otherwise I think there is a cycle from the animation module -> bridge -> UIManager.

You might be able to use [NSPointerArray weakObjectsPointerArray] or an NSArray of [NSValue valueWithNonretainedObjectValue:] but be very careful especially around the semantics of what happens when the values are dealloc'd and whether the references are properly zeroed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also trying to get the array to behave well might not be a good use of time now, you could change the property to be just __weak id<RCTUIManagerObserver> _observer similar to a traditional delegate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we properly remove the observer when we dealloc the animated module it shouldn't be an issue right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, are you sure the animated module eventually gets deallocated? Might be good to verify that dealloc is called.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I can verify that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume all native modules get deallocated at the same time, I guess the bridge is where it could leak.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Native modules never seemed to get deallocated during runtime so it shouldn't be an issue, even if they did shouldn't leak since we are removing the observer.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They get dealloc'd upon reload, make sure you test that case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did test reloading but dealloc never got called for neither UIManager nor AnimatedModule.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well makes sense that dealloc never gets called if it leaks lol. Moved the code that cleans up the module to invalidate and now everything gets dealloc'ed properly on reload.

@janicduplessis
Copy link
Contributor Author

I've also noticed that this bug is present on android too, didn't notice it because I am using different transitions on Android. Working on a similar fix.

@janicduplessis
Copy link
Contributor Author

@ide Ok I think all race conditions are fixed.

@@ -224,6 +224,8 @@ @implementation RCTUIManager
NSDictionary *_componentDataByName;

NSMutableSet<id<RCTComponent>> *_bridgeTransactionListeners;
NSMutableSet<id<RCTUIManagerObserver>> *_uiManagerObservers;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They get dealloc'd upon reload, make sure you test that case.

[_operationBatches removeAllObjects];
[_operationBatchesLock unlock];

for (OperationBatch *batch in _stagedOperationBatches) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I'm concerned about (please sanity check me, would be great if I'm mistaken), is that we might have multiple operation batches for several JS frames (ex: batches for JS frames 1 and 2) and if the main thread is lagging, then when the UIManager flushes the UI blocks for native/main-thread frame 1, we'll run the pre/post-operations for JS frames 1 and 2.

Sorry this is so tricky...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea you are right, I checked how UIManager works it it will dispatch to the main queue once per batch and execute only the operations from that batch instead of every it has available. I think what could work well here is only process one batch at a time, this will work because for each completed batch there will be a call to the UIManagerObserver methods.

@janicduplessis
Copy link
Contributor Author

@ide Ok, we're getting there! 😅

Tested that the module properly gets dealloc'ed on reload and batches should be processed properly now.

@janicduplessis
Copy link
Contributor Author

So I made things a lot harder than they should have been, I realized we can just leverage the UIManager operation queue to schedule our operations when we want. I simply added some API to also be able to add a block at the start of the queue.

@janicduplessis janicduplessis force-pushed the native-animations-default-values branch from 9465c1a to eb5152f Compare March 4, 2017 23:24
@janicduplessis
Copy link
Contributor Author

@ide Added the extra comments you mentioned.

@ide
Copy link
Contributor

ide commented Mar 22, 2017

@facebook-github-bot shipit

@facebook-github-bot
Copy link
Contributor

@ide has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator.

@javache
Copy link
Member

javache commented Mar 23, 2017

This is not landing internally because we have -Wimplicit-retain-self turned on. Any reference to an ivar inside a block will need to be prefixed with self->

@janicduplessis
Copy link
Contributor Author

@javache Thanks! Could you sync up clang flags between OSS and fb internal build to avoid these import failures in the future?

@janicduplessis
Copy link
Contributor Author

@ide @javache Updated to use explicit self retains, pending blocks are nil'ed in UIManager invalidate so there is no risks of leaks here, tested that both RCTUIManager and RCTNativeAnimatedModule get dealloc'ed on app reload to make sure.

@ide
Copy link
Contributor

ide commented Mar 24, 2017

@facebook-github-bot shipit

@facebook-github-bot
Copy link
Contributor

@ide has imported this pull request. If you are a Facebook employee, you can view this diff on Phabricator.

@janicduplessis
Copy link
Contributor Author

@javache Could you check why this is not landing?

@nihgwu
Copy link
Contributor

nihgwu commented Mar 28, 2017

@janicduplessis just test with the master branch on my app, seems this commit breaks Native Animation, then I revert this commit locally, it works perfectly as before

@janicduplessis
Copy link
Contributor Author

janicduplessis commented Mar 28, 2017

@nihgwu Thanks for testing this, looks like there is an issue when there is an value listener. Looking into it.

@nihgwu
Copy link
Contributor

nihgwu commented Mar 28, 2017

with the latest master:
nr3
nr4

after revert this commit:
nr2

You can see that the stickySectionheader is flickering and the stickyheader( implemented in Native Animation ) is jumping

@nihgwu
Copy link
Contributor

nihgwu commented Mar 28, 2017

I've to say that this commit should solve some of the issues when I play with Native Animation, for example the flashing screen issue when navigate using NavigationExperimental I guess, so I've been waiting for this PR so long

Thanks for you quick feedback, I believe you can solve the problem soon , and great thanks for your work on Native Animation

@nihgwu
Copy link
Contributor

nihgwu commented Mar 28, 2017

I've tried to remove the listener but still the same

@janicduplessis
Copy link
Contributor Author

It's actually related to rerendering, I should have a fix soon.

facebook-github-bot pushed a commit that referenced this pull request Mar 28, 2017
Summary:
This call got lost in the refactor of #11819 and caused some view updating issues.
Closes #13183

Differential Revision: D4787565

Pulled By: javache

fbshipit-source-id: 8f7d456824c67abee6ac1d5f906e4c831ede889b
@janicduplessis janicduplessis deleted the native-animations-default-values branch March 31, 2017 20:08
thotegowda pushed a commit to thotegowda/react-native that referenced this pull request May 7, 2017
Summary:
This fixes a bug that causes properties to keep stale values because they were not restored to their default after being removed when their value was controlled by native animated.

To fix this we restore default values in `disconnectFromView` by updating views with null values for all props that we modified previously. However this causes another issue where we lose any props that were set by the normal process because NativeAnimated operations are always executed after UIManager operatations. To fix this I added a way to hook into UIManager view updating process to be able to execute NativeAnimated operations either before or after updating native views.

In the case of disconnecting we want to do it before updating views so that it does: Value changed by native animated -> value restored to default -> (optional) value updated by normal prop.

This PR also depends on facebook#10658.

**Test plan**
Tested that this fixed a particular bug in an app that uses ex-navigation + native animations where a navbar w
Closes facebook#11819

Differential Revision: D4752566

Pulled By: javache

fbshipit-source-id: 68ee28200ffeba859ae1b98ac753bd7dcb8910f0
thotegowda pushed a commit to thotegowda/react-native that referenced this pull request May 7, 2017
Summary:
This call got lost in the refactor of facebook#11819 and caused some view updating issues.
Closes facebook#13183

Differential Revision: D4787565

Pulled By: javache

fbshipit-source-id: 8f7d456824c67abee6ac1d5f906e4c831ede889b
facebook-github-bot pushed a commit that referenced this pull request Aug 4, 2017
Summary:
Same as #11819 but for Android. I didn't notice the bug initially in my app because I was using different animations on Android which did not trigger this issue.

**Test plan**
Created a simple repro example and tested that this fixes it. https://gist.github.com/janicduplessis/0f3eb362dae63fedf99a0d3ee041796a
Closes #12842

Differential Revision: D5556439

Pulled By: hramos

fbshipit-source-id: d13f4ad258d03cca46c793751ebc49d942b99152
facebook-github-bot pushed a commit that referenced this pull request May 29, 2020
Summary:
Changelog: [Internal]

# Problem

Before every update, restoreDefaults is called on animated nodes. In paper this makes sure no stale properties are on animated nodes. In paper it works fine because restoreDefaults is called before mounting and animations are triggered after mounting within single commit.

Details: #11819

In Fabric however it is called outside of other mounting operations and it applies default values to the view and then re-applies animated values.

Reviewed By: JoshuaGross

Differential Revision: D21786765

fbshipit-source-id: a2cb6d6d9cbd39d4c403c97c2f51e7d92078102f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Platform: iOS iOS applications.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants