Skip to content

Commit

Permalink
[7_4_X] fix(ios): Fix iOS 8/9 compatibility of timers on main thread,…
Browse files Browse the repository at this point in the history
… register exception handler (#10428)

* fix(ios): Fix iOS 8/9 compatibility of timers on main thread, register exception handler
- Re-work timers to try and do less ourselves around cleaning up single-shot timers. Keep weak references to timers, let API invalidate those single-shot timers
- Handle unspecified interval/duration, set minimum to 1ms
- Make setTimeout/setInterval/clearTimeout block signatures match expected types (even though we cheat and access JSContext currentArguments for varargs)
  • Loading branch information
sgtcoolguy authored Nov 8, 2018
1 parent 27d23e8 commit 4b2e861
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 29 deletions.
35 changes: 31 additions & 4 deletions iphone/Classes/KrollContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -191,23 +191,50 @@ KrollContext *GetKrollContext(TiContextRef context);
#if defined(USE_JSCORE_FRAMEWORK) && !defined(TI_USE_KROLL_THREAD)

/**
* Handles creating and clearing timers when running with JavaScriptCore
* Object acting as the target that receives a message when a timer fires.
*/
@interface KrollTimerTarget : NSObject

/**
* The JS function to call when the timer fires.
*/
@property (strong, nonatomic, nonnull) JSValue *callback;

/**
* Additional arugments to pass to the callback function
*/
@property (strong, nonatomic, nullable) NSArray<JSValue *> *arguments;

- (instancetype)initWithCallback:(nonnull JSValue *)callback arguments:(nullable NSArray<NSValue *> *)arguments;

/**
* The method that will be triggered when a timer fires.
*/
- (void)timerFired:(nonnull NSTimer *)timer;

@end

/**
* Handles creating and clearing timers
*/
@interface KrollTimerManager : NSObject

/**
* Map of timer identifiers and the underlying native NSTimer.
*/
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, NSTimer *> *timers;
@property (nonatomic, strong) NSMapTable<NSNumber *, NSTimer *> *timers;

/**
* Initailizes the timer manager in the given JS context. Exposes the global set/clear
* Initializes the timer manager in the given JS context. Exposes the global set/clear
* functions for creating and clearing intervals/timeouts.
*
* @param context The JSContext where timer function should be made available to.
*/
- (instancetype)initInContext:(JSContext *)context;
- (instancetype)initInContext:(nonnull JSContext *)context;

/**
* Invalidates all timers.
*/
- (void)invalidateAllTimers;

@end
Expand Down
105 changes: 80 additions & 25 deletions iphone/Classes/KrollContext.m
Original file line number Diff line number Diff line change
Expand Up @@ -1566,6 +1566,40 @@ @interface KrollTimerManager ()

@end

@implementation KrollTimerTarget

- (instancetype)initWithCallback:(JSValue *)callback arguments:(NSArray<NSValue *> *)arguments
{
self = [super init];
if (!self) {
return nil;
}

self.callback = callback;
self.arguments = arguments;

return self;
}

- (void)dealloc
{
[_callback release];
_callback = nil;
if (_arguments != nil) {
[_arguments release];
_arguments = nil;
}

[super dealloc];
}

- (void)timerFired:(NSTimer *_Nonnull)timer
{
[self.callback callWithArguments:self.arguments];
}

@end

@implementation KrollTimerManager

- (instancetype)initInContext:(JSContext *)context
Expand All @@ -1576,30 +1610,41 @@ - (instancetype)initInContext:(JSContext *)context
}

self.nextTimerIdentifier = 0;
self.timers = [NSMutableDictionary new];
self.timers = [NSMapTable strongToWeakObjectsMapTable];

