Implement mount hooks in UIManager (#37460)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/37460

## Context

This implements the concept of mount hooks in UIManager which, similarly to commit hooks, receive a notification when a root shadow tree has been mounted in the host platform.

This is meant to be used internally in React Native, not by user-land libraries or products.

This will be used to implement `IntersectionObserver` in a following diff.

Changelog: [Internal]

Reviewed By: javache, sammy-SC

Differential Revision: D45866244

fbshipit-source-id: 4df48bf237a5cc89e37709faaeaa0ce582c0d0cc
This commit is contained in:
Rubén Norte 2023-05-26 09:06:00 -07:00 коммит произвёл Facebook GitHub Bot
Родитель f30716323a
Коммит 32bd60f863
21 изменённых файлов: 188 добавлений и 6 удалений

Просмотреть файл

@ -67,6 +67,8 @@ NS_ASSUME_NONNULL_BEGIN
- (void)animationTick;
- (void)reportMount:(facebook::react::SurfaceId)surfaceId;
- (void)addEventListener:(std::shared_ptr<facebook::react::EventListener> const &)listener;
- (void)removeEventListener:(std::shared_ptr<facebook::react::EventListener> const &)listener;

Просмотреть файл

@ -132,6 +132,11 @@ class LayoutAnimationDelegateProxy : public LayoutAnimationStatusDelegate, publi
_scheduler->animationTick();
}
- (void)reportMount:(facebook::react::SurfaceId)surfaceId
{
_scheduler->reportMount(surfaceId);
}
- (void)dealloc
{
if (_animationDriver) {

Просмотреть файл

@ -280,6 +280,10 @@ static BackgroundExecutor RCTGetBackgroundExecutor()
CoreFeatures::enableGranularScrollViewStateUpdatesIOS = true;
}
if (reactNativeConfig && reactNativeConfig->getBool("react_fabric:enable_mount_hooks_ios")) {
CoreFeatures::enableMountHooks = true;
}
auto componentRegistryFactory =
[factory = wrapManagedObject(_mountingManager.componentViewRegistry.componentViewFactory)](
EventDispatcher::Weak const &eventDispatcher, ContextContainer::Shared const &contextContainer) {
@ -442,6 +446,15 @@ static BackgroundExecutor RCTGetBackgroundExecutor()
[observer didMountComponentsWithRootTag:rootTag];
}
}
RCTScheduler *scheduler = [self scheduler];
if (scheduler) {
// Notify mount when the effects are visible and prevent mount hooks to
// delay paint.
dispatch_async(dispatch_get_main_queue(), ^{
[scheduler reportMount:rootTag];
});
}
}
- (NSArray<id<RCTSurfacePresenterObserver>> *)_getObservers

Просмотреть файл

@ -156,4 +156,7 @@ public class ReactFeatureFlags {
* HostObject pattern
*/
public static boolean useNativeState = false;
/** Report mount operations from the host platform to notify mount hooks. */
public static boolean enableMountHooks = false;
}

Просмотреть файл

