Skip to content

Commit 20b4879

Browse files
Luna Weifacebook-github-bot
Luna Wei
authored andcommitted
- LayoutAnimations cause invalid view operations
Summary: [Android] [Fixed] - LayoutAnimations cause invalid view operations The dating team has found a consistent repro where following an order of steps will get you the following exception: https://lookaside.facebook.com/intern/pixelcloudnew/asset/?id=2113362972287761 The exception is due to the fact that the following operation `delete child tag 17 from parent tag 481 which is located at index 2` cannot be performed because parent tag 481 only has 2 children.. and they also happen to not have the tag 17 as a child. Somehow the operations and the tree they act upon are out of sync. RN receives operations from React via the native module `UIManagerModule`. The operations use tags (an identifier for a view) and indices to determine what view is updated. These operations (grouped together as a batch) are then passed to the UI thread where we perform them on the platform views. LayoutAnimations are implemented per batch in RN. When LayoutAnimations are on, qualified view updates are animated. Because the delete operation is animated, RN doesn't remove the view from the platform view hierarchy immediately but asynchronously -- after the animation is complete. This is problematic for other view operations that rely on an index on where to insert or delete a view because during the creation of those operations, it was assumed all prior operations were performed synchronously. This is what was occurring in the repro and there were silent view errors occurring before the exception was thrown. This diff proposes a solution to track all pending delete operations across operations. We introduce a map that is keyed on view tags and has a value of a sparse array that represents child index -> count of views that are pending deletion. `{11: [0=1], 481: [2=1]}` where this would be interpreted as for index 0 under view tag 11, there is one async view deletion that has not completed and under tag 481 there is one async view deletion at index 2. We use the map to adjust indices on add / delete operations. We also update the count when the deletion animation is complete and remove the tag from the map when it is deleted. Regarding worst case sizing of the map => O(# of platform views rendered) Reviewed By: mdvacca Differential Revision: D14529038 fbshipit-source-id: 86d8982845e25a2b23d6d89ce27852fd77dbb060
1 parent 946f1a6 commit 20b4879

File tree

4 files changed

+78
-21
lines changed

4 files changed

+78
-21
lines changed

ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java

+56-11
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import android.os.Build;
1212
import android.util.SparseArray;
1313
import android.util.SparseBooleanArray;
14+
import android.util.SparseIntArray;
1415
import android.view.Menu;
1516
import android.view.MenuItem;
1617
import android.view.View;
@@ -35,6 +36,8 @@
3536
import com.facebook.react.uimanager.layoutanimation.LayoutAnimationListener;
3637
import com.facebook.systrace.Systrace;
3738
import com.facebook.systrace.SystraceMessage;
39+
import java.util.HashMap;
40+
import java.util.Map;
3841
import javax.annotation.Nullable;
3942
import javax.annotation.concurrent.NotThreadSafe;
4043

