垃圾收集后来自 Javascript 的 JavaFx WebView 回调失败
Posted
技术标签:
【中文标题】垃圾收集后来自 Javascript 的 JavaFx WebView 回调失败【英文标题】:JavaFx WebView callback from Javascript failing after Garbage Collection 【发布时间】:2017-06-13 16:14:53 【问题描述】:我目前正在开发一个基于 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>
点击"Send data to Java in endless loop"
按钮可以观察到回调变量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
返回object
,javaApp === undefined
计算为false
。这与回调调用不抛出异常的事实一致(否则,将打印以"caugt 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,问题解决了。 @AliAyadJalil:回退到旧版本并不总是可取的,甚至是不可能的。 :-/ 【参考方案1】:我通过在 Java 中创建一个实例变量 bridge
来解决该问题,该变量包含通过 setMember()
发送到 Javascript 的 JavaScriptBridge
实例。这样可以防止实例的垃圾收集。
相关代码sn-p:
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,不幸的是手头没有这些信息......) p>
【讨论】:
非常感谢您的代码挽救了我的一天,应该接受答案 天啊。多么可怕的海森堡。感谢一百万的这个答案。我花了几天时间寻找问题的根源。 这不是一种解决方法,它记录在 WebEngine 类中,您的应用程序负责维护硬引用以防止对您的对象进行垃圾收集。文档摘录: 请注意,在上面的示例中,应用程序持有对 JavaApplication 实例的引用。这是 JavaScript 回调执行所需方法所必需的。在以下示例中,应用程序不持有对 Java 对象的引用。在这种情况下,由于属性值是一个本地对象,该值可能会在下一次 GC 循环中被垃圾回收。 @dsh 感谢您的评论!我编辑了我的答案以参考 Java 文档。以上是关于垃圾收集后来自 Javascript 的 JavaFx WebView 回调失败的主要内容,如果未能解决你的问题,请参考以下文章