javascript - 垃圾收集后来自 Javascript 的 JavaFx WebView 回调失败

标签 javascript java javafx webview garbage-collection

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

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

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

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

}

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>

回调变量javaApp的状态可以通过点击“Send data to Java in endless loop”按钮来观察。它将不断尝试通过 javaApp.callback 运行回调方法,这会在 Java 应用程序中生成一些日志消息。警报被用作额外的沟通 channel 来支持事情(似乎总是有效并且目前用作解决方法,但事情并不是这样......)。

如果一切正常,每次都应打印类似于以下行的日志记录:

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

但是,一段时间后(2-7 分钟之间的任何时间),不再检索到回调,而是仅打印如下行的日志记录:

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

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

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

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

非常感谢您的建议!

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

最佳答案

我通过在 Java 中创建一个实例变量 bridge 来解决这个问题,该变量保存通过 setMember() 发送到 Javascript 的 JavaScriptBridge 实例。这样,就可以防止实例的垃圾收集。

相关代码片段:

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

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

编辑:自 Java 9 起记录了此行为的解释(感谢@dsh 的澄清评论!我当时正在使用 Java 8,不幸的是手头没有这些信息......)

关于javascript - 垃圾收集后来自 Javascript 的 JavaFx WebView 回调失败,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41903154/

相关文章:

javascript - Firefox 插件 - 如何发出 http 请求

java - 找不到符号错误

treeview - JavaFx 2.0 : get TreeItems or Nodes currently visible on screen

java - 当 getChildren() 返回一个以 Circle 作为第一个元素的可观察列表时,如何访问 Circle 中的值?

java - 更改javaFX中ImageView中的图像

javascript - 使用 javascript/jquery/api 显示有关音乐会的信息

javascript - 去除本地应用的跨域限制

javascript - 将 javascript 变量存储在数组中

java - 如何创建 TimePickerDialog android

java - GWT 手动序列化服务器上​​的域对象