【问题标题】:When is a WebView ready for a snapshot()?WebView 何时准备好进行快照()?
【发布时间】:2020-05-05 06:56:44
【问题描述】:

JavaFX 文档声明 WebView is ready when Worker.State.SUCCEEDED is reached 但是,除非您等待一段时间(即 AnimationTransitionPauseTransition 等),否则会呈现一个空白页面。

这表明 WebView 内部发生了一个事件,准备捕获它,但它是什么?

over 7,000 code snippets on GitHub which use SwingFXUtils.fromFXImage,但它们中的大多数似乎与 WebView 无关,是交互式的(人类掩盖了竞争条件)或使用任意转换(从 100 毫秒到 2000 毫秒)。

我试过了:

  • WebView的维度内监听changed(...)(高度和宽度属性DoubleProperty实现ObservableValue,可以监控这些东西)

    • ????不可行。有时,值似乎会独立于绘制例程而发生变化,从而导致部分内容。
  • 在 FX 应用程序线程上盲目地将任何事情告诉 runLater(...)

    • ????许多技术都使用它,但我自己的单元测试(以及其他开发人员的一些很好的反馈)解释说事件通常已经在正确的线程上,这个调用是多余的。我能想到的最好的办法是通过排队增加足够的延迟,这对某些人有用。
  • 将 DOM 侦听器/触发器或 JavaScript 侦听器/触发器添加到 WebView

    • ????尽管有空白捕获,但在调用SUCCEEDED 时,JavaScript 和 DOM 似乎都已正确加载。 DOM/JavaScript 监听器似乎没有帮助。
  • 使用AnimationTransition 有效地“休眠”而不阻塞主FX 线程。

    • ⚠️ 这种方法有效,如果延迟足够长,可以产生高达 100% 的单元测试,但转换时间似乎是 some future moment that we're just guessing 和糟糕的设计。对于高性能或任务关键型应用程序,这迫使程序员在速度或可靠性之间做出权衡,这对用户来说都是一种潜在的糟糕体验。

什么时候可以拨打WebView.snapshot(...)

用法:

SnapshotRaceCondition.initialize();
BufferedImage bufferedImage = SnapshotRaceCondition.capture("<html style='background-color: red;'><h1>TEST</h1></html>");
/**
 * Notes:
 * - The color is to observe the otherwise non-obvious cropping that occurs
 *   with some techniques, such as `setPrefWidth`, `autosize`, etc.
 * - Call this function in a loop and then display/write `BufferedImage` to
 *   to see strange behavior on subsequent calls.
 * - Recommended, modify `<h1>TEST</h1` with a counter to see content from
 *   previous captures render much later.
 */

代码片段:

import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.embed.swing.SwingFXUtils;
import javafx.scene.Scene;
import javafx.scene.SnapshotParameters;
import javafx.scene.image.WritableImage;
import javafx.scene.web.WebView;
import javafx.stage.Stage;

import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Logger;

public class SnapshotRaceCondition extends Application  {
    private static final Logger log = Logger.getLogger(SnapshotRaceCondition.class.getName());

    // self reference
    private static SnapshotRaceCondition instance = null;

    // concurrent-safe containers for flags/exceptions/image data
    private static AtomicBoolean started  = new AtomicBoolean(false);
    private static AtomicBoolean finished  = new AtomicBoolean(true);
    private static AtomicReference<Throwable> thrown = new AtomicReference<>(null);
    private static AtomicReference<BufferedImage> capture = new AtomicReference<>(null);

    // main javafx objects
    private static WebView webView = null;
    private static Stage stage = null;

    // frequency for checking fx is started
    private static final int STARTUP_TIMEOUT= 10; // seconds
    private static final int STARTUP_SLEEP_INTERVAL = 250; // millis

    // frequency for checking capture has occured 
    private static final int CAPTURE_SLEEP_INTERVAL = 10; // millis

    /** Called by JavaFX thread */
    public SnapshotRaceCondition() {
        instance = this;
    }

