diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/ElementsList.java b/ReactAndroid/src/main/java/com/facebook/react/flat/ElementsList.java new file mode 100644 index 000000000..e77b42599 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/ElementsList.java @@ -0,0 +1,175 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.flat; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.lang.reflect.Array; + +/** + * Helper class that supports 3 main operations: start(), add() an element and finish(). + * + * When started, it takes a baseline array to compare to. When adding a new element, it checks + * whether a corresponding element in baseline array is the same. On finish(), it will return null + * if baseline array contains exactly the same elements that were added with a sequence of add() + * calls, or a new array the recorded elements: + * + * Example 1: + * ----- + * start([A]) + * add(A) + * finish() -> null (because [A] == [A]) + * + * Example 2: + * ---- + * start([A]) + * add(B) + * finish() -> [B] (because [A] != [B]) + * + * It is important that start/finish can be nested: + * ---- + * start([A]) + * add(A) + * start([B]) + * add(B) + * finish() -> null + * add(C) + * finish() -> [A, C] + * + * StateBuilder is using this class to check if e.g. a DrawCommand list for a given View needs to be + * updated. + */ +/* package */ final class ElementsList { + + private static final class Scope { + Object[] elements; + int index; + int size; + } + + private final ArrayList mScopesStack = new ArrayList<>(); + private final ArrayDeque mElements = new ArrayDeque<>(); + private final E[] mEmptyArray; + private Scope mCurrentScope = null; + private int mScopeIndex = 0; + + public ElementsList(E[] emptyArray) { + mEmptyArray = emptyArray; + mScopesStack.add(mCurrentScope); + } + + /** + * Starts a new scope. + */ + public void start(Object[] elements) { + pushScope(); + + Scope scope = getCurrentScope(); + scope.elements = elements; + scope.index = 0; + scope.size = mElements.size(); + } + + /** + * Finished current scope, and returns null if there were no changes recorded, or a new array + * containing all the recorded elements otherwise. + */ + public E[] finish() { + Scope scope = getCurrentScope(); + popScope(); + + E[] result = null; + int size = mElements.size() - scope.size; + if (scope.index != scope.elements.length) { + result = extractElements(size); + } else { + // downsize + for (int i = 0; i < size; ++i) { + mElements.pollLast(); + } + } + + // to prevent leaks + scope.elements = null; + + return result; + } + + /** + * Adds a new element to the list. This method can be optimized to avoid inserts on same elements. + */ + public void add(E element) { + Scope scope = getCurrentScope(); + + if (scope.index < scope.elements.length && + scope.elements[scope.index] == element) { + ++scope.index; + } else { + scope.index = Integer.MAX_VALUE; + } + + mElements.add(element); + } + + /** + * Resets all references to the elements to null to avoid memory leaks. + */ + public void clear() { + if (getCurrentScope() != null) { + throw new RuntimeException("Must call finish() for every start() call being made."); + } + mElements.clear(); + } + + /** + * Extracts last size elements into an array. + */ + private E[] extractElements(int size) { + if (size == 0) { + // avoid allocating empty array + return mEmptyArray; + } + + E[] elements = (E[]) Array.newInstance(mEmptyArray.getClass().getComponentType(), size); + for (int i = size - 1; i >= 0; --i) { + elements[i] = mElements.pollLast(); + } + + return elements; + } + + /** + * Saves current scope in a stack. + */ + private void pushScope() { + ++mScopeIndex; + if (mScopeIndex == mScopesStack.size()) { + mCurrentScope = new Scope(); + mScopesStack.add(mCurrentScope); + } else { + mCurrentScope = mScopesStack.get(mScopeIndex); + } + } + + /** + * Restores last save current scope. + */ + private void popScope() { + --mScopeIndex; + mCurrentScope = mScopesStack.get(mScopeIndex); + } + + /** + * Returns current scope. + */ + private Scope getCurrentScope() { + return mCurrentScope; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/flat/StateBuilder.java b/ReactAndroid/src/main/java/com/facebook/react/flat/StateBuilder.java index 495af3860..eec17d3e7 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/flat/StateBuilder.java +++ b/ReactAndroid/src/main/java/com/facebook/react/flat/StateBuilder.java @@ -9,8 +9,6 @@ package com.facebook.react.flat; -import java.util.ArrayDeque; - /** * Shadow node hierarchy by itself cannot display UI, it is only a representation of what UI should * be from JavaScript perspective. StateBuilder is a helper class that can walk the shadow node tree @@ -21,10 +19,8 @@ import java.util.ArrayDeque; private final FlatUIViewOperationQueue mOperationsQueue; - // DrawCommands - private final ArrayDeque mDrawCommands = new ArrayDeque<>(); - private DrawCommand[] mPreviousDrawCommands = DrawCommand.EMPTY_ARRAY; - private int mPreviousDrawCommandsIndex; + private final ElementsList mDrawCommands = + new ElementsList(DrawCommand.EMPTY_ARRAY); /* package */ StateBuilder(FlatUIViewOperationQueue operationsQueue) { mOperationsQueue = operationsQueue; @@ -42,14 +38,7 @@ import java.util.ArrayDeque; * Adds a DrawCommand for current mountable node. */ /* package */ void addDrawCommand(AbstractDrawCommand drawCommand) { - if (mPreviousDrawCommandsIndex < mPreviousDrawCommands.length && - mPreviousDrawCommands[mPreviousDrawCommandsIndex] == drawCommand) { - ++mPreviousDrawCommandsIndex; - } else { - mPreviousDrawCommandsIndex = mPreviousDrawCommands.length + 1; - } - - mDrawCommands.addLast(drawCommand); + mDrawCommands.add(drawCommand); } /** @@ -86,47 +75,17 @@ import java.util.ArrayDeque; int tag, float width, float height) { - // save - int d = mDrawCommands.size(); - DrawCommand[] previousDrawCommands = mPreviousDrawCommands; - int previousDrawCommandsIndex = mPreviousDrawCommandsIndex; - - // reset - mPreviousDrawCommands = node.getDrawCommands(); - mPreviousDrawCommandsIndex = 0; + mDrawCommands.start(node.getDrawCommands()); collectStateRecursively(node, 0, 0, width, height); - if (mPreviousDrawCommandsIndex != mPreviousDrawCommands.length) { - // DrawCommands changes, need to re-mount them and re-draw the View. - DrawCommand[] drawCommands = extractDrawCommands(d); + final DrawCommand[] drawCommands = mDrawCommands.finish(); + if (drawCommands != null) { + // DrawCommands changed, need to re-mount them and re-draw the View. node.setDrawCommands(drawCommands); mOperationsQueue.enqueueUpdateMountState(tag, drawCommands); } - - // restore - mPreviousDrawCommandsIndex = previousDrawCommandsIndex; - mPreviousDrawCommands = previousDrawCommands; - } - - /** - * Returns all DrawCommands collectes so far starting from a given index. - */ - private DrawCommand[] extractDrawCommands(int lowerBound) { - int upperBound = mDrawCommands.size(); - int size = upperBound - lowerBound; - if (size == 0) { - // avoid allocating empty array - return DrawCommand.EMPTY_ARRAY; - } - - DrawCommand[] drawCommands = new DrawCommand[size]; - for (int i = 0; i < size; ++i) { - drawCommands[i] = mDrawCommands.pollFirst(); - } - - return drawCommands; } /**