@ -52,6 +52,8 @@ public interface Binding {
public void driveCxxAnimations();
public void reportMount(int surfaceId);
public ReadableNativeMap getInspectorDataForInstance(EventEmitterWrapper eventEmitterWrapper);
public void register(

Просмотреть файл

@ -86,6 +86,8 @@ public class BindingImpl implements Binding {
public native void driveCxxAnimations();
public native void reportMount(int surfaceId);
public native ReadableNativeMap getInspectorDataForInstance(
EventEmitterWrapper eventEmitterWrapper);

Просмотреть файл

@ -22,6 +22,8 @@ import static com.facebook.react.uimanager.common.UIManagerType.FABRIC;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Point;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
@ -81,10 +83,13 @@ import com.facebook.react.uimanager.events.EventDispatcherImpl;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.facebook.react.views.text.TextLayoutManager;
import com.facebook.react.views.text.TextLayoutManagerMapBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* We instruct ProGuard not to strip out any fields or methods, because many of these methods are
@ -166,6 +171,9 @@ public class FabricUIManager implements UIManager, LifecycleEventListener {
@NonNull
private final CopyOnWriteArrayList<UIManagerListener> mListeners = new CopyOnWriteArrayList<>();
@NonNull private final AtomicBoolean mMountNotificationScheduled = new AtomicBoolean(false);
@NonNull private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
@ThreadConfined(UI)
@NonNull
private final DispatchUIFrameCallback mDispatchUIFrameCallback;
@ -1179,17 +1187,50 @@ public class FabricUIManager implements UIManager, LifecycleEventListener {
private class MountItemDispatchListener implements MountItemDispatcher.ItemDispatchListener {
@Override
public void willMountItems() {
public void willMountItems(@Nullable List<MountItem> mountItems) {
for (UIManagerListener listener : mListeners) {
listener.willMountItems(FabricUIManager.this);
}
}
@Override
public void didMountItems() {
public void didMountItems(@Nullable List<MountItem> mountItems) {
for (UIManagerListener listener : mListeners) {
listener.didMountItems(FabricUIManager.this);
}
if (!ReactFeatureFlags.enableMountHooks) {
return;
}
boolean mountNotificationScheduled = mMountNotificationScheduled.getAndSet(true);
if (!mountNotificationScheduled) {
// Notify mount when the effects are visible and prevent mount hooks to
// delay paint.
mMainThreadHandler.postAtFrontOfQueue(
new Runnable() {
@Override
public void run() {
mMountNotificationScheduled.set(false);
if (mountItems == null) {
return;
}
// Collect surface IDs for all the mount items
List<Integer> surfaceIds = new ArrayList();
for (MountItem mountItem : mountItems) {
if (!surfaceIds.contains(mountItem.getSurfaceId())) {
surfaceIds.add(mountItem.getSurfaceId());
}
}
for (int surfaceId : surfaceIds) {
mBinding.reportMount(surfaceId);
}
}
});
}
}
@Override

Просмотреть файл

@ -192,7 +192,7 @@ public class MountItemDispatcher {
return false;
}
mItemDispatchListener.willMountItems();
mItemDispatchListener.willMountItems(mountItemsToDispatch);
// As an optimization, execute all ViewCommands first
// This should be:
@ -301,7 +301,7 @@ public class MountItemDispatcher {
mBatchedExecutionTime += SystemClock.uptimeMillis() - batchedExecutionStartTime;
}
mItemDispatchListener.didMountItems();
mItemDispatchListener.didMountItems(mountItemsToDispatch);
Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE);
@ -419,9 +419,9 @@ public class MountItemDispatcher {
}
public interface ItemDispatchListener {
void willMountItems();
void willMountItems(List<MountItem> mountItems);
void didMountItems();
void didMountItems(List<MountItem> mountItems);
void didDispatchMountItems();
}

Просмотреть файл

@ -97,6 +97,15 @@ void Binding::driveCxxAnimations() {
scheduler_->animationTick();
}
void Binding::reportMount(SurfaceId surfaceId) {
const auto &scheduler = getScheduler();
if (!scheduler) {
LOG(ERROR) << "Binding::reportMount: scheduler disappeared";
return;
}
scheduler->reportMount(surfaceId);
}
#pragma mark - Surface management
void Binding::startSurface(
@ -570,6 +579,7 @@ void Binding::registerNatives() {
makeNativeMethod("setConstraints", Binding::setConstraints),
makeNativeMethod("setPixelDensity", Binding::setPixelDensity),
makeNativeMethod("driveCxxAnimations", Binding::driveCxxAnimations),
makeNativeMethod("reportMount", Binding::reportMount),
makeNativeMethod(
"uninstallFabricUIManager", Binding::uninstallFabricUIManager),
makeNativeMethod("registerSurface", Binding::registerSurface),

Просмотреть файл

@ -119,6 +119,7 @@ class Binding : public jni::HybridClass<Binding>,
void setPixelDensity(float pointScaleFactor);
void driveCxxAnimations();
void reportMount(SurfaceId surfaceId);
void uninstallFabricUIManager();

Просмотреть файл

@ -54,6 +54,7 @@ Pod::Spec.new do |s|
s.dependency "React-debug"
s.dependency "React-utils"
s.dependency "React-runtimescheduler"
s.dependency "React-cxxreact"
if ENV["USE_HERMES"] == nil || ENV["USE_HERMES"] == "1"
s.dependency "hermes-engine"

Просмотреть файл

@ -17,5 +17,6 @@ bool CoreFeatures::cacheLastTextMeasurement = false;
bool CoreFeatures::cancelImageDownloadsOnRecycle = false;
bool CoreFeatures::disableTransactionCommit = false;
bool CoreFeatures::enableGranularScrollViewStateUpdatesIOS = false;
bool CoreFeatures::enableMountHooks = false;
} // namespace facebook::react

Просмотреть файл

@ -52,6 +52,9 @@ class CoreFeatures {
// When enabled, RCTScrollViewComponentView will trigger ShadowTree state
// updates for all changes in scroll position.
static bool enableGranularScrollViewStateUpdatesIOS;
// Report mount operations from the host platform to notify mount hooks.
static bool enableMountHooks;
};
} // namespace facebook::react

Просмотреть файл

@ -179,6 +179,10 @@ TelemetryController const &MountingCoordinator::getTelemetryController() const {
return telemetryController_;
}
ShadowTreeRevision const &MountingCoordinator::getBaseRevision() const {
return baseRevision_;
}
void MountingCoordinator::setMountingOverrideDelegate(
std::weak_ptr<MountingOverrideDelegate const> delegate) const {
std::lock_guard<std::mutex> lock(mutex_);

Просмотреть файл

@ -71,6 +71,8 @@ class MountingCoordinator final {
TelemetryController const &getTelemetryController() const;
ShadowTreeRevision const &getBaseRevision() const;
/*
* Methods from this section are meant to be used by
* `MountingOverrideDelegate` only.

Просмотреть файл

@ -377,6 +377,10 @@ void Scheduler::uiManagerDidSetIsJSResponder(
}
}
void Scheduler::reportMount(SurfaceId surfaceId) const {
uiManager_->reportMount(surfaceId);
}
ContextContainer::Shared Scheduler::getContextContainer() const {
return contextContainer_;
}

Просмотреть файл

@ -108,6 +108,8 @@ class Scheduler final : public UIManagerDelegate {
#pragma mark - UIManager
std::shared_ptr<UIManager> getUIManager() const;
void reportMount(SurfaceId surfaceId) const;
#pragma mark - Event listeners
void addEventListener(const std::shared_ptr<EventListener const> &listener);
void removeEventListener(

Просмотреть файл

@ -33,6 +33,7 @@ target_link_libraries(react_render_uimanager
react_render_runtimescheduler
react_render_mounting
react_config
reactnative
rrc_root
rrc_view
runtimeexecutor

Просмотреть файл

@ -7,6 +7,7 @@
#include "UIManager.h"
#include <cxxreact/JSExecutor.h>
#include <react/debug/react_native_assert.h>
#include <react/renderer/core/DynamicPropsUtilities.h>
#include <react/renderer/core/PropsParserContext.h>
@ -16,6 +17,7 @@
#include <react/renderer/uimanager/SurfaceRegistryBinding.h>
#include <react/renderer/uimanager/UIManagerBinding.h>
#include <react/renderer/uimanager/UIManagerCommitHook.h>
#include <react/renderer/uimanager/UIManagerMountHook.h>
#include <glog/logging.h>
@ -612,6 +614,21 @@ void UIManager::unregisterCommitHook(
commitHook.commitHookWasUnregistered(*this);
}
void UIManager::registerMountHook(UIManagerMountHook &mountHook) {
std::unique_lock lock(mountHookMutex_);
react_native_assert(
std::find(mountHooks_.begin(), mountHooks_.end(), &mountHook) ==
mountHooks_.end());
mountHooks_.push_back(&mountHook);
}
void UIManager::unregisterMountHook(UIManagerMountHook &mountHook) {
std::unique_lock lock(mountHookMutex_);
auto iterator = std::find(mountHooks_.begin(), mountHooks_.end(), &mountHook);
react_native_assert(iterator != mountHooks_.end());
mountHooks_.erase(iterator);
}
#pragma mark - ShadowTreeDelegate
RootShadowNode::Unshared UIManager::shadowTreeWillCommit(
@ -640,6 +657,28 @@ void UIManager::shadowTreeDidFinishTransaction(
}
}
void UIManager::reportMount(SurfaceId surfaceId) const {
auto time = JSExecutor::performanceNow();
auto rootShadowNode = RootShadowNode::Shared{};
shadowTreeRegistry_.visit(surfaceId, [&](ShadowTree const &shadowTree) {
rootShadowNode =
shadowTree.getMountingCoordinator()->getBaseRevision().rootShadowNode;
});
if (!rootShadowNode) {
return;
}
{
std::shared_lock lock(mountHookMutex_);
for (auto *mountHook : mountHooks_) {
mountHook->shadowTreeDidMount(rootShadowNode, time);
}
}
}
#pragma mark - UIManagerAnimationDelegate
void UIManager::setAnimationDelegate(UIManagerAnimationDelegate *delegate) {

Просмотреть файл

@ -30,6 +30,7 @@ namespace facebook::react {
class UIManagerBinding;
class UIManagerCommitHook;
class UIManagerMountHook;
class UIManager final : public ShadowTreeDelegate {
public:
@ -82,6 +83,12 @@ class UIManager final : public ShadowTreeDelegate {
void registerCommitHook(UIManagerCommitHook const &commitHook) const;
void unregisterCommitHook(UIManagerCommitHook const &commitHook) const;
/*
* Registers and unregisters a mount hook.
*/
void registerMountHook(UIManagerMountHook &mountHook);
void unregisterMountHook(UIManagerMountHook &mountHook);
ShadowNode::Shared getNewestCloneOfShadowNode(
ShadowNode const &shadowNode) const;
@ -191,6 +198,8 @@ class UIManager final : public ShadowTreeDelegate {
ShadowTreeRegistry const &getShadowTreeRegistry() const;
void reportMount(SurfaceId surfaceId) const;
private:
friend class UIManagerBinding;
friend class Scheduler;
@ -217,6 +226,9 @@ class UIManager final : public ShadowTreeDelegate {
mutable std::shared_mutex commitHookMutex_;
mutable std::vector<UIManagerCommitHook const *> commitHooks_;
mutable std::shared_mutex mountHookMutex_;
mutable std::vector<UIManagerMountHook *> mountHooks_;
std::unique_ptr<LeakChecker> leakChecker_;
};

Просмотреть файл

@ -0,0 +1,34 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <react/renderer/components/root/RootShadowNode.h>
#include "UIManager.h"
namespace facebook::react {
class ShadowTree;
class UIManager;
/*
* Implementing a mount hook allows to observe Shadow Trees being mounted in
* the host platform.
*/
class UIManagerMountHook {
public:
/*
* Called right after a `ShadowTree` is mounted in the host platform.
*/
virtual void shadowTreeDidMount(
RootShadowNode::Shared const &rootShadowNode,
double mountTime) noexcept = 0;
virtual ~UIManagerMountHook() noexcept = default;
};
} // namespace facebook::react