From 4fc15dbf1703f64ce8b3e111149ca2e06741f102 Mon Sep 17 00:00:00 2001 From: Tadeu Zagallo Date: Wed, 27 May 2015 20:15:33 -0700 Subject: [PATCH] [ReactNative] Implement proper event coalescing --- React/Base/RCTBridge.h | 5 + React/Base/RCTBridge.m | 12 --- React/Base/RCTEventDispatcher.h | 37 +++++-- React/Base/RCTEventDispatcher.m | 177 ++++++++++++++++++++++---------- React/Views/RCTNavigator.m | 1 + React/Views/RCTScrollView.h | 16 ++- React/Views/RCTScrollView.m | 118 +++++++++++++++++++++ 7 files changed, 289 insertions(+), 77 deletions(-) diff --git a/React/Base/RCTBridge.h b/React/Base/RCTBridge.h index 7c8af00cc..5f84ed9da 100644 --- a/React/Base/RCTBridge.h +++ b/React/Base/RCTBridge.h @@ -95,6 +95,11 @@ static const char *__rct_import_##module##_##method##__ = #module"."#method; /** * The event dispatcher is a wrapper around -enqueueJSCall:args: that provides a * higher-level interface for sending UI events such as touches and text input. + * + * NOTE: RCTEventDispatcher is now a bridge module, this is implemented as a + * category but remains declared in the bridge to avoid breaking changes + * + * To be moved. */ @property (nonatomic, readonly) RCTEventDispatcher *eventDispatcher; diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index 523333aac..47c7f5942 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -18,7 +18,6 @@ #import "RCTContextExecutor.h" #import "RCTConvert.h" -#import "RCTEventDispatcher.h" #import "RCTJavaScriptLoader.h" #import "RCTKeyCommands.h" #import "RCTLog.h" @@ -211,7 +210,6 @@ static NSArray *RCTBridgeModuleClassesByModuleID(void) @property (nonatomic, strong) RCTBatchedBridge *batchedBridge; @property (nonatomic, strong) RCTBridgeModuleProviderBlock moduleProvider; -@property (nonatomic, strong, readwrite) RCTEventDispatcher *eventDispatcher; - (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method @@ -875,11 +873,6 @@ static id _latestJSExecutor; return _batchedBridge.modules; } -- (RCTEventDispatcher *)eventDispatcher -{ - return _eventDispatcher ?: _batchedBridge.eventDispatcher; -} - #define RCT_INNER_BRIDGE_ONLY(...) \ - (void)__VA_ARGS__ \ { \ @@ -943,11 +936,6 @@ RCT_INNER_BRIDGE_ONLY(_invokeAndProcessModule:(NSString *)module method:(NSStrin _javaScriptExecutor = RCTCreateExecutor(executorClass); _latestJSExecutor = _javaScriptExecutor; - /** - * Setup event dispatcher before initializing modules to allow init calls - */ - self.eventDispatcher = [[RCTEventDispatcher alloc] initWithBridge:self]; - /** * Initialize and register bridge modules *before* adding the display link * so we don't have threading issues diff --git a/React/Base/RCTEventDispatcher.h b/React/Base/RCTEventDispatcher.h index 15cb18021..5576df64f 100644 --- a/React/Base/RCTEventDispatcher.h +++ b/React/Base/RCTEventDispatcher.h @@ -9,7 +9,7 @@ #import -@class RCTBridge; +#import "RCTBridge.h" typedef NS_ENUM(NSInteger, RCTTextEventType) { RCTTextEventTypeFocus, @@ -28,14 +28,36 @@ typedef NS_ENUM(NSInteger, RCTScrollEventType) { RCTScrollEventTypeEndAnimation, }; +@protocol RCTEvent + +@required + +@property (nonatomic, strong, readonly) NSNumber *viewTag; +@property (nonatomic, copy, readonly) NSString *eventName; +@property (nonatomic, copy, readonly) NSDictionary *body; +@property (nonatomic, assign, readonly) uint16_t coalescingKey; + +- (BOOL)canCoalesce; +- (id)coalesceWithEvent:(id)newEvent; + ++ (NSString *)moduleDotMethod; + +@end + +@interface RCTBaseEvent : NSObject + +- (instancetype)initWithViewTag:(NSNumber *)viewTag + eventName:(NSString *)eventName + body:(NSDictionary *)body NS_DESIGNATED_INITIALIZER; + +@end + /** * This class wraps the -[RCTBridge enqueueJSCall:args:] method, and * provides some convenience methods for generating event calls. */ @interface RCTEventDispatcher : NSObject -- (instancetype)initWithBridge:(RCTBridge *)bridge; - /** * Send an application-specific event that does not relate to a specific * view, e.g. a navigation or data update notification. @@ -61,13 +83,6 @@ typedef NS_ENUM(NSInteger, RCTScrollEventType) { reactTag:(NSNumber *)reactTag text:(NSString *)text; -/** - * Send a scroll event. - * (You can send a fake scroll event by passing nil for scrollView). - */ -- (void)sendScrollEventWithType:(RCTScrollEventType)type - reactTag:(NSNumber *)reactTag - scrollView:(UIScrollView *)scrollView - userData:(NSDictionary *)userData; +- (void)sendEvent:(id)event; @end diff --git a/React/Base/RCTEventDispatcher.m b/React/Base/RCTEventDispatcher.m index 8487556e5..e6ed698d9 100644 --- a/React/Base/RCTEventDispatcher.m +++ b/React/Base/RCTEventDispatcher.m @@ -11,16 +11,76 @@ #import "RCTAssert.h" #import "RCTBridge.h" +#import "RCTSparseArray.h" + +static uint64_t RCTGetEventID(id event) +{ + return ( + [event.viewTag intValue] | + (((uint64_t)event.eventName.hash & 0xFFFF) << 32) | + (((uint64_t)event.coalescingKey) << 48) + ); +} + +@implementation RCTBaseEvent + +@synthesize viewTag = _viewTag; +@synthesize eventName = _eventName; +@synthesize body = _body; + +- (instancetype)initWithViewTag:(NSNumber *)viewTag + eventName:(NSString *)eventName + body:(NSDictionary *)body +{ + if ((self = [super init])) { + _viewTag = viewTag; + _eventName = eventName; + _body = body; + } + return self; +} + +- (uint16_t)coalescingKey +{ + return 0; +} + +- (BOOL)canCoalesce +{ + return YES; +} + +- (id)coalesceWithEvent:(id)newEvent +{ + return newEvent; +} + ++ (NSString *)moduleDotMethod +{ + return nil; +} + +@end + +@interface RCTEventDispatcher() + +@end @implementation RCTEventDispatcher { - RCTBridge __weak *_bridge; + RCTSparseArray *_eventQueue; + NSLock *_eventQueueLock; } -- (instancetype)initWithBridge:(RCTBridge *)bridge +@synthesize bridge = _bridge; + +RCT_EXPORT_MODULE() + +- (instancetype)init { if ((self = [super init])) { - _bridge = bridge; + _eventQueue = [[RCTSparseArray alloc] init]; + _eventQueueLock = [[NSLock alloc] init]; } return self; } @@ -70,58 +130,71 @@ RCT_IMPORT_METHOD(RCTEventEmitter, receiveEvent); }]; } -/** - * TODO: throttling - * NOTE: the old system used a per-scrollview throttling - * which would be fairly easy to re-implement if needed, - * but this is non-optimal as it leads to degradation in - * scroll responsiveness. A better solution would be to - * coalesce multiple scroll events into a single batch. - */ -- (void)sendScrollEventWithType:(RCTScrollEventType)type - reactTag:(NSNumber *)reactTag - scrollView:(UIScrollView *)scrollView - userData:(NSDictionary *)userData +- (void)sendEvent:(id)event { - static NSString *events[] = { - @"topScrollBeginDrag", - @"topScroll", - @"topScrollEndDrag", - @"topMomentumScrollBegin", - @"topMomentumScrollEnd", - @"topScrollAnimationEnd", - }; - - NSDictionary *body = @{ - @"contentOffset": @{ - @"x": @(scrollView.contentOffset.x), - @"y": @(scrollView.contentOffset.y) - }, - @"contentInset": @{ - @"top": @(scrollView.contentInset.top), - @"left": @(scrollView.contentInset.left), - @"bottom": @(scrollView.contentInset.bottom), - @"right": @(scrollView.contentInset.right) - }, - @"contentSize": @{ - @"width": @(scrollView.contentSize.width), - @"height": @(scrollView.contentSize.height) - }, - @"layoutMeasurement": @{ - @"width": @(scrollView.frame.size.width), - @"height": @(scrollView.frame.size.height) - }, - @"zoomScale": @(scrollView.zoomScale ?: 1), - @"target": reactTag - }; - - if (userData) { - NSMutableDictionary *mutableBody = [body mutableCopy]; - [mutableBody addEntriesFromDictionary:userData]; - body = mutableBody; + if (!event.canCoalesce) { + [self dispatchEvent:event]; + return; } - [self sendInputEventWithName:events[type] body:body]; + [_eventQueueLock lock]; + + uint64_t eventID = RCTGetEventID(event); + id previousEvent = _eventQueue[eventID]; + + if (previousEvent) { + event = [previousEvent coalesceWithEvent:event]; + } + + _eventQueue[eventID] = event; + + [_eventQueueLock unlock]; +} + +- (void)dispatchEvent:(id)event +{ + NSMutableArray *arguments = [[NSMutableArray alloc] init]; + + if (event.viewTag) { + [arguments addObject:event.viewTag]; + } + + [arguments addObject:event.eventName]; + + if (event.body) { + [arguments addObject:event.body]; + } + + [_bridge enqueueJSCall:[[event class] moduleDotMethod] + args:arguments]; +} + +- (dispatch_queue_t)methodQueue +{ + return RCTJSThread; +} + +- (void)didUpdateFrame:(RCTFrameUpdate *)update +{ + RCTSparseArray *eventQueue; + + [_eventQueueLock lock]; + eventQueue = _eventQueue; + _eventQueue = [[RCTSparseArray alloc] init]; + [_eventQueueLock unlock]; + + for (id event in eventQueue.allObjects) { + [self dispatchEvent:event]; + } +} + +@end + +@implementation RCTBridge (RCTEventDispatcher) + +- (RCTEventDispatcher *)eventDispatcher +{ + return self.modules[RCTBridgeModuleNameForClass([RCTEventDispatcher class])]; } @end diff --git a/React/Views/RCTNavigator.m b/React/Views/RCTNavigator.m index 57415fbb7..381846569 100644 --- a/React/Views/RCTNavigator.m +++ b/React/Views/RCTNavigator.m @@ -15,6 +15,7 @@ #import "RCTEventDispatcher.h" #import "RCTLog.h" #import "RCTNavItem.h" +#import "RCTScrollView.h" #import "RCTUtils.h" #import "RCTView.h" #import "RCTWrapperViewController.h" diff --git a/React/Views/RCTScrollView.h b/React/Views/RCTScrollView.h index 0333a38a7..e5c1d550a 100644 --- a/React/Views/RCTScrollView.h +++ b/React/Views/RCTScrollView.h @@ -10,13 +10,12 @@ #import #import "RCTAutoInsetsProtocol.h" +#import "RCTEventDispatcher.h" #import "RCTScrollableProtocol.h" #import "RCTView.h" @protocol UIScrollViewDelegate; -@class RCTEventDispatcher; - @interface RCTScrollView : RCTView - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; @@ -48,3 +47,16 @@ @property (nonatomic, copy) NSIndexSet *stickyHeaderIndices; @end + +@interface RCTEventDispatcher (RCTScrollView) + +/** + * Send a scroll event. + * (You can send a fake scroll event by passing nil for scrollView). + */ +- (void)sendScrollEventWithType:(RCTScrollEventType)type + reactTag:(NSNumber *)reactTag + scrollView:(UIScrollView *)scrollView + userData:(NSDictionary *)userData; + +@end diff --git a/React/Views/RCTScrollView.m b/React/Views/RCTScrollView.m index d64a583af..e6fdb83ca 100644 --- a/React/Views/RCTScrollView.m +++ b/React/Views/RCTScrollView.m @@ -21,6 +21,107 @@ CGFloat const ZINDEX_DEFAULT = 0; CGFloat const ZINDEX_STICKY_HEADER = 50; +@interface RCTScrollEvent : NSObject + +- (instancetype)initWithType:(RCTScrollEventType)type + reactTag:(NSNumber *)reactTag + scrollView:(UIScrollView *)scrollView + userData:(NSDictionary *)userData NS_DESIGNATED_INITIALIZER; + +@end + +@implementation RCTScrollEvent +{ + RCTScrollEventType _type; + UIScrollView *_scrollView; + NSDictionary *_userData; +} + +@synthesize viewTag = _viewTag; + +- (instancetype)initWithType:(RCTScrollEventType)type + reactTag:(NSNumber *)reactTag + scrollView:(UIScrollView *)scrollView + userData:(NSDictionary *)userData +{ + if (self = [super init]) { + _type = type; + _viewTag = reactTag; + _scrollView = scrollView; + _userData = userData; + } + return self; +} + +- (uint16_t)coalescingKey +{ + return 0; +} + +- (NSDictionary *)body +{ + NSDictionary *body = @{ + @"contentOffset": @{ + @"x": @(_scrollView.contentOffset.x), + @"y": @(_scrollView.contentOffset.y) + }, + @"contentInset": @{ + @"top": @(_scrollView.contentInset.top), + @"left": @(_scrollView.contentInset.left), + @"bottom": @(_scrollView.contentInset.bottom), + @"right": @(_scrollView.contentInset.right) + }, + @"contentSize": @{ + @"width": @(_scrollView.contentSize.width), + @"height": @(_scrollView.contentSize.height) + }, + @"layoutMeasurement": @{ + @"width": @(_scrollView.frame.size.width), + @"height": @(_scrollView.frame.size.height) + }, + @"zoomScale": @(_scrollView.zoomScale ?: 1), + }; + + if (_userData) { + NSMutableDictionary *mutableBody = [body mutableCopy]; + [mutableBody addEntriesFromDictionary:_userData]; + body = mutableBody; + } + + return body; +} + +- (NSString *)eventName +{ + static NSString *events[] = { + @"topScrollBeginDrag", + @"topScroll", + @"topScrollEndDrag", + @"topMomentumScrollBegin", + @"topMomentumScrollEnd", + @"topScrollAnimationEnd", + }; + + return events[_type]; +} + +- (BOOL)canCoalesce +{ + return YES; +} + +- (id)coalesceWithEvent:(id)newEvent +{ + return newEvent; +} + ++ (NSString *)moduleDotMethod +{ + return @"RCTEventEmitter.receiveEvent"; +} + +@end + /** * Include a custom scroll view subclass because we want to limit certain * default UIKit behaviors such as textFields automatically scrolling @@ -442,6 +543,7 @@ RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, RCTScrollEventTypeMove) reactTag:self.reactTag scrollView:scrollView userData:userData]; + // Update dispatch time _lastScrollDispatchTime = now; _allowNextScrollNoMatterWhat = NO; @@ -630,3 +732,19 @@ RCT_SET_AND_PRESERVE_OFFSET(setScrollIndicatorInsets, UIEdgeInsets); } @end + +@implementation RCTEventDispatcher (RCTScrollView) + +- (void)sendScrollEventWithType:(RCTScrollEventType)type + reactTag:(NSNumber *)reactTag + scrollView:(UIScrollView *)scrollView + userData:(NSDictionary *)userData +{ + RCTScrollEvent *scrollEvent = [[RCTScrollEvent alloc] initWithType:type + reactTag:reactTag + scrollView:scrollView + userData:userData]; + [self sendEvent:scrollEvent]; +} + +@end