Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7_4_X] fix(ios): Fix iOS 8/9 compatibility of timers on main thread, register exception handler #10428

Merged
merged 3 commits into from Nov 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
35 changes: 31 additions & 4 deletions iphone/Classes/KrollContext.h
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
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