【问题标题】:iOS implemention of "window.setTimeout" with JavascriptCoreiOS 用 Ja​​vascriptCore 实现“window.setTimeout”
【发布时间】:2013-04-13 17:58:42
【问题描述】:

我在 iOS 应用程序中使用 JavaScriptCore 库,我正在尝试实现 setTimeout 函数。

setTimeout(func, period)

应用程序启动后,会创建具有全局上下文的 JSC 引擎,并在该上下文中添加两个函数:

_JSContext = JSGlobalContextCreate(NULL);

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

这是将具有所需名称的全局 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);
}

这里是objective 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);
}

延迟一段时间后应该在 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);
  }
}

用于 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;
}

在整个设置之后,JSC 引擎应该评估这个:

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

JS执行调用原生_setTimeout,5秒后调用原生onTimeout,在JSObjectCallAsFunction发生crash。 timeoutCtx 无效。听起来超时函数上下文是本地的,并且在此期间垃圾收集器会在 JSC 端删除该上下文。

有趣的是,如果更改_setTimeout 函数以便立即调用JSObjectCllAsFunction,而不等待超时,那么它会按预期工作。

如何防止在这种异步回调中自动删除上下文?

【问题讨论】:

  • 你能解决这个问题吗?我也面临 setTimeOut() 函数的类似问题。
  • 如果您是注册 iOS 开发者,请观看 wwdc 2013 中有关 javascript 核心的视频。您将在那里找到解决此问题的方法。
  • 是的,Prcela,我已经看过了,但那是针对 iOS 7 的。我会尝试为第三方 JSCore 库提供 iOS 5 和 iOS 6 解决方案的答案。谢谢... +1。
  • 嗨 Prcela,你从哪里获得 JSEngine 类?我正在使用phoboslab.org/log/2011/06/javascriptcore-project-files-for-ios 提供的 JSCore 库。我必须使用它,因为我必须支持 iOS 5 和 iOS 6 设备。你能指导我吗??

标签: javascript iphone ios javascriptcore


【解决方案1】:

我最终像这样将 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:@[]];
    });
};

就我而言,这允许我在 JavaScriptCore 中使用 cljs.core.async/timeout。

【讨论】:

    【解决方案2】:

    不要挂在一个 JSContextRefs 上,除了你用 JSGlobalContextCreate 创建的那个

    具体来说,这很糟糕:

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

    不要保存 ctx,而是将全局上下文传递给 JSObjectCallAsFunction。

    您必须 JSValueProtect 任何想要保留的时间超过回调的值

    JavaScriptCore 垃圾收集器可以在您调用 JavaScriptCore 函数的任何时候运行。在您的示例中,作为 setTimeout 的单个参数创建的匿名函数没有被 JavaScript 中的任何内容引用,这意味着它可以在对 setTimeout 的调用完成后的任何时间点被垃圾收集。因此 setTimeout 必须通过 JSValueProtect 函数告诉 JavaScriptCore 不要收集它。使用 JSObjectCallAsFunction 调用该函数后,您应该使用 JSValueUnprotect 它,否则该匿名函数将在内存中徘徊,直到全局上下文被销毁。

    奖励:如果您不想从函数返回任何内容,通常应该返回 JSValueMakeUndefined(ctx)

    JSValueMakeNull(ctx) 与 undefined 不同。我的一般规则是,如果我在 Objective-C 中返回 void,则返回 JSValueMakeUndefined;如果我返回 nil 对象,则返回 JSValueMakeNull。但是,如果你想像 window 对象一样实现 setTimeout,那么它需要返回一个 ID/句柄,可以传递给 clearTimeout 以取消计时器。

    【讨论】:

    • 对内部结构的出色而有趣的洞察... thx
    【解决方案3】:

    对于已注册的 iOS 开发人员,请观看 wwdc 2013 中有关 javascript 核心的新视频,名为 "Integrating JavaScript into Native Apps"。您将在那里找到最新 iOS 版本的解决方案。

    对于当前的 iOS 版本,我的替代解决方案是在 JSC 中创建一个全局数组,用于存储应该受到垃圾收集器保护的对象。因此,您可以控制在不再需要时从数组中弹出变量。

    【讨论】:

    • 链接失效
    【解决方案4】:

    我已经在 Swift 上实现了setTimout、setInterval 和clearTimeout 来解决这个问题。通常,示例仅显示setTimeout 函数,而没有使用clearTimeout 的选项。如果您使用的是 JS 依赖项,那么您很有可能还需要 clearTimeout 和 setInterval 函数。

    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
        }
    

    用法示例:

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

    我希望这会有所帮助。 :)

    【讨论】:

    • 缺少callJsCallback: func callJsCallback(timer: Timer) { let callback = (timer.userInfo as!JSValue) callback.call(withArguments: nil) }
    【解决方案5】:

    根据@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")
    

    【讨论】:

      【解决方案6】:

      这是我的两分钱。

      我认为没有必要在_setTimeout 中保留对上下文的引用。您可以利用全局上下文稍后调用计时器函数。

      您应该使用JSValueProtect 来保护jsEngine.timeoutFunc 免受_setTimeout 中的GC。否则它可能会变成一个无效的引用并在以后导致崩溃。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2017-10-29
        • 2012-03-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-09-07
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多