简体   繁体   English

iOS使用JavascriptCore实现“window.setTimeout”

[英]iOS implemention of “window.setTimeout” with JavascriptCore

I am using JavaScriptCore library inside iOS application and I am trying to implement setTimeout function. 我在iOS应用程序中使用JavaScriptCore库,我正在尝试实现setTimeout函数。

setTimeout(func, period)

After application is launched, the JSC engine with global context is created and two functions are added to that context: 启动应用程序后,将创建具有全局上下文的JSC引擎,并向该上下文添加两个函数:

_JSContext = JSGlobalContextCreate(NULL);

[self mapName:"iosSetTimeout" toFunction:_setTimeout];
[self mapName:"iosLog" toFunction:_log];

Here is native implementation that is mapping global JS function with desired name to static objective C function: 这是本机实现,它将具有所需名称的全局JS函数映射到静态目标C函数:

- (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);
}

And here is the implementation of objective C setTimeout function: 这里是目标C 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);
}

Function that should be called on jsEngine after some delay: 一段延迟后应该在jsEngine上调用的函数:

- (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 function for javascript evaluation: 用于javascript评估的本机函数:

- (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;
}

After that whole setup, the JSC engine should evaluate this: 在整个设置之后,JSC引擎应该评估这个:

[jsEngine evaluate:@"iosSetTimeout(function(){iosLog('timeout done')}, 5000)"];

The JS execution calls the native _setTimeout , and after five seconds, the native onTimeout is called and crash happens in JSObjectCallAsFunction . JS执行调用本机_setTimeout ,五秒后,调用本机onTimeout并在JSObjectCallAsFunction发生崩溃。 The timeoutCtx becomes invalid. timeoutCtx变为无效。 Sounds like timeout function context is local and during the time period garbage collector deletes that context in JSC side. 听起来像超时功能上下文是本地的,在此期间垃圾收集器删除JSC端的上下文。

The interesting thing is also, if _setTimeout function is changed in order to call JSObjectCllAsFunction immediately, without waiting for timeout, then it works as expected. 有趣的是,如果更改_setTimeout函数以便立即调用JSObjectCllAsFunction ,而不等待超时,那么它将按预期工作。

How to prevent automatic context deletion in such asynchronous callbacks? 如何防止这种异步回调中的自动上下文删除?

I ended up adding setTimeout to a specific JavaScriptCore context like this, and it worked well: 我最终将setTimeout添加到这样的特定JavaScriptCore上下文中,并且它运行良好:

JSVirtualMachine *vm = [[JSVirtualMachine alloc] init];
JSContext *context = [[JSContext alloc] initWithVirtualMachine: vm];

// Add setTimout
context[@"setTimeout"] = ^(JSValue* function, JSValue* timeout) {
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)([timeout toInt32] * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{
        [function callWithArguments:@[]];
    });
};

In my case, this allowed me to use cljs.core.async/timeout inside of JavaScriptCore. 在我的例子中,这允许我在JavaScriptCore中使用cljs.core.async/timeout

Don't hang onto a JSContextRefs except for the one you created with JSGlobalContextCreate 除了使用JSGlobalContextCreate创建的JSContextRefs之外,不要挂起JSContextRefs

Specifically, this is bad: 具体来说,这很糟糕:

jsEngine.timeoutCtx =  ctx;
....
JSObjectCallAsFunction(timeoutCtx

Instead of saving ctx, pass your global context to JSObjectCallAsFunction. 而不是保存ctx,将您的全局上下文传递给JSObjectCallAsFunction。

You must JSValueProtect any values you want to keep around longer than your callback 你必须JSValueProtect你想要保留的任何值比你的回调更长

The JavaScriptCore garbage collector could run anytime you call a JavaScriptCore function. JavaScriptCore垃圾收集器可以在您调用JavaScriptCore函数时随时运行。 In your example, the anonymous function created as the single argument to setTimeout is not referenced by anything in JavaScript, which means it could be garbage collected at any point in time after the call to setTimeout completes. 在您的示例中,作为setTimeout的单个参数创建的匿名函数未被JavaScript中的任何内容引用,这意味着可以在调用setTimeout完成后的任何时间点对其进行垃圾回收。 setTimeout must therefore JSValueProtect the function to tell JavaScriptCore not to collect it. 因此,setTimeout必须JSValueProtect函数告诉JavaScriptCore不要收集它。 After you invoke the function with JSObjectCallAsFunction you should then JSValueUnprotect it, otherwise that anonymous function will hang around in memory until the global context is destroyed. 在使用JSObjectCallAsFunction调用函数之后,您应该JSValueUnprotect它,否则匿名函数将在内存中挂起,直到全局上下文被销毁。

Bonus: you usually should return JSValueMakeUndefined(ctx) if you don't want to return anything from a function 加成:如果你不想从函数返回任何东西,你通常应该返回JSValueMakeUndefined(ctx)

JSValueMakeNull(ctx) is a different than undefined. JSValueMakeNull(ctx)与undefined不同。 My general rule is to return JSValueMakeUndefined if I would return void in Objective-C and JSValueMakeNull if I would return a nil object. 我的一般规则是返回JSValueMakeUndefined如果我在Objective-C和JSValueMakeNull中返回void,如果我将返回一个nil对象。 However, if you want to implement setTimeout like the window object then it needs to return an ID/handle that can be passed to clearTimeout to cancel the timer. 但是,如果要像窗口对象一样实现setTimeout,则需要返回一个ID /句柄,该句柄可以传递给clearTimeout以取消计时器。

For registered iOS developer, take a look at the new video about javascript core from wwdc 2013 called "Integrating JavaScript into Native Apps" . 对于已注册的iOS开发人员,请查看来自wwdc 2013的名为“将JavaScript集成到本机应用程序”的 javascript核心视频。 You will find there the solution for newest iOS version. 你会发现最新的iOS版本的解决方案。

My alternative solution, for the current iOS version, was to make a global array in JSC for storing objects that should be protected from garbage collector. 对于当前的iOS版本,我的替代解决方案是在JSC中创建一个全局数组 ,用于存储应该受到垃圾收集器保护的对象。 So, you have control to pop variable from array when it is not needed any more. 因此,您可以控制在不再需要时从数组中弹出变量。

I have implemented setTimout , setInterval and clearTimeout on Swift to solve this problem. 我在Swift上实现了setTimoutsetIntervalclearTimeout来解决这个问题。 Usually, the examples show only the setTimeout function without the option to use clearTimeout . 通常,这些示例仅显示setTimeout函数,但没有使用clearTimeout的选项。 If you are using JS dependencies, there's a big chance that you are going to need the clearTimeout and setInterval functions as well. 如果您正在使用JS依赖项,那么您很可能也需要clearTimeoutsetInterval函数。

import Foundation
import JavaScriptCore

let timerJSSharedInstance = TimerJS()

@objc protocol TimerJSExport : JSExport {

    func setTimeout(_ callback : JSValue,_ ms : Double) -> String

    func clearTimeout(_ identifier: String)

    func setInterval(_ callback : JSValue,_ ms : Double) -> String

}

// Custom class must inherit from `NSObject`
@objc class TimerJS: NSObject, TimerJSExport {
    var timers = [String: Timer]()

    static func registerInto(jsContext: JSContext, forKeyedSubscript: String = "timerJS") {
        jsContext.setObject(timerJSSharedInstance,
                            forKeyedSubscript: forKeyedSubscript as (NSCopying & NSObjectProtocol))
        jsContext.evaluateScript(
            "function setTimeout(callback, ms) {" +
            "    return timerJS.setTimeout(callback, ms)" +
            "}" +
            "function clearTimeout(indentifier) {" +
            "    timerJS.clearTimeout(indentifier)" +
            "}" +
            "function setInterval(callback, ms) {" +
            "    return timerJS.setInterval(callback, ms)" +
            "}"
        )       
    }

    func clearTimeout(_ identifier: String) {
        let timer = timers.removeValue(forKey: identifier)

        timer?.invalidate()
    }


    func setInterval(_ callback: JSValue,_ ms: Double) -> String {
        return createTimer(callback: callback, ms: ms, repeats: true)
    }

    func setTimeout(_ callback: JSValue, _ ms: Double) -> String {
        return createTimer(callback: callback, ms: ms , repeats: false)
    }

    func createTimer(callback: JSValue, ms: Double, repeats : Bool) -> String {
        let timeInterval  = ms/1000.0

        let uuid = NSUUID().uuidString

        // make sure that we are queueing it all in the same executable queue...
        // JS calls are getting lost if the queue is not specified... that's what we believe... ;)
        DispatchQueue.main.async(execute: {
            let timer = Timer.scheduledTimer(timeInterval: timeInterval,
                                             target: self,
                                             selector: #selector(self.callJsCallback),
                                             userInfo: callback,
                                             repeats: repeats)
            self.timers[uuid] = timer
        })


        return uuid
    }

Usage Example: 用法示例:

jsContext = JSContext()
TimerJS.registerInto(jsContext: jsContext)

I hope that helps. 我希望有所帮助。 :) :)

Based on @ninjudd's answer here is what I did in swift 根据@ninjudd的回答,这是我在swift中所做的

    let setTimeout: @objc_block (JSValue, Int) -> Void = {
        [weak self] (cb, wait) in

        let callback = cb as JSValue

        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(UInt64(wait) * NSEC_PER_MSEC)), dispatch_get_main_queue(), { () -> Void in
            callback.callWithArguments([])
        })
    }
    context.setObject(unsafeBitCast(setTimeout, AnyObject.self), forKeyedSubscript: "setTimeout")

Here is my two cents. 这是我的两分钱。

I think there is no need to keep a reference to context in _setTimeout . 我认为没有必要在_setTimeout保持对上下文的_setTimeout You can leverage the global context to invoke a timer function later. 您可以利用全局上下文稍后调用计时器函数。

You should use JSValueProtect to protect jsEngine.timeoutFunc from GC in _setTimeout . 您应该使用JSValueProtect保护jsEngine.timeoutFunc从GC在_setTimeout Otherwise it can turn to an invalid reference and cause crash later. 否则,它可能会变为无效的引用,并导致以后崩溃。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM