6

iOS アプリケーション内で JavaScriptCore ライブラリを使用しており、setTimeout 関数を実装しようとしています。

setTimeout(func, period)

アプリケーションの起動後、グローバル コンテキストを持つ JSC エンジンが作成され、そのコンテキストに 2 つの関数が追加されます。

_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、5 秒後にネイティブonTimeoutが呼び出されて でクラッシュが発生しJSObjectCallAsFunctionます。はtimeoutCtx無効になります。タイムアウト関数のコンテキストはローカルのように聞こえ、その間、ガベージコレクターはJSC側でそのコンテキストを削除します。

興味深いことに、タイムアウトを待たずにすぐ_setTimeoutに呼び出すように関数を変更すると、期待どおりに動作します。JSObjectCllAsFunction

このような非同期コールバックでの自動コンテキスト削除を防ぐにはどうすればよいですか?

4

6 に答える 6

2

この問題を解決するために、Swift にsetTimoutsetIntervalを実装しました。clearTimeout通常、例はsetTimeoutuse のオプションなしで関数のみを示していますclearTimeout。JS の依存関係を使用している場合は、clearTimeoutandsetInterval関数も必要になる可能性が高くなります。

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)

それが役立つことを願っています。:)

于 2016-10-05T02:03:22.017 に答える
2

登録済みの iOS 開発者は、 「JavaScript をネイティブ アプリに統合する」という wwdc 2013 の JavaScript コアに関する新しいビデオをご覧ください。最新の iOS バージョンのソリューションが見つかります。

現在の iOS バージョンでの私の別の解決策は、ガベージ コレクターから保護する必要があるオブジェクトを格納するために、JSC でグローバル配列を作成することでした。そのため、変数が不要になったときに配列から変数をポップすることができます。

于 2013-07-23T11:08:30.260 に答える
2

JSGlobalContextCreate で作成したものを除いて、JSContextRefs に固執しないでください。

具体的には、これは悪いです:

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 を実装する場合は、タイマーをキャンセルするために clearTimeout に渡すことができる ID/ハンドルを返す必要があります。

于 2013-05-07T05:46:57.137 に答える
1

@ninjuddの回答に基づいて、私が迅速に行ったことは次のとおりです

    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")
于 2015-06-15T11:18:59.050 に答える
0

これが私の2セントです。

でコンテキストへの参照を保持する必要はないと思います_setTimeout。グローバル コンテキストを利用して、後でタイマー関数を呼び出すことができます。

で GCJSValueProtectから保護するために使用する必要があります。そうしないと、無効な参照になり、後でクラッシュする可能性があります。jsEngine.timeoutFunc_setTimeout

于 2013-04-14T02:48:13.700 に答える