    /** Starts JavaFX thread if not already running */
    public static synchronized void initialize() throws IOException {
        if (instance == null) {
            new Thread(() -> Application.launch(SnapshotRaceCondition.class)).start();
        }

        for(int i = 0; i < (STARTUP_TIMEOUT * 1000); i += STARTUP_SLEEP_INTERVAL) {
            if (started.get()) { break; }

            log.fine("Waiting for JavaFX...");
            try { Thread.sleep(STARTUP_SLEEP_INTERVAL); } catch(Exception ignore) {}
        }

        if (!started.get()) {
            throw new IOException("JavaFX did not start");
        }
    }


    @Override
    public void start(Stage primaryStage) {
        started.set(true);
        log.fine("Started JavaFX, creating WebView...");
        stage = primaryStage;
        primaryStage.setScene(new Scene(webView = new WebView()));

        // Add listener for SUCCEEDED
        Worker<Void> worker = webView.getEngine().getLoadWorker();
        worker.stateProperty().addListener(stateListener);

        // Prevents JavaFX from shutting down when hiding window, useful for calling capture(...) in succession
        Platform.setImplicitExit(false);
    }

    /** Listens for a SUCCEEDED state to activate image capture **/
    private static ChangeListener<Worker.State> stateListener = (ov, oldState, newState) -> {
        if (newState == Worker.State.SUCCEEDED) {
            WritableImage snapshot = webView.snapshot(new SnapshotParameters(), null);

            capture.set(SwingFXUtils.fromFXImage(snapshot, null));
            finished.set(true);
            stage.hide();
        }
    };

    /** Listen for failures **/
    private static ChangeListener<Throwable> exceptListener = new ChangeListener<Throwable>() {
        @Override
        public void changed(ObservableValue<? extends Throwable> obs, Throwable oldExc, Throwable newExc) {
            if (newExc != null) { thrown.set(newExc); }
        }
    };

    /** Loads the specified HTML, triggering stateListener above **/
    public static synchronized BufferedImage capture(final String html) throws Throwable {
        capture.set(null);
        thrown.set(null);
        finished.set(false);

        // run these actions on the JavaFX thread
        Platform.runLater(new Thread(() -> {
            try {
                webView.getEngine().loadContent(html, "text/html");
                stage.show(); // JDK-8087569: will not capture without showing stage
                stage.toBack();
            }
            catch(Throwable t) {
                thrown.set(t);
            }
        }));

        // wait for capture to complete by monitoring our own finished flag
        while(!finished.get() && thrown.get() == null) {
            log.fine("Waiting on capture...");
            try {
                Thread.sleep(CAPTURE_SLEEP_INTERVAL);
            }
            catch(InterruptedException e) {
                log.warning(e.getLocalizedMessage());
            }
        }

        if (thrown.get() != null) {
            throw thrown.get();
        }

        return capture.get();
    }
}

相关:

【问题讨论】:

  • Platform.runLater 不是多余的。可能存在 WebView 完成其呈现所必需的待处理事件。 Platform.runLater 是我会尝试的第一件事。
  • 比赛和单元测试表明事件不是未决的,而是发生在一个单独的线程中。 Platform.runLater 已经过测试并且没有修复它。如果您不同意,请自行尝试。我很高兴错了,它会关闭问题。
  • 此外,官方文档将SUCCEEDED 状态(侦听器在 FX 线程上触发)是正确的技术。如果有办法显示排队事件,我会很高兴尝试。我在 Oracle 论坛上通过 cmets 找到了一些建议,一些 SO 问题WebView 必须按照设计在其自己的线程中运行,因此经过几天的测试,我将精力集中在那里。如果这个假设是错误的,那就太好了。我愿意接受任何可以解决问题而无需任意等待时间的合理建议。
  • 我自己写了一个很短的测试,并且能够成功地在负载工作者的状态监听器中获取一个 WebView 的快照。但是你的程序确实给了我一个空白页。我仍在尝试了解其中的区别。
  • 这似乎只在使用 loadContent 方法或加载文件 URL 时发生。

标签: java multithreading javafx race-condition


【解决方案1】:

这似乎是使用 WebEngine 的 loadContent 方法时出现的错误。使用load 加载本地文件时也会出现这种情况,但在这种情况下,调用reload() 会补偿它。

另外,由于需要在拍摄快照时显示舞台,因此您需要在加载内容之前调用show()。由于内容是异步加载的,因此完全有可能在调用 loadloadContent 之后的语句完成之前加载它。

