iOS-Implementierung von „window.setTimeout“ mit JavascriptCore
Ich verwende die JavaScriptCore-Bibliothek in einer iOS-Anwendung und versuche, die setTimeout-Funktion zu implementieren.
setTimeout(func, period)
Nach dem Start der Anwendung wird die JSC-Engine mit globalem Kontext erstellt und diesem Kontext werden zwei Funktionen hinzugefügt:
_JSContext = JSGlobalContextCreate(NULL);
[self mapName:"iosSetTimeout" toFunction:_setTimeout];
[self mapName:"iosLog" toFunction:_log];
Die folgende native Implementierung ordnet die globale JS-Funktion mit dem gewünschten Namen der statischen Ziel-C-Funktion zu:
- (void) mapName:(const char*)name toFunction:(JSObjectCallAsFunctionCallback)func
{
JSStringRef nameRef = JSStringCreateWithUTF8CString(name);
JSObjectRef funcRef = JSObjectMakeFunctionWithCallback(_JSContext, nameRef, func);
JSObjectSetProperty(_JSContext, JSContextGetGlobalObject(_JSContext), nameRef, funcRef, kJSPropertyAttributeNone, NULL);
JSStringRelease(nameRef);
}
Und hier ist die Implementierung der Ziel-C-Funktion setTimeout:
JSValueRef _setTimeout(JSContextRef ctx,
JSObjectRef function,
JSObjectRef thisObject,
size_t argumentCount,
const JSValueRef arguments[],
JSValueRef* exception)
{
if(argumentCount == 2)
{
JSEngine *jsEngine = [JSEngine shared];
jsEngine.timeoutCtx = ctx;
jsEngine.timeoutFunc = (JSObjectRef)arguments[0];
[jsEngine performSelector:@selector(onTimeout) withObject:nil afterDelay:5];
}
return JSValueMakeNull(ctx);
}
Funktion, die nach einiger Zeit auf jsEngine aufgerufen werden soll:
- (void) onTimeout
{
JSValueRef excp = NULL;
JSObjectCallAsFunction(timeoutCtx, timeoutFunc, NULL, 0, 0, &excp);
if (excp) {
JSStringRef exceptionArg = JSValueToStringCopy([self JSContext], excp, NULL);
NSString* exceptionRes = (__bridge_transfer NSString*)JSStringCopyCFString(kCFAllocatorDefault, exceptionArg);
JSStringRelease(exceptionArg);
NSLog(@"[JSC] JavaScript exception: %@", exceptionRes);
}
}
Native Funktion zur Javascript-Auswertung:
- (NSString *)evaluate:(NSString *)script
{
if (!script) {
NSLog(@"[JSC] JS String is empty!");
return nil;
}
JSStringRef scriptJS = JSStringCreateWithUTF8CString([script UTF8String]);
JSValueRef exception = NULL;
JSValueRef result = JSEvaluateScript([self JSContext], scriptJS, NULL, NULL, 0, &exception);
NSString *res = nil;
if (!result) {
if (exception) {
JSStringRef exceptionArg = JSValueToStringCopy([self JSContext], exception, NULL);
NSString* exceptionRes = (__bridge_transfer NSString*)JSStringCopyCFString(kCFAllocatorDefault, exceptionArg);
JSStringRelease(exceptionArg);
NSLog(@"[JSC] JavaScript exception: %@", exceptionRes);
}
NSLog(@"[JSC] No result returned");
} else {
JSStringRef jstrArg = JSValueToStringCopy([self JSContext], result, NULL);
res = (__bridge_transfer NSString*)JSStringCopyCFString(kCFAllocatorDefault, jstrArg);
JSStringRelease(jstrArg);
}
JSStringRelease(scriptJS);
return res;
}
Nach diesem gesamten Setup sollte die JSC-Engine Folgendes auswerten:
[jsEngine evaluate:@"iosSetTimeout(function(){iosLog('timeout done')}, 5000)"];
Die JS-Ausführung ruft die native auf_setTimeout
und nach fünf Sekunden der EingeboreneonTimeout
wird aufgerufen und Absturz passiert inJSObjectCallAsFunction
. DastimeoutCtx
wird ungültig. Klingt so, als wäre der Kontext der Zeitüberschreitungsfunktion lokal und während des Zeitraums löscht der Garbage Collector diesen Kontext auf der JSC-Seite.
Das Interessante ist auch, wenn_setTimeout
Funktion wird geändert, um anzurufenJSObjectCllAsFunction
sofort, ohne auf ein Timeout zu warten, funktioniert es dann wie erwartet.
Wie kann das automatische Löschen von Kontexten in solchen asynchronen Rückrufen verhindert werden?