diff --git a/Libraries/ReactNative/ReactNativeBaseComponent.js b/Libraries/ReactNative/ReactNativeBaseComponent.js index cfd3df3e8..b187a39f0 100644 --- a/Libraries/ReactNative/ReactNativeBaseComponent.js +++ b/Libraries/ReactNative/ReactNativeBaseComponent.js @@ -16,6 +16,7 @@ var ReactNativeAttributePayload = require('ReactNativeAttributePayload'); var ReactNativeEventEmitter = require('ReactNativeEventEmitter'); var ReactNativeStyleAttributes = require('ReactNativeStyleAttributes'); var ReactNativeTagHandles = require('ReactNativeTagHandles'); +var ReactNativeViewPool = require('ReactNativeViewPool'); var ReactMultiChild = require('ReactMultiChild'); var RCTUIManager = require('NativeModules').UIManager; @@ -88,6 +89,7 @@ ReactNativeBaseComponent.Mixin = { unmountComponent: function() { deleteAllListeners(this._rootNodeID); this.unmountChildren(); + ReactNativeViewPool.release(this); this._rootNodeID = null; }, @@ -204,24 +206,7 @@ ReactNativeBaseComponent.Mixin = { mountComponent: function(rootID, transaction, context) { this._rootNodeID = rootID; - var tag = ReactNativeTagHandles.allocateTag(); - - if (__DEV__) { - deepFreezeAndThrowOnMutationInDev(this._currentElement.props); - } - - var updatePayload = ReactNativeAttributePayload.create( - this._currentElement.props, - this.viewConfig.validAttributes - ); - - var nativeTopRootID = ReactNativeTagHandles.getNativeTopRootIDFromNodeID(rootID); - RCTUIManager.createView( - tag, - this.viewConfig.uiViewClassName, - nativeTopRootID ? ReactNativeTagHandles.rootNodeIDToTag[nativeTopRootID] : null, - updatePayload - ); + var tag = ReactNativeViewPool.acquire(this); this._registerListenersUponCreation(this._currentElement.props); this.initializeChildren( diff --git a/Libraries/ReactNative/ReactNativeMount.js b/Libraries/ReactNative/ReactNativeMount.js index 02dbcb40f..aeef4cba7 100644 --- a/Libraries/ReactNative/ReactNativeMount.js +++ b/Libraries/ReactNative/ReactNativeMount.js @@ -15,6 +15,7 @@ var RCTUIManager = require('NativeModules').UIManager; var ReactElement = require('ReactElement'); var ReactNativeTagHandles = require('ReactNativeTagHandles'); +var ReactNativeViewPool = require('ReactNativeViewPool'); var ReactPerf = require('ReactPerf'); var ReactReconciler = require('ReactReconciler'); var ReactUpdateQueue = require('ReactUpdateQueue'); @@ -216,6 +217,7 @@ var ReactNativeMount = { ReactNativeMount.unmountComponentAtNode(containerTag); // call back into native to remove all of the subviews from this container RCTUIManager.removeRootView(containerTag); + ReactNativeViewPool.clearPoolForRootView(containerTag); }, /** diff --git a/Libraries/ReactNative/ReactNativeReconcileTransaction.js b/Libraries/ReactNative/ReactNativeReconcileTransaction.js index 309630e3c..aff41081d 100644 --- a/Libraries/ReactNative/ReactNativeReconcileTransaction.js +++ b/Libraries/ReactNative/ReactNativeReconcileTransaction.js @@ -14,6 +14,7 @@ var CallbackQueue = require('CallbackQueue'); var PooledClass = require('PooledClass'); var Transaction = require('Transaction'); +var ReactNativeViewPool = require('ReactNativeViewPool'); /** * Provides a `CallbackQueue` queue for collecting `onDOMReady` callbacks during @@ -35,12 +36,18 @@ var ON_DOM_READY_QUEUEING = { } }; +var RN_VIEW_POOL_WRAPPER = { + close: function() { + ReactNativeViewPool.onReconcileTransactionClose(); + }, +}; + /** * Executed within the scope of the `Transaction` instance. Consider these as * being member methods, but with an implied ordering while being isolated from * each other. */ -var TRANSACTION_WRAPPERS = [ON_DOM_READY_QUEUEING]; +var TRANSACTION_WRAPPERS = [ON_DOM_READY_QUEUEING, RN_VIEW_POOL_WRAPPER]; /** * Currently: diff --git a/Libraries/ReactNative/ReactNativeViewPool.js b/Libraries/ReactNative/ReactNativeViewPool.js new file mode 100644 index 000000000..968449d8b --- /dev/null +++ b/Libraries/ReactNative/ReactNativeViewPool.js @@ -0,0 +1,265 @@ +/** + * 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. + * + * @providesModule ReactNativeViewPool + * @flow + */ +'use strict'; + +var ReactNativeTagHandles = require('ReactNativeTagHandles'); +var ReactNativeAttributePayload = require('ReactNativeAttributePayload'); +var RCTUIManager = require('NativeModules').UIManager; +var Platform = require('Platform'); + +var deepFreezeAndThrowOnMutationInDev = require('deepFreezeAndThrowOnMutationInDev'); +var emptyFunction = require('emptyFunction'); +var flattenStyle = require('flattenStyle'); + +var EMPTY_POOL = [[]]; + +var ENABLED = !!RCTUIManager.dropViews; + +/* indicies used for _addToPool arrays */ +var TAGS_IDX = 0; +var KEYS_IDX = 1; +var PROPS_IDX = 2; + +var _pools = {}; +var _poolSize = {}; + +var layoutOnlyProps = RCTUIManager.layoutOnlyProps; + +function isCollapsableForStyle(style) { + var flatStyle = flattenStyle(style); + for (var styleKey in flatStyle) { + if (layoutOnlyProps[styleKey] !== true) { + return false; + } + } + return true; +} + +function isCollapsable(viewRef) { + var props = viewRef._currentElement.props; + if (props.collapsable !== undefined && !props.collapsable) { + return false; + } + var validAttributes = viewRef.viewConfig.validAttributes; + for (var propKey in props) { + if (!!validAttributes[propKey] && propKey !== 'style' && propKey !== 'collapsable') { + return false; + } + } + return !props.style || isCollapsableForStyle(viewRef._currentElement.props.style); +} + +function enqueueCreate(viewRef, rootTag) { + var tag = ReactNativeTagHandles.allocateTag(); + + if (__DEV__) { + deepFreezeAndThrowOnMutationInDev(viewRef._currentElement.props); + } + + var updatePayload = ReactNativeAttributePayload.create( + viewRef._currentElement.props, + viewRef.viewConfig.validAttributes + ); + + RCTUIManager.createView( + tag, + viewRef.viewConfig.uiViewClassName, + rootTag, + updatePayload + ); + + return tag; +} + +function getViewTag(viewRef) { + return ReactNativeTagHandles.mostRecentMountedNodeHandleForRootNodeID(viewRef._rootNodeID); +} + +function getViewProps(viewRef) { + return viewRef._currentElement.props; +} + +function getViewValidAttributes(viewRef) { + return viewRef.viewConfig.validAttributes; +} + +function getRootViewTag(viewRef) { + var nativeTopRootID = ReactNativeTagHandles.getNativeTopRootIDFromNodeID(viewRef._rootNodeID); + return ReactNativeTagHandles.rootNodeIDToTag[nativeTopRootID]; +} + +function poolKey(viewRef) { + var viewClass = viewRef.viewConfig.uiViewClassName; + if (Platform.OS === 'android' && viewClass === 'RCTView') { + return isCollapsable(viewRef) ? 'CollapsedRCTView' : 'RCTView'; + } + return viewClass; +} + +class ReactNativeViewPool { + constructor() { + this._pool = {}; + this._poolQueue = {}; + this._addToPool = [[],[],[]]; + this._viewsToDelete = []; + if (__DEV__) { + this._recycleStats = {}; + this._deleteStats = {}; + } + } + + onReconcileTransactionClose() { + // flush all deletes, move object from pool_queue to the actual pool + if (this._viewsToDelete.length > 0) { + RCTUIManager.dropViews(this._viewsToDelete); + } + var addToPoolTags = this._addToPool[TAGS_IDX]; + var addToPoolKeys = this._addToPool[KEYS_IDX]; + var addToPoolProps = this._addToPool[PROPS_IDX]; + for (var i = addToPoolTags.length - 1; i >= 0; i--) { + var nativeTag = addToPoolTags[i]; + var key = addToPoolKeys[i]; + var props = addToPoolProps[i]; + var views = this._pool[key] || [[],[]]; + views[0].push(nativeTag); + views[1].push(props); + this._pool[key] = views; + } + this._viewsToDelete = []; + this._addToPool = [[],[],[]]; + this._poolQueue = {}; + } + + acquire(viewRef, rootTag) { + var key = poolKey(viewRef); + if ((this._pool[key] || EMPTY_POOL)[0].length) { + var views = this._pool[key]; + var nativeTag = views[0].pop(); + var oldProps = views[1].pop(); + var updatePayload = ReactNativeAttributePayload.diff( + oldProps, + getViewProps(viewRef), + getViewValidAttributes(viewRef) + ); + if (__DEV__) { + this._recycleStats[key] = (this._recycleStats[key] || 0) + 1; + } + + if (updatePayload) { + RCTUIManager.updateView( + nativeTag, + viewRef.viewConfig.uiViewClassName, + updatePayload + ); + } + return nativeTag; + } else { + // If there is no view available for the given pool key, we just enqueue call to create one + return enqueueCreate(viewRef, rootTag); + } + } + + release(viewRef) { + var key = poolKey(viewRef); + var nativeTag = getViewTag(viewRef); + var pooledCount = (this._pool[key] || EMPTY_POOL)[0].length + (this._poolQueue[key] || 0); + if (pooledCount < (_poolSize[key] || 0)) { + // we have room in the pool for this view + // we can add it to the queue so that it will be added to the actual pull in + // onReconcileTransactionClose + this._addToPool[TAGS_IDX].push(nativeTag); + this._addToPool[KEYS_IDX].push(key); + this._addToPool[PROPS_IDX].push(getViewProps(viewRef)); + this._poolQueue[key] = (this._poolQueue[key] || 0) + 1; + } else { + if (__DEV__) { + if (_poolSize[key]) { + this._deleteStats[key] = (this._deleteStats[key] || 0) + 1; + } + } + this._viewsToDelete.push(nativeTag); + } + } + + clear() { + for (var key in this._pool) { + var poolTags = this._pool[key][0]; + for (var i = poolTags.length - 1; i >= 0; i--) { + this._viewsToDelete.push(poolTags[i]); + } + } + var addToPoolTags = this._addToPool[0]; + for (var i = addToPoolTags.length - 1; i >= 0; i--) { + this._viewsToDelete.push(addToPoolTags[i]); + } + this._addToPool = [[],[],[]]; + this.onReconcileTransactionClose(); + } + + printStats() { + if (__DEV__) { + console.log('Stats', this._recycleStats, this._deleteStats); + } + } +} + +module.exports = { + + onReconcileTransactionClose: function() { + if (ENABLED) { + for (var pool in _pools) { + _pools[pool].onReconcileTransactionClose(); + } + } + }, + + acquire: function(viewRef) { + var rootTag = getRootViewTag(viewRef); + if (ENABLED) { + var pool = _pools[rootTag]; + if (!pool) { + pool = _pools[rootTag] = new ReactNativeViewPool(); + } + return pool.acquire(viewRef, rootTag); + } else { + return enqueueCreate(viewRef, rootTag); + } + }, + + release: ENABLED ? function(viewRef) { + var pool = _pools[getRootViewTag(viewRef)]; + if (pool) { + pool.release(viewRef); + } + } : emptyFunction, + + clearPoolForRootView: ENABLED ? function(rootID) { + var pool = _pools[rootID]; + if (pool) { + pool.clear(); + delete _pools[rootID]; + } + } : emptyFunction, + + configure: function(pool_size) { + _poolSize = pool_size; + }, + + printStats: function() { + if (__DEV__) { + console.log('Pool size', _poolSize); + for (var pool in _pools) { + _pools[pool].onReconcileTransactionClose(); + } + } + }, +}; diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java index 68a1e567f..0b7b7e1f0 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyManager.java @@ -349,7 +349,7 @@ import com.facebook.react.touch.JSResponderHandler; viewsToAdd, tagsToDelete)); } - dropView(viewToDestroy); + detachView(viewToDestroy); } } } @@ -378,10 +378,15 @@ import com.facebook.react.touch.JSResponderHandler; view.setId(tag); } + public void dropView(int tag) { + mTagsToViews.remove(tag); + mTagsToViewManagers.remove(tag); + } + /** * Releases all references to given native View. */ - private void dropView(View view) { + private void detachView(View view) { UiThreadUtil.assertOnUiThread(); if (!mRootTags.get(view.getId())) { // For non-root views we notify viewmanager with {@link ViewManager#onDropInstance} @@ -396,13 +401,11 @@ import com.facebook.react.touch.JSResponderHandler; for (int i = viewGroupManager.getChildCount(viewGroup) - 1; i >= 0; i--) { View child = viewGroupManager.getChildAt(viewGroup, i); if (mTagsToViews.get(child.getId()) != null) { - dropView(child); + detachView(child); } } viewGroupManager.removeAllViews(viewGroup); } - mTagsToViews.remove(view.getId()); - mTagsToViewManagers.remove(view.getId()); } public void removeRootView(int rootViewTag) { @@ -412,7 +415,7 @@ import com.facebook.react.touch.JSResponderHandler; "View with tag " + rootViewTag + " is not registered as a root view"); } View rootView = mTagsToViews.get(rootViewTag); - dropView(rootView); + detachView(rootView); mRootTags.delete(rootViewTag); mRootViewsContext.remove(rootViewTag); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyOptimizer.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyOptimizer.java index fd3946eeb..e01fab28d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyOptimizer.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/NativeViewHierarchyOptimizer.java @@ -90,6 +90,10 @@ public class NativeViewHierarchyOptimizer { } } + public void handleDropViews(int[] viewTagsToDrop, int length) { + mUIViewOperationQueue.enqueueDropViews(viewTagsToDrop, length); + } + /** * Handles native children cleanup when css node is removed from hierarchy */ diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java index a1d498e1c..6482a209b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModule.java @@ -261,6 +261,23 @@ public class UIManagerModule extends ReactContextBaseJavaModule implements } } + @ReactMethod + public void dropViews(ReadableArray viewTags) { + int size = viewTags.size(), realViewsCount = 0; + int realViewTags[] = new int[size]; + for (int i = 0; i < size; i++) { + int tag = viewTags.getInt(i); + ReactShadowNode cssNode = mShadowNodeRegistry.getNode(tag); + if (!cssNode.isVirtual()) { + realViewTags[realViewsCount++] = tag; + } + mShadowNodeRegistry.removeNode(tag); + } + if (realViewsCount > 0) { + mNativeViewHierarchyOptimizer.handleDropViews(realViewTags, realViewsCount); + } + } + @ReactMethod public void updateView(int tag, String className, ReadableMap props) { ViewManager viewManager = mViewManagers.get(className); @@ -405,7 +422,6 @@ public class UIManagerModule extends ReactContextBaseJavaModule implements private void removeShadowNode(ReactShadowNode nodeToRemove) { mNativeViewHierarchyOptimizer.handleRemoveNode(nodeToRemove); - mShadowNodeRegistry.removeNode(nodeToRemove.getReactTag()); for (int i = nodeToRemove.getChildCount() - 1; i >= 0; i--) { removeShadowNode(nodeToRemove.getChildAt(i)); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java index 3287ebf78..eab1c3489 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java @@ -154,4 +154,12 @@ import com.facebook.react.uimanager.events.TouchEventType; return constants; } + + public static Map getLayoutOnlyPropsConstants() { + HashMap constants = new HashMap<>(); + for (String propName : ViewProps.LAYOUT_ONLY_PROPS) { + constants.put(propName, Boolean.TRUE); + } + return constants; + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java index bce7c3736..d5b566529 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstantsHelper.java @@ -25,6 +25,7 @@ import com.facebook.react.common.MapBuilder; private static final String CUSTOM_BUBBLING_EVENT_TYPES_KEY = "customBubblingEventTypes"; private static final String CUSTOM_DIRECT_EVENT_TYPES_KEY = "customDirectEventTypes"; + private static final String LAYOUT_ONLY_PROPS = "layoutOnlyProps"; /** * Generates map of constants that is then exposed by {@link UIManagerModule}. The constants map @@ -75,6 +76,7 @@ import com.facebook.react.common.MapBuilder; constants.put(CUSTOM_BUBBLING_EVENT_TYPES_KEY, bubblingEventTypesConstants); constants.put(CUSTOM_DIRECT_EVENT_TYPES_KEY, directEventTypesConstants); + constants.put(LAYOUT_ONLY_PROPS, UIManagerModuleConstants.getLayoutOnlyPropsConstants()); return constants; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java index 739558938..bb9cea705 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIViewOperationQueue.java @@ -142,6 +142,25 @@ public class UIViewOperationQueue { } } + private final class DropViewsOperation extends ViewOperation { + + private final int[] mViewTagsToDrop; + private final int mArrayLength; + + public DropViewsOperation(int[] viewTagsToDrop, int length) { + super(-1); + mViewTagsToDrop = viewTagsToDrop; + mArrayLength = length; + } + + @Override + public void execute() { + for (int i = 0; i < mArrayLength; i++) { + mNativeViewHierarchyManager.dropView(mViewTagsToDrop[i]); + } + } + } + private final class ManageChildrenOperation extends ViewOperation { private final @Nullable int[] mIndicesToRemove; @@ -502,6 +521,10 @@ public class UIViewOperationQueue { initialProps)); } + public void enqueueDropViews(int[] viewTagsToDrop, int length) { + mOperations.add(new DropViewsOperation(viewTagsToDrop, length)); + } + public void enqueueUpdateProperties(int reactTag, String className, CatalystStylesDiffMap props) { mOperations.add(new UpdatePropertiesOperation(reactTag, props)); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java index 7e87a8419..d19ea165e 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/ViewProps.java @@ -84,7 +84,7 @@ public class ViewProps { Spacing.BOTTOM }; - private static final HashSet LAYOUT_ONLY_PROPS = new HashSet<>( + /*package*/ static final HashSet LAYOUT_ONLY_PROPS = new HashSet<>( Arrays.asList( ALIGN_SELF, ALIGN_ITEMS,