JavaFx WebView在垃圾回收后无法从Javascript中回调

10

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

然而,我面临着一个难以调试的问题,涉及到使用WebEngine的setMember()方法向嵌入的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>

点击“在无限循环中向Java发送数据”按钮,可以观察回调变量javaApp的状态。通过javaApp.callback不断尝试运行回调方法,并在Java应用程序中产生一些日志消息。警报被用作附加通信渠道来备份事情(似乎总是有效并且目前被用作解决方法,但这不是事情的本意……)。

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

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 alerted: javaApp=undefined(type=object), data=Test ```
现在打印变量会返回`'undefined'`,而不是Java实例路径。一个奇怪的观察结果是,javaApp的状态并不真正“未定义”。使用typeof返回objectjavaApp === undefined评估为false。这符合回调调用不抛出异常的事实(否则,将打印以“caught exception:”开头的警报)。使用Java VisualVM显示故障时间恰好与启动垃圾收集器的时间重合。可以通过观察堆内存消耗来看到这一点,由于GC,堆内存从约60MB下降到16MB左右。
发生了什么?你有任何想法如何进一步调试此问题吗?我找不到任何相关的已知错误......
非常感谢您的建议!
PS:当包含JavaScript代码以通过Leaflet显示世界地图时(参见[1]),该问题更快地复现。加载或移动地图大多数情况下立即导致GC开始工作。在调试此原始问题时,我将问题追踪到了此处提供的最小示例。

我也遇到了同样的问题,使用jdk 1.8.0_121版本时出现了问题。后来我发现在另一台安装了jdk 1.8.0_60版本的电脑上可以正常运行。于是我切换到了1.8.0_60版本,问题得以解决。 - Ali Ayad Jalil
@AliAyadJalil:退回到旧版本并不总是可取的,甚至有时也不可能。 :-/ - Eric Duminil
1个回答

12
我通过在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,很遗憾手头没有这些信息...)

非常感谢你的代码,它救了我的一天。应该被接受为答案。 - Dinesh Falwadiya
哦,我的天啊。这个Heisenbug真是太可怕了。非常感谢你的回答。我已经花了好几天时间寻找问题的根源。 - Eric Duminil
1
这不是一个变通方法,而是在WebEngine类中有文档表明,您的应用程序负责维护一个硬引用以防止对象被垃圾回收。以下是文档的摘录: 请注意,在上面的示例中,应用程序持有对JavaApplication实例的引用。这是为了执行所需的JavaScript回调方法。 在下面的示例中,应用程序没有保留对Java对象的引用。在这种情况下,由于属性值是本地对象,因此该值可能会在下一个GC周期中被垃圾回收。 - dsh
1
@dsh 感谢您的评论!我编辑了我的答案以参考Java文档。 - luddwich_r

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接