/** * 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.uimanager.events; import javax.annotation.Nullable; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.Map; import android.util.LongSparseArray; import android.view.Choreographer; import com.facebook.infer.annotation.Assertions; import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.common.MapBuilder; import com.facebook.react.uimanager.ReactChoreographer; import com.facebook.systrace.Systrace; /** * Class responsible for dispatching UI events to JS. The main purpose of this class is to act as an * intermediary between UI code generating events and JS, making sure we don't send more events than * JS can process. * * To use it, create a subclass of {@link Event} and call {@link #dispatchEvent(Event)} whenever * there's a UI event to dispatch. * * This class works by installing a Choreographer frame callback on the main thread. This callback * then enqueues a runnable on the JS thread (if one is not already pending) that is responsible for * actually dispatch events to JS. This implementation depends on the properties that * 1) FrameCallbacks run after UI events have been processed in Choreographer.java * 2) when we enqueue a runnable on the JS queue thread, it won't be called until after any * previously enqueued JS jobs have finished processing * * If JS is taking a long time processing events, then the UI events generated on the UI thread can * be coalesced into fewer events so that when the runnable runs, we don't overload JS with a ton * of events and make it get even farther behind. * * Ideally, we don't need this and JS is fast enough to process all the events each frame, but bad * things happen, including load on CPUs from the system, and we should handle this case well. * * == Event Cookies == * * An event cookie is made up of the event type id, view tag, and a custom coalescing key. Only * Events that have the same cookie can be coalesced. * * Event Cookie Composition: * VIEW_TAG_MASK = 0x00000000ffffffff * EVENT_TYPE_ID_MASK = 0x0000ffff00000000 * COALESCING_KEY_MASK = 0xffff000000000000 */ public class EventDispatcher implements LifecycleEventListener { private static final Comparator<Event> EVENT_COMPARATOR = new Comparator<Event>() { @Override public int compare(Event lhs, Event rhs) { if (lhs == null && rhs == null) { return 0; } if (lhs == null) { return -1; } if (rhs == null) { return 1; } long diff = lhs.getTimestampMs() - rhs.getTimestampMs(); if (diff == 0) { return 0; } else if (diff < 0) { return -1; } else { return 1; } } }; private final Object mEventsStagingLock = new Object(); private final Object mEventsToDispatchLock = new Object(); private final ReactApplicationContext mReactContext; private final LongSparseArray<Integer> mEventCookieToLastEventIdx = new LongSparseArray<>(); private final Map<String, Short> mEventNameToEventId = MapBuilder.newHashMap(); private final DispatchEventsRunnable mDispatchEventsRunnable = new DispatchEventsRunnable(); private final ArrayList<Event> mEventStaging = new ArrayList<>(); private final ArrayList<EventDispatcherListener> mListeners = new ArrayList<>(); private Event[] mEventsToDispatch = new Event[16]; private int mEventsToDispatchSize = 0; private volatile @Nullable RCTEventEmitter mRCTEventEmitter; private final ScheduleDispatchFrameCallback mCurrentFrameCallback; private short mNextEventTypeId = 0; private volatile boolean mHasDispatchScheduled = false; private volatile int mHasDispatchScheduledCount = 0; public EventDispatcher(ReactApplicationContext reactContext) { mReactContext = reactContext; mReactContext.addLifecycleEventListener(this); mCurrentFrameCallback = new ScheduleDispatchFrameCallback(); } /** * Sends the given Event to JS, coalescing eligible events if JS is backed up. */ public void dispatchEvent(Event event) { Assertions.assertCondition(event.isInitialized(), "Dispatched event hasn't been initialized"); for (EventDispatcherListener listener : mListeners) { listener.onEventDispatch(event); } synchronized (mEventsStagingLock) { mEventStaging.add(event); Systrace.startAsyncFlow( Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, event.getEventName(), event.getUniqueID()); } if (mRCTEventEmitter != null) { // If the host activity is paused, the frame callback may not be currently // posted. Ensure that it is so that this event gets delivered promptly. mCurrentFrameCallback.maybePostFromNonUI(); } else { // No JS application has started yet, or resumed. This can happen when a ReactRootView is // added to view hierarchy, but ReactContext creation has not completed yet. In this case, any // touch event dispatch will hit this codepath, and we simply queue them so that they // are dispatched once ReactContext creation completes and JS app is running. } } /** * Add a listener to this EventDispatcher. */ public void addListener(EventDispatcherListener listener) { mListeners.add(listener); } /** * Remove a listener from this EventDispatcher. */ public void removeListener(EventDispatcherListener listener) { mListeners.remove(listener); } @Override public void onHostResume() { UiThreadUtil.assertOnUiThread(); if (mRCTEventEmitter == null) { mRCTEventEmitter = mReactContext.getJSModule(RCTEventEmitter.class); } mCurrentFrameCallback.maybePost(); } @Override public void onHostPause() { stopFrameCallback(); } @Override public void onHostDestroy() { stopFrameCallback(); } public void onCatalystInstanceDestroyed() { stopFrameCallback(); } private void stopFrameCallback() { UiThreadUtil.assertOnUiThread(); mCurrentFrameCallback.stop(); } /** * We use a staging data structure so that all UI events generated in a single frame are * dispatched at once. Otherwise, a JS runnable enqueued in a previous frame could run while the * UI thread is in the process of adding UI events and we might incorrectly send one event this * frame and another from this frame during the next. */ private void moveStagedEventsToDispatchQueue() { synchronized (mEventsStagingLock) { synchronized (mEventsToDispatchLock) { for (int i = 0; i < mEventStaging.size(); i++) { Event event = mEventStaging.get(i); if (!event.canCoalesce()) { addEventToEventsToDispatch(event); continue; } long eventCookie = getEventCookie( event.getViewTag(), event.getEventName(), event.getCoalescingKey()); Event eventToAdd = null; Event eventToDispose = null; Integer lastEventIdx = mEventCookieToLastEventIdx.get(eventCookie); if (lastEventIdx == null) { eventToAdd = event; mEventCookieToLastEventIdx.put(eventCookie, mEventsToDispatchSize); } else { Event lastEvent = mEventsToDispatch[lastEventIdx]; Event coalescedEvent = event.coalesce(lastEvent); if (coalescedEvent != lastEvent) { eventToAdd = coalescedEvent; mEventCookieToLastEventIdx.put(eventCookie, mEventsToDispatchSize); eventToDispose = lastEvent; mEventsToDispatch[lastEventIdx] = null; } else { eventToDispose = event; } } if (eventToAdd != null) { addEventToEventsToDispatch(eventToAdd); } if (eventToDispose != null) { eventToDispose.dispose(); } } } mEventStaging.clear(); } } private long getEventCookie(int viewTag, String eventName, short coalescingKey) { short eventTypeId; Short eventIdObj = mEventNameToEventId.get(eventName); if (eventIdObj != null) { eventTypeId = eventIdObj; } else { eventTypeId = mNextEventTypeId++; mEventNameToEventId.put(eventName, eventTypeId); } return getEventCookie(viewTag, eventTypeId, coalescingKey); } private static long getEventCookie(int viewTag, short eventTypeId, short coalescingKey) { return viewTag | (((long) eventTypeId) & 0xffff) << 32 | (((long) coalescingKey) & 0xffff) << 48; } private class ScheduleDispatchFrameCallback implements Choreographer.FrameCallback { private volatile boolean mIsPosted = false; private boolean mShouldStop = false; @Override public void doFrame(long frameTimeNanos) { UiThreadUtil.assertOnUiThread(); if (mShouldStop) { mIsPosted = false; } else { post(); } Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "ScheduleDispatchFrameCallback"); try { moveStagedEventsToDispatchQueue(); if (mEventsToDispatchSize > 0 && !mHasDispatchScheduled) { mHasDispatchScheduled = true; Systrace.startAsyncFlow( Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "ScheduleDispatchFrameCallback", mHasDispatchScheduledCount); mReactContext.runOnJSQueueThread(mDispatchEventsRunnable); } } finally { Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); } } public void stop() { mShouldStop = true; } public void maybePost() { if (!mIsPosted) { mIsPosted = true; post(); } } private void post() { ReactChoreographer.getInstance() .postFrameCallback(ReactChoreographer.CallbackType.TIMERS_EVENTS, mCurrentFrameCallback); } public void maybePostFromNonUI() { if (mIsPosted) { return; } // We should only hit this slow path when we receive events while the host activity is paused. if (mReactContext.isOnUiQueueThread()) { maybePost(); } else { mReactContext.runOnUiQueueThread(new Runnable() { @Override public void run() { maybePost(); } }); } } } private class DispatchEventsRunnable implements Runnable { @Override public void run() { Systrace.beginSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "DispatchEventsRunnable"); try { Systrace.endAsyncFlow( Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, "ScheduleDispatchFrameCallback", mHasDispatchScheduledCount); mHasDispatchScheduled = false; mHasDispatchScheduledCount++; Assertions.assertNotNull(mRCTEventEmitter); synchronized (mEventsToDispatchLock) { // We avoid allocating an array and iterator, and "sorting" if we don't need to. // This occurs when the size of mEventsToDispatch is zero or one. if (mEventsToDispatchSize > 1) { Arrays.sort(mEventsToDispatch, 0, mEventsToDispatchSize, EVENT_COMPARATOR); } for (int eventIdx = 0; eventIdx < mEventsToDispatchSize; eventIdx++) { Event event = mEventsToDispatch[eventIdx]; // Event can be null if it has been coalesced into another event. if (event == null) { continue; } Systrace.endAsyncFlow( Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, event.getEventName(), event.getUniqueID()); event.dispatch(mRCTEventEmitter); event.dispose(); } clearEventsToDispatch(); mEventCookieToLastEventIdx.clear(); } } finally { Systrace.endSection(Systrace.TRACE_TAG_REACT_JAVA_BRIDGE); } } } private void addEventToEventsToDispatch(Event event) { if (mEventsToDispatchSize == mEventsToDispatch.length) { mEventsToDispatch = Arrays.copyOf(mEventsToDispatch, 2 * mEventsToDispatch.length); } mEventsToDispatch[mEventsToDispatchSize++] = event; } private void clearEventsToDispatch() { Arrays.fill(mEventsToDispatch, 0, mEventsToDispatchSize, null); mEventsToDispatchSize = 0; } }