因此,解决方法是将内容放在一个文件中,然后只调用一次 WebEngine 的 reload() 方法。第二次加载内容时,可以从加载工作线程的 state 属性的侦听器中成功获取快照。

通常,这很容易:

Path htmlFile = Files.createTempFile("snapshot-", ".html");
Files.writeString(htmlFile, html);

WebEngine engine = myWebView.getEngine();
engine.getLoadWorker().stateProperty().addListener(
    new ChangeListener<Worker.State>() {
        private boolean reloaded;

        @Override
        public void changed(ObservableValue<? extends Worker.State> obs,
                            Worker.State oldState,
                            Worker.State newState) {
            if (reloaded) {
                Image image = myWebView.snapshot(null, null);
                doStuffWithImage(image);

                try {
                    Files.delete(htmlFile);
                } catch (IOException e) {
                    log.log(Level.WARN, "Couldn't delete " + htmlFile, e);
                }
            } else {
                reloaded = true;
                engine.reload();
            }
        }
    });


engine.load(htmlFile.toUri().toString());

但是因为您对所有内容都使用static,所以您必须添加一些字段:

private static boolean reloaded;
private static volatile Path htmlFile;

你可以在这里使用它们:

/** Listens for a SUCCEEDED state to activate image capture **/
private static ChangeListener<Worker.State> stateListener = (ov, oldState, newState) -> {
    if (newState == Worker.State.SUCCEEDED) {
        if (reloaded) {
            WritableImage snapshot = webView.snapshot(new SnapshotParameters(), null);

            capture.set(SwingFXUtils.fromFXImage(snapshot, null));
            finished.set(true);
            stage.hide();

            try {
                Files.delete(htmlFile);
            } catch (IOException e) {
                log.log(Level.WARN, "Couldn't delete " + htmlFile, e);
            }
        } else {
            reloaded = true;
            webView.getEngine().reload();
        }
    }
};

然后您必须在每次加载内容时重置它:

Path htmlFile = Files.createTempFile("snapshot-", ".html");
Files.writeString(htmlFile, html);

Platform.runLater(new Thread(() -> {
    try {
        reloaded = false;
        stage.show(); // JDK-8087569: will not capture without showing stage
        stage.toBack();
        webView.getEngine().load(htmlFile);
    }
    catch(Throwable t) {
        thrown.set(t);
    }
}));

请注意,执行多线程处理有更好的方法。您可以简单地使用volatile 字段,而不是使用原子类:

private static volatile boolean started;
private static volatile boolean finished = true;
private static volatile Throwable thrown;
private static volatile BufferedImage capture;

(布尔字段默认为false,对象字段默认为null。与C程序不同,这是Java做出的硬保证;不存在未初始化的内存。)

与其在循环中轮询另一个线程中所做的更改,不如使用同步、锁或更高级别的类,如 CountDownLatch,它在内部使用这些东西:

private static final CountDownLatch initialized = new CountDownLatch(1);
private static volatile CountDownLatch finished;
private static volatile BufferedImage capture;
private static volatile Throwable thrown;
private static boolean reloaded;

private static volatile Path htmlFile;

// main javafx objects
private static WebView webView = null;
private static Stage stage = null;

private static ChangeListener<Worker.State> stateListener = (ov, oldState, newState) -> {
    if (newState == Worker.State.SUCCEEDED) {
        if (reloaded) {
            WritableImage snapshot = webView.snapshot(null, null);
            capture = SwingFXUtils.fromFXImage(snapshot, null);
            finished.countDown();
            stage.hide();

            try {
                Files.delete(htmlFile);
            } catch (IOException e) {
                log.log(Level.WARNING, "Could not delete " + htmlFile, e);
            }
        } else {
            reloaded = true;
            webView.getEngine().reload();
        }
    }
};

@Override
public void start(Stage primaryStage) {
    log.fine("Started JavaFX, creating WebView...");
    stage = primaryStage;
    primaryStage.setScene(new Scene(webView = new WebView()));

    Worker<Void> worker = webView.getEngine().getLoadWorker();
    worker.stateProperty().addListener(stateListener);

    webView.getEngine().setOnError(e -> {
        thrown = e.getException();
    });

    // Prevents JavaFX from shutting down when hiding window, useful for calling capture(...) in succession
    Platform.setImplicitExit(false);

    initialized.countDown();
}