@@ -73,6 +76,7 @@ public class NativeViewHierarchyManager {
7376
private final JSResponderHandler mJSResponderHandler = new JSResponderHandler();
7477
private final RootViewManager mRootViewManager;
7578
private final LayoutAnimationController mLayoutAnimator = new LayoutAnimationController();
79+
private final Map<Integer, SparseIntArray> mTagsToPendingIndicesToDelete = new HashMap<>();
7680

7781
private boolean mLayoutAnimationEnabled;
7882
private PopupMenu mPopupMenu;
@@ -336,19 +340,49 @@ private static String constructManageChildrenErrorMessage(
336340
return stringBuilder.toString();
337341
}
338342

343+
/**
344+
* Given an index to action on under synchronous deletes, return an updated index factoring in
345+
* asynchronous deletes (where the async delete operations have not yet been performed)
346+
*/
347+
private int normalizeIndex(int index, SparseIntArray pendingIndices) {
348+
int normalizedIndex = index;
349+
for (int i = 0; i <= index; i++) {
350+
normalizedIndex += pendingIndices.get(i);
351+
}
352+
return normalizedIndex;
353+
}
354+
355+
/**
356+
* Given React tag, return sparse array of direct child indices that are pending deletion (due to
357+
* async view deletion)
358+
*/
359+
private SparseIntArray getOrCreatePendingIndicesToDelete(int tag) {
360+
SparseIntArray pendingIndicesToDelete = mTagsToPendingIndicesToDelete.get(tag);
361+
if (pendingIndicesToDelete == null) {
362+
pendingIndicesToDelete = new SparseIntArray();
363+
mTagsToPendingIndicesToDelete.put(tag, pendingIndicesToDelete);
364+
}
365+
return pendingIndicesToDelete;
366+
}
367+
339368
/**
340369
* @param tag react tag of the node we want to manage
341370
* @param indicesToRemove ordered (asc) list of indicies at which view should be removed
342371
* @param viewsToAdd ordered (asc based on mIndex property) list of tag-index pairs that represent
343372
* a view which should be added at the specified index
344373
* @param tagsToDelete list of tags corresponding to views that should be removed
374+
* @param indicesToDelete parallel list to tagsToDelete, list of indices of those tags
345375
*/
346376
public synchronized void manageChildren(
347377
int tag,
348378
@Nullable int[] indicesToRemove,
349379
@Nullable ViewAtIndex[] viewsToAdd,
350-
@Nullable int[] tagsToDelete) {
380+
@Nullable int[] tagsToDelete,
381+
@Nullable int[] indicesToDelete) {
351382
UiThreadUtil.assertOnUiThread();
383+
384+
final SparseIntArray pendingIndicesToDelete = getOrCreatePendingIndicesToDelete(tag);
385+
352386
final ViewGroup viewToManage = (ViewGroup) mTagsToViews.get(tag);
353387
final ViewGroupManager viewManager = (ViewGroupManager) resolveViewManager(tag);
354388
if (viewToManage == null) {
@@ -405,15 +439,16 @@ public synchronized void manageChildren(
405439
tagsToDelete));
406440
}
407441

408-
View viewToRemove = viewManager.getChildAt(viewToManage, indexToRemove);
442+
int normalizedIndexToRemove = normalizeIndex(indexToRemove, pendingIndicesToDelete);
443+
View viewToRemove = viewManager.getChildAt(viewToManage, normalizedIndexToRemove);
409444

410445
if (mLayoutAnimationEnabled &&
411446
mLayoutAnimator.shouldAnimateLayout(viewToRemove) &&
412447
arrayContains(tagsToDelete, viewToRemove.getId())) {
413448
// The view will be removed and dropped by the 'delete' layout animation
414449
// instead, so do nothing
415450
} else {
416-
viewManager.removeViewAt(viewToManage, indexToRemove);
451+
viewManager.removeViewAt(viewToManage, normalizedIndexToRemove);
417452
}
418453

419454
lastIndexToRemove = indexToRemove;
@@ -435,13 +470,15 @@ public synchronized void manageChildren(
435470
viewsToAdd,
436471
tagsToDelete));
437472
}
438-
viewManager.addView(viewToManage, viewToAdd, viewAtIndex.mIndex);
473+
int normalizedIndexToAdd = normalizeIndex(viewAtIndex.mIndex, pendingIndicesToDelete);
474+
viewManager.addView(viewToManage, viewToAdd, normalizedIndexToAdd);
439475
}
440476
}
441477