NSUInteger (^setInterval)(void) = ^() {
return [self setIntervalFromArguments:JSContext.currentArguments shouldRepeat:YES];
NSUInteger (^setInterval)(JSValue *, double) = ^(JSValue *callback, double interval) {
return [self setInterval:interval withCallback:callback shouldRepeat:YES];
};
context[@"setInterval"] = setInterval;

NSUInteger (^setTimeout)(void) = ^() {
return [self setIntervalFromArguments:JSContext.currentArguments shouldRepeat:NO];
NSUInteger (^setTimeout)(JSValue *, double) = ^(JSValue *callback, double interval) {
return [self setInterval:interval withCallback:callback shouldRepeat:NO];
};
context[@"setTimeout"] = setTimeout;

void (^clearInterval)(JSValue *) = ^(JSValue *value) {
return [self clearIntervalWithIdentifier:value.toInt32];
void (^clearInterval)(NSUInteger) = ^(NSUInteger value) {
return [self clearIntervalWithIdentifier:value];
};
context[@"clearInterval"] = clearInterval;
context[@"clearTimeout"] = clearInterval;

// This is more useful than just in timers, should be registered in some better place like KrollBridge?
[context setExceptionHandler:^(JSContext *context, JSValue *exception) {
id exc;
if ([exception isObject]) {
exc = [exception toObject]; // Hope it becomes an NSDictionary?
} else {
exc = [exception toString];
}
[[TiExceptionHandler defaultExceptionHandler] reportScriptError:[TiUtils scriptErrorValue:exc]];
}];
return self;
}

- (void)dealloc
{
if (self.timers != nil) {
[self invalidateAllTimers];
[self.timers removeAllObjects];
[self.timers release];
self.timers = nil;
Expand All @@ -1610,36 +1655,46 @@ - (void)dealloc

- (void)invalidateAllTimers
{
for (NSNumber *timerIdentifier in self.timers.allKeys) {
NSArray<NSNumber *> *keys = [[self.timers keyEnumerator] allObjects];
for (NSNumber *timerIdentifier in keys) {
[self clearIntervalWithIdentifier:timerIdentifier.unsignedIntegerValue];
}
}

- (NSUInteger)setIntervalFromArguments:(NSArray<JSValue *> *)arguments shouldRepeat:(BOOL)shouldRepeat
- (NSUInteger)setInterval:(double)interval withCallback:(JSValue *)callback shouldRepeat:(BOOL)shouldRepeat
{
NSMutableArray *callbackArgs = [arguments.mutableCopy autorelease];
JSValue *callbackFunction = [callbackArgs objectAtIndex:0];
[callbackArgs removeObjectAtIndex:0];
double interval = [[callbackArgs objectAtIndex:0] toDouble] / 1000;
[callbackArgs removeObjectAtIndex:0];
// interval is optional, should default to 0 per spec, but let's enforce at least 1 ms minimum (like Node)
if (isnan(interval) || interval < 1) {
interval = 1; // defaults to 1ms
}
interval = interval / 1000.0; // convert from ms to seconds

// Handle additional arguments being passed in
NSArray<JSValue *> *args = [JSContext currentArguments];
NSUInteger argCount = [args count];
NSArray<JSValue *> *callbackArgs = nil;
if (argCount > 2) {
callbackArgs = [args subarrayWithRange:NSMakeRange(2, argCount - 2)];
}
NSNumber *timerIdentifier = @(self.nextTimerIdentifier++);
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:interval
repeats:shouldRepeat
block:^(NSTimer *_Nonnull timer) {
[callbackFunction callWithArguments:callbackArgs];
if (!shouldRepeat) {
[self.timers removeObjectForKey:timerIdentifier];
}
}];
self.timers[timerIdentifier] = timer;
KrollTimerTarget *timerTarget = [[KrollTimerTarget alloc] initWithCallback:callback arguments:callbackArgs];
NSTimer *timer = [NSTimer timerWithTimeInterval:interval target:timerTarget selector:@selector(timerFired:) userInfo:timerTarget repeats:shouldRepeat];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

[self.timers setObject:timer forKey:timerIdentifier];
return [timerIdentifier unsignedIntegerValue];
}

- (void)clearIntervalWithIdentifier:(NSUInteger)identifier
{
NSNumber *timerIdentifier = @(identifier);
[self.timers[timerIdentifier] invalidate];
[self.timers removeObjectForKey:timerIdentifier];
NSTimer *timer = [self.timers objectForKey:timerIdentifier];
if (timer != nil) {
if ([timer isValid]) {
[timer invalidate];
}
[self.timers removeObjectForKey:timerIdentifier];
}
}

@end
Expand Down

0 comments on commit 4b2e861

Please sign in to comment.