Javafx WebEngine:后台工作者到底发生了什么? UI 挂在 loadContent(大 HTML 文档)
Javafx WebEngine: what really happens in a background worker? UI hangs on loadContent(big HTML document)
来自 WebEngine 文档:
Loading always happens on a background thread. Methods that initiate
loading return immediately after scheduling a background job. To track
progress and/or cancel a job, use the Worker instance available from
the getLoadWorker() method.
我有一个 HTML string,我通过 WebEngine.loadContent(String)
在 WebView 上加载它。该字符串大约有 500 万个字符长。在 运行 在 Platform.runLater()
中(我必须在 JavaFX 线程中 运行 它,否则我会得到一个错误)我的 UI 挂起大约一分钟。
如果我不 运行 它在 Platform.runLater()
我得到:
java.lang.IllegalStateException: Not on FX application thread; currentThread = populator
at com.sun.javafx.tk.Toolkit.checkFxUserThread(Toolkit.java:236)
at com.sun.javafx.tk.quantum.QuantumToolkit.checkFxUserThread(QuantumToolkit.java:423)
at javafx.scene.web.WebEngine.checkThread(WebEngine.java:1216)
at javafx.scene.web.WebEngine.loadContent(WebEngine.java:931)
at javafx.scene.web.WebEngine.loadContent(WebEngine.java:919)
...
我无法通过 webEngine.getLoadWorker().getProgress()
查询 webEngine,也无法通过 webEngine.getLoadWorker().cancel()
取消,因为我必须再次 运行 在挂起的 JavaFX 线程上...
所以我必须等到页面加载,然后之前(在网页加载过程中)提交的任何 Platform.runLater(()->webEngine.getLoadWorker().getProgress())
都会 运行,每次给我 1.0...
我用来查询 Worker 的代码:
// WebView
wvIn.getEngine().getLoadWorker().stateProperty().addListener(new ChangeListener<Worker.State>() {
class ProgressThread extends Thread {
private Worker.State loadWorkerState;
synchronized Worker.State getLoadWorkerState() {
return loadWorkerState;
}
synchronized void setLoadWorkerState(Worker.State loadWorkerState) {
this.loadWorkerState = loadWorkerState;
}
{
setDaemon(true);
setName("LoadingWebpageProgressThread");
}
public void run() {
while (true) {
try {
if (getLoadWorkerState() == Worker.State.RUNNING)
// piWv ProgressIndicator (WebView loading)
Platform.runLater(() -> piWv.setVisible(true));
while (getLoadWorkerState() == Worker.State.RUNNING) {
Platform.runLater(() -> {
piWv.setProgress(wvIn.getEngine().getLoadWorker().getProgress());
// TODO delete
System.out.println(wvIn.getEngine().getLoadWorker().getProgress());
});
Thread.sleep(100);
}
if (getLoadWorkerState() == Worker.State.SUCCEEDED) {
Platform.runLater(() -> piWv.setProgress(1d));
Thread.sleep(100);
Platform.runLater(() -> {
piWv.setVisible(false);
piWv.setProgress(0d);
});
}
synchronized (this) {
wait();
}
} catch (InterruptedException e) {
}
}
}
};
final ProgressThread progressThread = new ProgressThread();
{
progressThread.start();
}
// executed on JavaFX Thread
@Override
public void changed(ObservableValue<? extends State> observable, State oldValue, State newValue) {
if (newValue == State.SUCCEEDED) {
JSObject window = (JSObject) wvIn.getEngine().executeScript("window");
window.setMember("controller", mainController);
progressThread.setLoadWorkerState(newValue);
progressThread.interrupt();
} else if (newValue == State.RUNNING) {
progressThread.setLoadWorkerState(newValue);
progressThread.interrupt();
}
// TODO delete
System.out.println(oldValue + "->" + newValue);
}
});
有没有办法在后台线程中强制加载?
JavaFX 线程中到底发生了什么?是填充WebView的过程吗?
你在问题中显示的 HTML 文件几乎立即加载到我的机器上。
在我看来,问题出在您的代码中。好吧,这很麻烦。请让我指出它看起来不太好,因为它是 deadlock-prone, and uses exceptions to implement a form of guarded-blocks.
看来你还没有完全理解events的概念。
除非您为了简洁起见以某种方式切断了问题中的代码片段,否则 ProgressThread
实例是完全无用的。
您已经 ChangeListener
根据需要分派事件,在 JavaFX 应用程序线程上执行回调;随便用吧。
indicator.setVisible(false); // Start hidden
engine.getLoadWorker().progressProperty().addListener((observable, oldValue, newValue) -> {
indicator.setProgress(newValue.doubleValue());
indicator.setVisible(indicator.getProgress() != 1D);
});
或者,您可以使用 binding 的强大功能:
Worker<Void> worker = engine.getLoadWorker();
indicator.visibleProperty().bind(worker.stateProperty().isEqualTo(Worker.State.RUNNING));
indicator.progressProperty().bind(worker.progressProperty());
对于您的 JavaScript 代码,我建议您监听文档事件而不是工作状态事件:它们在您的情况下更有意义,并且在页面实际完成呈现时触发它们,而不仅仅是当页面数据下载完成时。
engine.documentProperty().addListener((observable, oldValue, newValue) -> {
if (newValue == null) {
return;
}
JSObject window = (JSObject)engine.executeScript("window");
window.setMember("controller", mainController);
});
请注意,在我的机器上,加载过程中进度似乎始终为零。我认为问题出在您的 HTML 代码中:如果您尝试加载一些其他资源,例如您正在阅读的网页,进度指示器将提供更愉快的体验,在页面内容已完成加载。
所以...
我最终构建了自己的 Http 服务器来测试 load
而不是 loadContent
(注意:这是一个糟糕的服务器):
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Calendar;
import java.util.Locale;
import java.util.TimeZone;
import java.util.zip.GZIPOutputStream;
public class BadHttpServer {
private static int request = 0, connection = 0;
private static final BadHttpServer SINGLETON = new BadHttpServer();
private ServerSocket serverSocket;
private String htmlString, formattedDate;
private byte[] compressedHTML;
public String getURL() {
return "http://localhost:60000";
}
synchronized byte[] getCompressedHTML() {
return compressedHTML;
}
synchronized void setCompressedHTML(String string) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream(string.length() / 2);
GZIPOutputStream gos = new GZIPOutputStream(baos);) {
gos.write(string.getBytes("utf-8"));
gos.close();
this.compressedHTML = baos.toByteArray();
formattedDate = String.format(Locale.US, "%1$ta, %1$td %1$tb %1$tY %1$tH:%1$tM:%1$tS %1$tZ",
Calendar.getInstance(TimeZone.getTimeZone("GMT")));
} catch (IOException e) {
e.printStackTrace();
}
}
private synchronized String getFormattedDate() {
return formattedDate;
}
private BadHttpServer() {
try {
serverSocket = new ServerSocket(60000);
} catch (IOException e) {
e.printStackTrace();
}
new Thread() {
{
setDaemon(true);
setName("serverSocketThread");
}
public void run() {
while (true) {
try {
new Thread(acceptAndSendHTML(serverSocket.accept())) {
{
setDaemon(true);
setName("acceptThread" + connection++);
}
}.start();
} catch (IOException e) {
e.printStackTrace();
}
}
};
}.start();
}
protected Runnable acceptAndSendHTML(final Socket socket) {
return new Runnable() {
@Override
public void run() {
try (BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream(), "ASCII"));
BufferedWriter asciiWriter = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream(), "ASCII"));
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream(), 1000)) {
while (!socket.isClosed()) {
int request = BadHttpServer.request++;
// READING
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(br.readLine() + "\r\n");
for (String line = ""; !(line = br.readLine()).isEmpty(); stringBuilder.append(line + "\r\n"))
;
// TODO delete
System.out.print("Connection " + request + " request: " + stringBuilder);
// WRITING
byte[] compressedHTML = getCompressedHTML();
asciiWriter.write("HTTP/1.1 100 Continue\r\n");
asciiWriter.write("\r\n");
asciiWriter.write("HTTP/1.1 200 OK\r\n");
asciiWriter.write("Date: " + getFormattedDate() + "\r\n");
asciiWriter.write("Content-Type: text/html; charset=utf-8\r\n");
asciiWriter.write("Content-Encoding: gzip\r\n");
asciiWriter.write("Content-Charset: utf-8\r\n");
asciiWriter.write("Content-Length: " + compressedHTML.length + "\r\n");
// TODO delete
System.out.println("Content-Length: " + compressedHTML.length + "\r\n");
asciiWriter.write("\r\n");
asciiWriter.flush();
for (int writtenOverall = 0, writtenThisIteration = 0; writtenOverall < compressedHTML.length; writtenOverall += writtenThisIteration) {
bos.write(compressedHTML, writtenOverall,
writtenThisIteration = Math.min(compressedHTML.length - writtenOverall, 1000));
Thread.sleep(10); // Bandwidth throttling
}
bos.flush();
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
}
public static BadHttpServer getIntance() {
return SINGLETON;
}
public synchronized void setHtmlString(String htmlString) {
this.htmlString = htmlString;
setCompressedHTML(htmlString);
}
synchronized String getHtmlString() {
return htmlString;
}
}
现在,我
而不是 loadContent
BadHttpServer.getIntance().setHtmlString(htmlString);
Platform.runLater(() -> wvIn.getEngine().load(BadHttpServer.getIntance().getURL()));
我做了一些基准测试——测量从 loadWorker 启动 RUNNING
到 SUCCEDED
:
的时间
via BadHttpServer:
javascript and style after body: 13,5 s
javascript and style before body: 8,6 s
so I continued to test with javascript and style before body.
without javascript injection: 8,5 s
I continued to test with javascript injection.
Next I did with loadContent: 170 s - no UI resposivness during loading
Yep - almost 3 minutes...
So I returned back with BadHttpServer.
Next I tried "throttling the bandwidth" by changing the sleep time.
no sleep: 8,2 s
5 ms sleep: 6,8 s
10 ms sleep: 6,7 s
20 ms sleep: 11,6 s
In these cases the UI was responsive, although it was lagging a bit.
The progressIndicator showed nicely, pausing a bit at 90%.
By increasing the sleep time, the UI was more responsive.
所以,我想 loadContent
方法可能会好一点,除非我在实施它时做错了什么...
来自 WebEngine 文档:
Loading always happens on a background thread. Methods that initiate loading return immediately after scheduling a background job. To track progress and/or cancel a job, use the Worker instance available from the getLoadWorker() method.
我有一个 HTML string,我通过 WebEngine.loadContent(String)
在 WebView 上加载它。该字符串大约有 500 万个字符长。在 运行 在 Platform.runLater()
中(我必须在 JavaFX 线程中 运行 它,否则我会得到一个错误)我的 UI 挂起大约一分钟。
如果我不 运行 它在 Platform.runLater()
我得到:
java.lang.IllegalStateException: Not on FX application thread; currentThread = populator
at com.sun.javafx.tk.Toolkit.checkFxUserThread(Toolkit.java:236)
at com.sun.javafx.tk.quantum.QuantumToolkit.checkFxUserThread(QuantumToolkit.java:423)
at javafx.scene.web.WebEngine.checkThread(WebEngine.java:1216)
at javafx.scene.web.WebEngine.loadContent(WebEngine.java:931)
at javafx.scene.web.WebEngine.loadContent(WebEngine.java:919)
...
我无法通过 webEngine.getLoadWorker().getProgress()
查询 webEngine,也无法通过 webEngine.getLoadWorker().cancel()
取消,因为我必须再次 运行 在挂起的 JavaFX 线程上...
所以我必须等到页面加载,然后之前(在网页加载过程中)提交的任何 Platform.runLater(()->webEngine.getLoadWorker().getProgress())
都会 运行,每次给我 1.0...
我用来查询 Worker 的代码:
// WebView
wvIn.getEngine().getLoadWorker().stateProperty().addListener(new ChangeListener<Worker.State>() {
class ProgressThread extends Thread {
private Worker.State loadWorkerState;
synchronized Worker.State getLoadWorkerState() {
return loadWorkerState;
}
synchronized void setLoadWorkerState(Worker.State loadWorkerState) {
this.loadWorkerState = loadWorkerState;
}
{
setDaemon(true);
setName("LoadingWebpageProgressThread");
}
public void run() {
while (true) {
try {
if (getLoadWorkerState() == Worker.State.RUNNING)
// piWv ProgressIndicator (WebView loading)
Platform.runLater(() -> piWv.setVisible(true));
while (getLoadWorkerState() == Worker.State.RUNNING) {
Platform.runLater(() -> {
piWv.setProgress(wvIn.getEngine().getLoadWorker().getProgress());
// TODO delete
System.out.println(wvIn.getEngine().getLoadWorker().getProgress());
});
Thread.sleep(100);
}
if (getLoadWorkerState() == Worker.State.SUCCEEDED) {
Platform.runLater(() -> piWv.setProgress(1d));
Thread.sleep(100);
Platform.runLater(() -> {
piWv.setVisible(false);
piWv.setProgress(0d);
});
}
synchronized (this) {
wait();
}
} catch (InterruptedException e) {
}
}
}
};
final ProgressThread progressThread = new ProgressThread();
{
progressThread.start();
}
// executed on JavaFX Thread
@Override
public void changed(ObservableValue<? extends State> observable, State oldValue, State newValue) {
if (newValue == State.SUCCEEDED) {
JSObject window = (JSObject) wvIn.getEngine().executeScript("window");
window.setMember("controller", mainController);
progressThread.setLoadWorkerState(newValue);
progressThread.interrupt();
} else if (newValue == State.RUNNING) {
progressThread.setLoadWorkerState(newValue);
progressThread.interrupt();
}
// TODO delete
System.out.println(oldValue + "->" + newValue);
}
});
有没有办法在后台线程中强制加载?
JavaFX 线程中到底发生了什么?是填充WebView的过程吗?
你在问题中显示的 HTML 文件几乎立即加载到我的机器上。
在我看来,问题出在您的代码中。好吧,这很麻烦。请让我指出它看起来不太好,因为它是 deadlock-prone, and uses exceptions to implement a form of guarded-blocks.
看来你还没有完全理解events的概念。
除非您为了简洁起见以某种方式切断了问题中的代码片段,否则 ProgressThread
实例是完全无用的。
您已经 ChangeListener
根据需要分派事件,在 JavaFX 应用程序线程上执行回调;随便用吧。
indicator.setVisible(false); // Start hidden
engine.getLoadWorker().progressProperty().addListener((observable, oldValue, newValue) -> {
indicator.setProgress(newValue.doubleValue());
indicator.setVisible(indicator.getProgress() != 1D);
});
或者,您可以使用 binding 的强大功能:
Worker<Void> worker = engine.getLoadWorker();
indicator.visibleProperty().bind(worker.stateProperty().isEqualTo(Worker.State.RUNNING));
indicator.progressProperty().bind(worker.progressProperty());
对于您的 JavaScript 代码,我建议您监听文档事件而不是工作状态事件:它们在您的情况下更有意义,并且在页面实际完成呈现时触发它们,而不仅仅是当页面数据下载完成时。
engine.documentProperty().addListener((observable, oldValue, newValue) -> {
if (newValue == null) {
return;
}
JSObject window = (JSObject)engine.executeScript("window");
window.setMember("controller", mainController);
});
请注意,在我的机器上,加载过程中进度似乎始终为零。我认为问题出在您的 HTML 代码中:如果您尝试加载一些其他资源,例如您正在阅读的网页,进度指示器将提供更愉快的体验,在页面内容已完成加载。
所以...
我最终构建了自己的 Http 服务器来测试 load
而不是 loadContent
(注意:这是一个糟糕的服务器):
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Calendar;
import java.util.Locale;
import java.util.TimeZone;
import java.util.zip.GZIPOutputStream;
public class BadHttpServer {
private static int request = 0, connection = 0;
private static final BadHttpServer SINGLETON = new BadHttpServer();
private ServerSocket serverSocket;
private String htmlString, formattedDate;
private byte[] compressedHTML;
public String getURL() {
return "http://localhost:60000";
}
synchronized byte[] getCompressedHTML() {
return compressedHTML;
}
synchronized void setCompressedHTML(String string) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream(string.length() / 2);
GZIPOutputStream gos = new GZIPOutputStream(baos);) {
gos.write(string.getBytes("utf-8"));
gos.close();
this.compressedHTML = baos.toByteArray();
formattedDate = String.format(Locale.US, "%1$ta, %1$td %1$tb %1$tY %1$tH:%1$tM:%1$tS %1$tZ",
Calendar.getInstance(TimeZone.getTimeZone("GMT")));
} catch (IOException e) {
e.printStackTrace();
}
}
private synchronized String getFormattedDate() {
return formattedDate;
}
private BadHttpServer() {
try {
serverSocket = new ServerSocket(60000);
} catch (IOException e) {
e.printStackTrace();
}
new Thread() {
{
setDaemon(true);
setName("serverSocketThread");
}
public void run() {
while (true) {
try {
new Thread(acceptAndSendHTML(serverSocket.accept())) {
{
setDaemon(true);
setName("acceptThread" + connection++);
}
}.start();
} catch (IOException e) {
e.printStackTrace();
}
}
};
}.start();
}
protected Runnable acceptAndSendHTML(final Socket socket) {
return new Runnable() {
@Override
public void run() {
try (BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream(), "ASCII"));
BufferedWriter asciiWriter = new BufferedWriter(
new OutputStreamWriter(socket.getOutputStream(), "ASCII"));
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream(), 1000)) {
while (!socket.isClosed()) {
int request = BadHttpServer.request++;
// READING
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(br.readLine() + "\r\n");
for (String line = ""; !(line = br.readLine()).isEmpty(); stringBuilder.append(line + "\r\n"))
;
// TODO delete
System.out.print("Connection " + request + " request: " + stringBuilder);
// WRITING
byte[] compressedHTML = getCompressedHTML();
asciiWriter.write("HTTP/1.1 100 Continue\r\n");
asciiWriter.write("\r\n");
asciiWriter.write("HTTP/1.1 200 OK\r\n");
asciiWriter.write("Date: " + getFormattedDate() + "\r\n");
asciiWriter.write("Content-Type: text/html; charset=utf-8\r\n");
asciiWriter.write("Content-Encoding: gzip\r\n");
asciiWriter.write("Content-Charset: utf-8\r\n");
asciiWriter.write("Content-Length: " + compressedHTML.length + "\r\n");
// TODO delete
System.out.println("Content-Length: " + compressedHTML.length + "\r\n");
asciiWriter.write("\r\n");
asciiWriter.flush();
for (int writtenOverall = 0, writtenThisIteration = 0; writtenOverall < compressedHTML.length; writtenOverall += writtenThisIteration) {
bos.write(compressedHTML, writtenOverall,
writtenThisIteration = Math.min(compressedHTML.length - writtenOverall, 1000));
Thread.sleep(10); // Bandwidth throttling
}
bos.flush();
}
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
}
public static BadHttpServer getIntance() {
return SINGLETON;
}
public synchronized void setHtmlString(String htmlString) {
this.htmlString = htmlString;
setCompressedHTML(htmlString);
}
synchronized String getHtmlString() {
return htmlString;
}
}
现在,我
而不是loadContent
BadHttpServer.getIntance().setHtmlString(htmlString);
Platform.runLater(() -> wvIn.getEngine().load(BadHttpServer.getIntance().getURL()));
我做了一些基准测试——测量从 loadWorker 启动 RUNNING
到 SUCCEDED
:
via BadHttpServer: javascript and style after body: 13,5 s javascript and style before body: 8,6 s so I continued to test with javascript and style before body. without javascript injection: 8,5 s I continued to test with javascript injection. Next I did with loadContent: 170 s - no UI resposivness during loading Yep - almost 3 minutes... So I returned back with BadHttpServer. Next I tried "throttling the bandwidth" by changing the sleep time. no sleep: 8,2 s 5 ms sleep: 6,8 s 10 ms sleep: 6,7 s 20 ms sleep: 11,6 s In these cases the UI was responsive, although it was lagging a bit. The progressIndicator showed nicely, pausing a bit at 90%. By increasing the sleep time, the UI was more responsive.
所以,我想 loadContent
方法可能会好一点,除非我在实施它时做错了什么...