442478
if (tagsToDelete != null) {
443479
for (int i = 0; i < tagsToDelete.length; i++) {
444480
int tagToDelete = tagsToDelete[i];
481+
final int indexToDelete = indicesToDelete[i];
445482
final View viewToDestroy = mTagsToViews.get(tagToDelete);
446483
if (viewToDestroy == null) {
447484
throw new IllegalViewOperationException(
@@ -457,13 +494,20 @@ public synchronized void manageChildren(
457494

458495
if (mLayoutAnimationEnabled &&
459496
mLayoutAnimator.shouldAnimateLayout(viewToDestroy)) {
460-
mLayoutAnimator.deleteView(viewToDestroy, new LayoutAnimationListener() {
461-
@Override
462-
public void onAnimationEnd() {
463-
viewManager.removeView(viewToManage, viewToDestroy);
464-
dropView(viewToDestroy);
465-
}
466-
});
497+
int updatedCount = pendingIndicesToDelete.get(indexToDelete, 0) + 1;
498+
pendingIndicesToDelete.put(indexToDelete, updatedCount);
499+
mLayoutAnimator.deleteView(
500+
viewToDestroy,
501+
new LayoutAnimationListener() {
502+
@Override
503+
public void onAnimationEnd() {
504+
viewManager.removeView(viewToManage, viewToDestroy);
505+
dropView(viewToDestroy);
506+
507+
int count = pendingIndicesToDelete.get(indexToDelete, 0);
508+
pendingIndicesToDelete.put(indexToDelete, Math.max(0, count - 1));
509+
}
510+
});
467511
} else {
468512
dropView(viewToDestroy);
469513
}
@@ -580,6 +624,7 @@ protected synchronized void dropView(View view) {
580624
}
581625
viewGroupManager.removeAllViews(viewGroup);
582626
}
627+
mTagsToPendingIndicesToDelete.remove(view.getId());
583628
mTagsToViews.remove(view.getId());
584629
mTagsToViewManagers.remove(view.getId());
585630
}

ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyOptimizer.java

+9-5
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,15 @@ public void handleManageChildren(
145145
int[] indicesToRemove,
146146
int[] tagsToRemove,
147147
ViewAtIndex[] viewsToAdd,
148-
int[] tagsToDelete) {
148+
int[] tagsToDelete,
149+
int[] indicesToDelete) {
149150
if (!ENABLED) {
150151
mUIViewOperationQueue.enqueueManageChildren(
151152
nodeToManage.getReactTag(),
152153
indicesToRemove,
153154
viewsToAdd,
154-
tagsToDelete);
155+
tagsToDelete,
156+
indicesToDelete);
155157
return;
156158
}
157159

@@ -276,9 +278,10 @@ private void removeNodeFromParent(ReactShadowNode nodeToRemove, boolean shouldDe
276278

277279
mUIViewOperationQueue.enqueueManageChildren(
278280
nativeNodeToRemoveFrom.getReactTag(),
279-
new int[]{index},
281+
new int[] {index},
280282
null,
281-
shouldDelete ? new int[]{nodeToRemove.getReactTag()} : null);
283+
shouldDelete ? new int[] {nodeToRemove.getReactTag()} : null,
284+
shouldDelete ? new int[] {index} : null);
282285
} else {
283286
for (int i = nodeToRemove.getChildCount() - 1; i >= 0; i--) {
284287
removeNodeFromParent(nodeToRemove.getChildAt(i), shouldDelete);
@@ -301,7 +304,8 @@ private void addNonLayoutNode(
301304
mUIViewOperationQueue.enqueueManageChildren(
302305
parent.getReactTag(),
303306
null,
304-
new ViewAtIndex[]{new ViewAtIndex(child.getReactTag(), index)},
307+
new ViewAtIndex[] {new ViewAtIndex(child.getReactTag(), index)},
308+
null,
305309
null);
306310
}
307311

ReactAndroid/src/main/java/com/facebook/react/uimanager/UIImplementation.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ public void manageChildren(
347347
int[] indicesToRemove = new int[numToMove + numToRemove];
348348
int[] tagsToRemove = new int[indicesToRemove.length];
349349
int[] tagsToDelete = new int[numToRemove];
350+
int[] indicesToDelete = new int[numToRemove];
350351

351352
if (numToMove > 0) {
352353
Assertions.assertNotNull(moveFrom);
@@ -380,6 +381,7 @@ public void manageChildren(
380381
indicesToRemove[numToMove + i] = indexToRemove;
381382
tagsToRemove[numToMove + i] = tagToRemove;
382383
tagsToDelete[i] = tagToRemove;
384+
indicesToDelete[i] = indexToRemove;
383385
}
384386
}
385387

@@ -423,7 +425,8 @@ public void manageChildren(
423425
indicesToRemove,
424426
tagsToRemove,
425427
viewsToAdd,
426-
tagsToDelete);
428+
tagsToDelete,
429+
indicesToDelete);
427430
}
428431

429432
for (int i = 0; i < tagsToDelete.length; i++) {

ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java

+9-4
Original file line numberDiff line numberDiff line change
@@ -210,16 +210,19 @@ private final class ManageChildrenOperation extends ViewOperation {
210210
private final @Nullable int[] mIndicesToRemove;
211211
private final @Nullable ViewAtIndex[] mViewsToAdd;
212212
private final @Nullable int[] mTagsToDelete;
213+
private final @Nullable int[] mIndicesToDelete;
213214

214215
public ManageChildrenOperation(
215216
int tag,
216217
@Nullable int[] indicesToRemove,
217218
@Nullable ViewAtIndex[] viewsToAdd,
218-
@Nullable int[] tagsToDelete) {
219+
@Nullable int[] tagsToDelete,
220+
@Nullable int[] indicesToDelete) {
219221
super(tag);
220222
mIndicesToRemove = indicesToRemove;
221223
mViewsToAdd = viewsToAdd;
222224
mTagsToDelete = tagsToDelete;
225+
mIndicesToDelete = indicesToDelete;
223226
}
224227

225228
@Override
@@ -228,7 +231,8 @@ public void execute() {
228231
mTag,
229232
mIndicesToRemove,
230233
mViewsToAdd,
231-
mTagsToDelete);
234+
mTagsToDelete,
235+
mIndicesToDelete);
232236
}
233237
}
234238

@@ -778,9 +782,10 @@ public void enqueueManageChildren(
778782
int reactTag,
779783
@Nullable int[] indicesToRemove,
780784
@Nullable ViewAtIndex[] viewsToAdd,
781-
@Nullable int[] tagsToDelete) {
785+
@Nullable int[] tagsToDelete,
786+
@Nullable int[] indicesToDelete) {
782787
mOperations.add(
783-
new ManageChildrenOperation(reactTag, indicesToRemove, viewsToAdd, tagsToDelete));
788+
new ManageChildrenOperation(reactTag, indicesToRemove, viewsToAdd, tagsToDelete, indicesToDelete));
784789
}
785790

786791
public void enqueueSetChildren(

0 commit comments

Comments
 (0)