public static BufferedImage capture(String html)
throws InterruptedException,
       IOException {

    htmlFile = Files.createTempFile("snapshot-", ".html");
    Files.writeString(htmlFile, html);

    if (initialized.getCount() > 0) {
        new Thread(() -> Application.launch(SnapshotRaceCondition2.class)).start();
        initialized.await();
    }

    finished = new CountDownLatch(1);
    thrown = null;

    Platform.runLater(() -> {
        reloaded = false;
        stage.show(); // JDK-8087569: will not capture without showing stage
        stage.toBack();
        webView.getEngine().load(htmlFile.toUri().toString());
    });

    finished.await();

    if (thrown != null) {
        throw new IOException(thrown);
    }

    return capture;
}

reloaded 未声明为 volatile,因为它只能在 JavaFX 应用程序线程中访问。

【讨论】:

  • 这是一篇非常好的文章,尤其是围绕线程和volatile 变量的代码改进。不幸的是,调用WebEngine.reload() 并等待后续的SUCCEEDED 不起作用。如果我在 HTML 内容中放置一个计数器,我会收到:0, 0, 1, 3, 3, 5 而不是0, 1, 2, 3, 4, 5,这表明它实际上并没有修复潜在的竞争条件。
  • 引用:“更好地使用 [...] CountDownLatch”。赞成,因为这些信息不容易找到,它有助于在初始 FX 启动时加快和简化代码。
【解决方案2】:

为了适应调整大小以及底层快照行为,我(我们)提出了以下可行的解决方案。请注意,这些测试运行了 2,000 倍(Windows、macOS 和 Linux),提供随机 WebView 大小并 100% 成功。

首先,我将引用其中一位 JavaFX 开发人员。这是从私人(赞助)错误报告中引用的:

“我假设您在 FX AppThread 上启动调整大小,并且在达到 SUCCEEDED 状态后完成。在这种情况下,在我看来,在那一刻,等待 2 个脉冲(不阻塞 FX AppThread)应该给 webkit 实现足够的时间来进行更改,除非这会导致 JavaFX 中的某些维度发生更改,这可能会再次导致 webkit 内部的维度发生更改。

我正在考虑如何将这些信息提供给 JBS 中的讨论,但我很确定会有“只有在 web 组件稳定时才应该拍摄快照”的答案。因此,为了预测这个答案,最好看看这种方法是否适合你。或者,如果结果证明会导致其他问题,最好考虑一下这些问题,看看是否/如何在 OpenJFX 本身中解决它们。”

  1. 默认情况下,如果高度正好是0,JavaFX 8 使用默认值600。 代码重用WebView 应该使用setMinHeight(1), setPrefHeight(1) 来避免这个问题。这不在下面的代码中,但对于任何将其应用于他们的项目的人来说都值得一提。
  2. 为了适应 WebKit 的准备情况,请等待来自动画计时器的恰好两个脉冲。
  3. 为防止出现快照空白错误,请利用快照回调,该回调也会侦听脉冲。
// without this runlater, the first capture is missed and all following captures are offset
Platform.runLater(new Runnable() {
    public void run() {
        // start a new animation timer which waits for exactly two pulses
        new AnimationTimer() {
            int frames = 0;

            @Override
            public void handle(long l) {
                // capture at exactly two frames
                if (++frames == 2) {
                    System.out.println("Attempting image capture");
                    webView.snapshot(new Callback<SnapshotResult,Void>() {
                        @Override
                        public Void call(SnapshotResult snapshotResult) {
                            capture.set(SwingFXUtils.fromFXImage(snapshotResult.getImage(), null));
                            unlatch();
                            return null;
                        }
                    }, null, null);

                    //stop timer after snapshot
                    stop();
                }
            }
        }.start();
    }
});

【讨论】:

    猜你喜欢
    • 2017-01-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-09-01
    • 1970-01-01
    • 2014-12-01
    • 2010-12-16
    • 1970-01-01
    相关资源
    最近更新 更多