垃圾收集后Javascript的JavaFx WebView回调失败

时间:2021-08-24 20:43:36

I am currently working on a JavaFX based application, where users can interact with places that are marked on a world map. To do this, I am using an approach similiar to the one described in http://captaincasa.blogspot.de/2014/01/javafx-and-osm-openstreetmap.html ([1]).

我目前正在开发基于JavaFX的应用程序,用户可以在该应用程序中与世界地图上标记的地点进行交互。为此,我使用的方法类似于http://captaincasa.blogspot.de/2014/01/javafx-and-osm-openstreetmap.html([1])中描述的方法。

However, I am facing a hard-to-debug problem related to the Javascript callback variable injected to the embedded HTML-page using the WebEngine's setMember() method (see also https://docs.oracle.com/javase/8/javafx/embedded-browser-tutorial/js-javafx.htm ([2]) for an official tutorial).

但是,我正面临一个难以调试的问题,该问题与使用WebEngine的setMember()方法注入嵌入式HTML页面的Javascript回调变量有关(另请参阅https://docs.oracle.com/javase/8/javafx /embedded-browser-tutorial/js-javafx.htm([2)(官方教程)。

When running the program for a while, the callback variable is loosing its state unpredictably! To demonstrate this behaviour, I developed a minimal working/failing example. I am using jdk1.8.0_121 64-bit on a Windows 10 machine.

运行程序一段时间后,回调变量无法预测地失去其状态!为了演示这种行为,我开发了一个最小的工作/失败示例。我在Windows 10计算机上使用64位的jdk1.8.0_121。

The JavaFx App looks as follows:

JavaFx应用程序如下所示:

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

import javafx.application.Application;
import javafx.concurrent.Worker.State;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import netscape.javascript.JSObject;

public class WebViewJsCallbackTest extends Application {

    private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");

    public static void main(String[] args) {
        launch(args);
    }

    public class JavaScriptBridge {
        public void callback(String data) {
            System.out.println("callback retrieved: " + data);
        }
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        WebView webView = new WebView();
        primaryStage.setScene(new Scene(new AnchorPane(webView)));
        primaryStage.show();

        final WebEngine webEngine = webView.getEngine();
        webEngine.load(getClass().getClassLoader().getResource("page.html").toExternalForm());

        webEngine.getLoadWorker().stateProperty().addListener((observableValue, oldValue, newValue) -> {
            if (newValue == State.SUCCEEDED) {
                JSObject window = (JSObject) webEngine.executeScript("window");
                window.setMember("javaApp", new JavaScriptBridge());
            }
        });

        webEngine.setOnAlert(event -> {
            System.out.println(DATE_FORMAT.format(new Date()) + " alerted: " + event.getData());
        });
    }

}

The HTML file "page.html" looks as follows:

HTML文件“page.html”如下所示:

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<!-- use for in-browser debugging -->
<!-- <script type='text/javascript' src='http://getfirebug.com/releases/lite/1.2/firebug-lite-compressed.js'></script> -->
<script type="text/javascript">
    var javaApp = null;

    function javaCallback(data) {           
        try {
            alert("javaApp=" + javaApp + "(type=" + typeof javaApp + "), data=" + data);
            javaApp.callback(data);
        } catch (e) {
            alert("caugt exception: " + e);
        }
    }
</script>
</head>
<body>
    <button onclick="javaCallback('Test')">Send data to Java</button>
    <button onclick="setInterval(function(){ javaCallback('Test'); }, 1000)">Send data to Java in endless loop</button>
</body>
</html>

The state of the callback variable javaApp can be observed by clicking on the "Send data to Java in endless loop" button. It will continuously try to run the callback method via javaApp.callback, which produces some logging message in the Java app. Alerts are used as an additional communication channel to back things up (always seems to work and currently used as work-around, but that's not how things are ment to be...).

通过单击“无限循环中将数据发送到Java”按钮,可以观察回调变量javaApp的状态。它将不断尝试通过javaApp.callback运行回调方法,这会在Java应用程序中生成一些日志消息。警报被用作备份通信渠道(似乎总是起作用,目前用作解决办法,但事情并非如此......)。

If everything is working as supposed, each time logging similiar to the following lines should be printed:

如果一切按预期工作,则每次都应该打印类似以下行的记录:

callback retrieved: Test
2017/01/27 21:26:11 alerted: javaApp=webviewtests.WebViewJsCallbackTest$JavaScriptBridge@51fac693(type=object), data=Test

However, after a while (anything from 2-7 minutes), no more callbacks are retrieved, but only loggings like the following line are printed:

但是,过了一段时间(2-7分钟之间的任何事情),不再检索回调,但只打印如下行所示的记录:

2017/01/27 21:32:01 alerted: javaApp=undefined(type=object), data=Test

2017/01/27 21:32:01警告:javaApp = undefined(type = object),data = Test

Printing the variable now gives 'undefined' instead of the Java instance path. A strange observation is that the state of javaApp is not truly "undefined". using typeof returnsobject, javaApp === undefined evaluates to false. This is in accordance with the fact that the callback-call does not throw an exception (otherwise, an alert starting with "caugt exception: " would be printed).

现在打印变量会给出'undefined'而不是Java实例路径。一个奇怪的观察是javaApp的状态并非真正“未定义”。使用typeof returnsobject,javaApp === undefined计算结果为false。这符合回调调用不会抛出异常的事实(否则,将打印以“caugt exception:”开头的警报)。

Using Java VisualVM showed that the time of failure happens to coincide with the time the Garbage Collector is activated. This can be seen by observing the Heap memory consumption, which drops from approx. 60MB to 16MB due to GC.

使用Java VisualVM显示故障时间恰好与垃圾收集器激活的时间一致。这可以通过观察堆内存消耗来看出,它从大约下降。由于GC,60MB到16MB。

What's goining on there? Do you have any idea how I can further debug the issue? I could not find any related know bug...

那里有什么东西?你知道如何进一步调试这个问题吗?我找不到任何相关的知道错误......

Thanks a lot for your advice!

非常感谢您的建议!

PS: the problem was reproduced much faster when including Javascript code to display a world map via Leaflet (cf [1]). Loading or shifting the map most of the time instantly caused the GC to do its job. While debugging this original issue, I traced the problem to the minimal example presented here.

PS:当包含Javascript代码以通过Leaflet显示世界地图时,问题被再现得更快(cf [1])。大部分时间加载或移动地图会立即导致GC完成其工作。在调试这个原始问题时,我将问题追溯到此处提供的最小示例。

1 个解决方案

#1


7  

I solved the problem by creating an instance variable bridge in Java that holds the JavaScriptBridge instance sent to Javascript via setMember(). This way, Gargbage Collection of the instance is prevented.

我通过在Java中创建一个实例变量桥解决了这个问题,该实例变量桥保存了通过setMember()发送到Javascript的JavaScriptBridge实例。这样,防止了实例的Gargbage Collection。

Relevant code snippet:

相关代码段:

public class JavaScriptBridge {
    public void callback(String data) {
        System.out.println("callback retrieved: " + data);
    }
}

private JavaScriptBridge bridge;

@Override
public void start(Stage primaryStage) throws Exception {
    WebView webView = new WebView();
    primaryStage.setScene(new Scene(new AnchorPane(webView)));
    primaryStage.show();

    final WebEngine webEngine = webView.getEngine();
    webEngine.load(getClass().getClassLoader().getResource("page.html").toExternalForm());

    bridge = new JavaScriptBridge();
    webEngine.getLoadWorker().stateProperty().addListener((observableValue, oldValue, newValue) -> {
        if (newValue == State.SUCCEEDED) {
            JSObject window = (JSObject) webEngine.executeScript("window");
            window.setMember("javaApp", bridge);
        }
    });

    webEngine.setOnAlert(event -> {
        System.out.println(DATE_FORMAT.format(new Date()) + " alerted: " + event.getData());
    });
}

Altough the code now works smoothly (also in conjunction with Leaflet), I am still irritated of this unexpected behaviour...

尽管代码现在顺利运行(也与Leaflet一起使用),我仍然对这种意想不到的行为感到恼火......

Since it feels more like a work-around, I won't accept it as correct answer for now unless someone can explain why this behaviour should be expected / is correct.

由于感觉更像是一种解决方法,我现在不会接受它作为正确的答案,除非有人能解释为什么这种行为应该是预期的/是正确的。

#1


7  

I solved the problem by creating an instance variable bridge in Java that holds the JavaScriptBridge instance sent to Javascript via setMember(). This way, Gargbage Collection of the instance is prevented.

我通过在Java中创建一个实例变量桥解决了这个问题,该实例变量桥保存了通过setMember()发送到Javascript的JavaScriptBridge实例。这样,防止了实例的Gargbage Collection。

Relevant code snippet:

相关代码段:

public class JavaScriptBridge {
    public void callback(String data) {
        System.out.println("callback retrieved: " + data);
    }
}

private JavaScriptBridge bridge;

@Override
public void start(Stage primaryStage) throws Exception {
    WebView webView = new WebView();
    primaryStage.setScene(new Scene(new AnchorPane(webView)));
    primaryStage.show();

    final WebEngine webEngine = webView.getEngine();
    webEngine.load(getClass().getClassLoader().getResource("page.html").toExternalForm());

    bridge = new JavaScriptBridge();
    webEngine.getLoadWorker().stateProperty().addListener((observableValue, oldValue, newValue) -> {
        if (newValue == State.SUCCEEDED) {
            JSObject window = (JSObject) webEngine.executeScript("window");
            window.setMember("javaApp", bridge);
        }
    });

    webEngine.setOnAlert(event -> {
        System.out.println(DATE_FORMAT.format(new Date()) + " alerted: " + event.getData());
    });
}

Altough the code now works smoothly (also in conjunction with Leaflet), I am still irritated of this unexpected behaviour...

尽管代码现在顺利运行(也与Leaflet一起使用),我仍然对这种意想不到的行为感到恼火......

Since it feels more like a work-around, I won't accept it as correct answer for now unless someone can explain why this behaviour should be expected / is correct.

由于感觉更像是一种解决方法,我现在不会接受它作为正确的答案,除非有人能解释为什么这种行为应该是预期的/是正确的。