我在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);
}
这里是目标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
,五秒后,调用本机onTimeout
并在JSObjectCallAsFunction
中发生崩溃。 timeoutCtx
无效。听起来像超时功能上下文是本地的,在这段时间内,垃圾收集器会删除JSC端的上下文。
有趣的是,如果_setTimeout
函数被更改以便立即调用JSObjectCllAsFunction
而不等待超时,那么它会按预期工作。
如何防止此类异步回调中的自动上下文删除?
答案 0 :(得分:4)
我最终将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
。
答案 1 :(得分:2)
具体来说,这很糟糕:
jsEngine.timeoutCtx = ctx;
....
JSObjectCallAsFunction(timeoutCtx
不是保存ctx,而是将全局上下文传递给JSObjectCallAsFunction。
JavaScriptCore垃圾收集器可以在您调用JavaScriptCore函数时随时运行。在您的示例中,作为setTimeout的单个参数创建的匿名函数未被JavaScript中的任何内容引用,这意味着可以在调用setTimeout完成后的任何时间点对其进行垃圾回收。因此,setTimeout必须JSValueProtect函数告诉JavaScriptCore不要收集它。在使用JSObjectCallAsFunction调用函数之后,您应该JSValueUnprotect它,否则匿名函数将在内存中挂起,直到全局上下文被销毁。
JSValueMakeNull(ctx)与undefined不同。我的一般规则是返回JSValueMakeUndefined如果我在Objective-C和JSValueMakeNull中返回void,如果我将返回一个nil对象。但是,如果你想像窗口对象一样实现setTimeout,那么它需要返回一个ID /句柄,可以传递给clearTimeout取消计时器。
答案 2 :(得分:2)
对于已注册的iOS开发人员,请查看来自wwdc 2013的名为"Integrating JavaScript into Native Apps"的javascript核心新视频。你会找到最新iOS版本的解决方案。
对于当前的iOS版本,我的替代解决方案是在JSC中创建一个全局数组,用于存储应该受到垃圾收集器保护的对象。因此,您可以控制在不再需要时从数组中弹出变量。
答案 3 :(得分:2)
我已在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)
我希望有所帮助。 :)
答案 4 :(得分:1)
基于@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")
答案 5 :(得分:0)
这是我的两分钱。
我认为没有必要在_setTimeout
中保留对上下文的引用。您可以利用全局上下文稍后调用计时器函数。
您应JSValueProtect
使用jsEngine.timeoutFunc
来保护_setTimeout
中的{{1}}。否则它可能会变成无效的引用并导致以后崩溃。