前言:该问题通常是重复问题的目标,这些问题询问如何在JavaFX中执行定期操作,无论该操作是否应在后台执行。尽管该问题已经有了不错的答案,但该答案试图将所有给定的信息(以及更多信息)合并为一个答案,并说明/显示每种方法之间的差异。
该答案侧重于JavaSE和JavaFX中可用的API,而不是第三方库(如ReactFX)(在Tomas Mikula的答案中展示)。
背景信息:JavaFX和线程
像大多数主流GUI框架一样,JavaFX是单线程的。这意味着只有一个线程专门用于读取和写入UI状态以及处理用户生成的事件(例如,鼠标事件,按键事件等)。在JavaFX中,此线程称为“ JavaFX Application Thread”,有时简称为“ FX thread”,但其他框架可能将其称为其他名称。其他一些名称包括“ UI线程”,“事件调度线程”和“主线程”。
绝对重要的是,只有在JavaFX Application Thread上才能访问或操纵与屏幕上显示的GUI相连的任何内容。JavaFX框架不是线程安全的,使用其他线程来不正确地读取或写入UI状态可能会导致未定义的行为。即使您看不到任何外部可见的问题,在没有必要的同步的情况下访问线程之间共享的状态也会破坏代码。
但是,许多GUI对象只要不处于活动状态就可以在任何线程上进行操作。从以下文档中javafx.scene.Node
:
节点对象可以被构造和修改上的任何线程,只要它们还没有附着到Scene
在一个Window
即showing
[加上强调]。应用程序必须将节点附加到此类场景,或在JavaFX Application Thread上对其进行修改。
但是其他GUI对象(例如Window
甚至是Node
(例如WebView
)的某些子类)更加严格。例如,从以下文档中javafx.stage.Window
:
窗口对象必须在JavaFX Application Thread上构造和修改。
如果不确定GUI对象的线程规则,则其文档应提供所需的信息。
由于JavaFX是单线程的,因此您还必须确保不要阻塞或独占FX线程。如果线程不能自由地执行其工作,则永远不会重绘UI,并且无法处理用户生成的新事件。不遵循此规则可能会导致臭名昭著的无响应/冻结的UI,并且用户不满意。
它几乎总是错误的睡觉的JavaFX应用程序线程。
定期任务
有两种不同类型的定期任务,至少出于此答案的目的:
- 定期前台“任务”。
- 这可能包括诸如“闪烁”节点或在图像之间定期切换之类的内容。
- 定期的后台任务。
- 一个示例可能是定期检查远程服务器是否有更新,如果有更新,则下载新信息并将其显示给用户。
定期前台任务
如果您的周期性任务又短又简单,那么使用后台线程就显得过头了,只会增加不必要的复杂性。更合适的解决方案是使用javafx.animation
API。动画是异步的,但完全保留在JavaFX Application Thread中。换句话说,动画提供了一种在FX线程上“循环”的方法,每次迭代之间都有延迟,而无需实际使用循环。
有三类独特地适合于周期性前台任务。
时间线
ATimeline
由一个或多个KeyFrame
s组成。每个KeyFrame
都有一个指定的完成时间。每个人也可以有一个“完成”处理程序,该处理程序在指定的时间量过去之后被调用。这意味着您可以创建一个Timeline
具有单个的KeyFrame
,该定期执行一个动作,并根据需要进行多次循环(包括ever)。
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class App extends Application {
@Override
public void start(Stage primaryStage) {
Rectangle rect = new Rectangle(100, 100);
Timeline timeline =
new Timeline(new KeyFrame(Duration.millis(500), e -> rect.setVisible(!rect.isVisible())));
timeline.setCycleCount(Animation.INDEFINITE);
timeline.play();
primaryStage.setScene(new Scene(new StackPane(rect), 200, 200));
primaryStage.show();
}
}
由于aTimeline
可以有多个,KeyFrame
因此可以以不同的间隔执行动作。请记住,每个时间KeyFrame
都不会叠加。如果您的一个动画的时间为2秒,KeyFrame
然后另一个动画KeyFrame
的时间为2秒,则两个KeyFrame
s将在动画开始后两秒钟完成。要使第二个KeyFrame
完成比第一个完成晚2秒,它的时间需要为4秒。
暂停过渡
与其他动画类不同,aPauseTransition
并不用于实际设置任何动画。它的主要目的是用作SequentialTransition
在其他两个动画之间暂停的子级。但是,像Animation
它的所有子类一样,它可以有一个“完成”处理程序,该处理程序在完成后执行,从而可以用于定期任务。
import javafx.animation.PauseTransition;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class App extends Application {
@Override
public void start(Stage primaryStage) {
Rectangle rect = new Rectangle(100, 100);
PauseTransition pause = new PauseTransition(Duration.millis(500));
pause.setOnFinished(
e -> {
rect.setVisible(!rect.isVisible());
pause.playFromStart();
});
pause.play();
primaryStage.setScene(new Scene(new StackPane(rect), 200, 200));
primaryStage.show();
}
}
注意,完成的处理程序将调用playFromStart()
。这是再次“循环”动画的必要条件。该cycleCount
属性不能使用,因为未完成的处理程序不会在每个周期的末尾调用,而是仅在最后一个周期的末尾调用。同样的事实也是如此Timeline
; 之所以可以在Timeline
上面使用它,是因为未完成的处理程序未在中注册,Timeline
而是在中注册了KeyFrame
。
由于该cycleCount
属性不能PauseTransition
用于多个循环,因此仅循环一定次数(而不是永远循环)变得更加困难。您必须自己跟踪状态,并且仅playFromStart()
在适当时调用。请记住,在lambda表达式或匿名类之外声明但在所述lambda表达式或匿名类内部使用的局部变量必须是final或有效的final。
动画计时器
该AnimationTimer
班是JavaFX的动画API的最低水平。它不是它的子类,Animation
因此没有上面使用的任何属性。相反,它有一个抽象方法,当启动计时器时,每帧以当前帧的时间戳(以纳秒为单位)调用一次#handle(long)
。为了定期执行某些操作AnimationTimer
(每帧一次),需要手动计算handle
使用该方法的参数的两次调用之间的时间差。
import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class App extends Application {
@Override
public void start(Stage primaryStage) {
Rectangle rect = new Rectangle(100, 100);
AnimationTimer timer =
new AnimationTimer() {
private long lastToggle;
@Override
public void handle(long now) {
if (lastToggle == 0L) {
lastToggle = now;
} else {
long diff = now - lastToggle;
if (diff >= 500_000_000L) {
rect.setVisible(!rect.isVisible());
lastToggle = now;
}
}
}
};
timer.start();
primaryStage.setScene(new Scene(new StackPane(rect), 200, 200));
primaryStage.show();
}
}
对于与上述相似的大多数用例,使用Timeline
或PauseTransition
将是更好的选择。
定期后台任务
如果您的定期任务很耗时(例如,昂贵的计算)或阻塞(例如,I / O),则需要使用后台线程。JavaFX内置了一些并发实用程序,以辅助后台线程和FX线程之间的通信。这些实用程序在以下内容中进行了描述:
对于需要与FX线程通信的定期后台任务,要使用的类为javafx.concurrent.ScheduledService
。该类将定期执行其任务,并根据指定的时间段在成功执行后重新启动。如果配置为这样做,它甚至会在执行失败后重试可配置的次数。
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.concurrent.Worker.State;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;
public class App extends Application {
private UpdateCheckService service;
@Override
public void start(Stage primaryStage) {
service = new UpdateCheckService();
service.setPeriod(Duration.seconds(5));
Label resultLabel = new Label();
service.setOnRunning(e -> resultLabel.setText(null));
service.setOnSucceeded(
e -> {
if (service.getValue()) {
resultLabel.setText("UPDATES AVAILABLE");
} else {
resultLabel.setText("UP-TO-DATE");
}
});
Label msgLabel = new Label();
msgLabel.textProperty().bind(service.messageProperty());
ProgressBar progBar = new ProgressBar();
progBar.setMaxWidth(Double.MAX_VALUE);
progBar.progressProperty().bind(service.progressProperty());
progBar.visibleProperty().bind(service.stateProperty().isEqualTo(State.RUNNING));
VBox box = new VBox(3, msgLabel, progBar);
box.setMaxHeight(Region.USE_PREF_SIZE);
box.setPadding(new Insets(3));
StackPane root = new StackPane(resultLabel, box);
StackPane.setAlignment(box, Pos.BOTTOM_LEFT);
primaryStage.setScene(new Scene(root, 400, 200));
primaryStage.show();
service.start();
}
private static class UpdateCheckService extends ScheduledService<Boolean> {
@Override
protected Task<Boolean> createTask() {
return new Task<>() {
@Override
protected Boolean call() throws Exception {
updateMessage("Checking for updates...");
for (int i = 0; i < 1000; i++) {
updateProgress(i + 1, 1000);
Thread.sleep(1L);
}
return Math.random() < 0.5;
}
};
}
}
}
这是来自以下文档的注释ScheduledService
:
此类的时间并不是绝对可靠的。一个非常繁忙的事件线程可能会在后台Task的执行开始时引入一些时间延迟,因此周期或延迟的很小值可能不准确。几百毫秒或更长的延迟或周期应该是相当可靠的。
还有一个:
在ScheduledService
引入了一个名为新的属性lastValue
。的lastValue
是,在去年成功地计算出的值。因为a会在每次运行时Service
清除其value
属性,并且由于ScheduledService
会在完成后立即重新安排运行的时间(除非它进入已取消或失败状态),所以该value
属性在上的作用不是太有用ScheduledService
。在大多数情况下,您将需要使用由返回的值lastValue
。
最后一个注释意味着绑定到a的value
属性ScheduledService
很可能是无用的。尽管查询了value
属性,但上面的示例仍然有效,因为在onSucceeded
重新安排服务之前,在处理程序中查询了属性。
与用户界面无交互
如果定期后台任务不需要与UI交互,则可以改用Java的标准API。更具体地说,可以:
请注意,它ScheduledExecutorService
支持线程池,而Timer
后者仅支持单个线程。
ScheduledService不是一个选项
如果出于某种原因您不能使用ScheduledService
,但无论如何都需要与UI进行交互,则需要确保与UI交互的代码(仅该代码)在FX线程上执行。这可以通过使用完成Platform#runLater(Runnable)
。
将来在某些未指定的时间在JavaFX Application Thread上运行指定的Runnable。可以从任何线程调用此方法,该方法会将Runnable张贴到事件队列中,然后立即返回给调用者。Runnable按照其发布的顺序执行。传递给runLater方法的runnable将在传递给后续对runLater的调用的任何Runnable之前执行。如果在关闭JavaFX运行时之后调用此方法,则该调用将被忽略:Runnable将不会执行,并且不会引发任何异常。
注意:应用程序应避免将太多未决Runnable泛洪到JavaFX。否则,应用程序可能无法响应。鼓励应用程序将多个操作分批到更少的runLater调用中。另外,应在可能的情况下在后台线程上执行长时间运行的操作,从而释放JavaFX Application Thread进行GUI操作。
[...]
注意上述文档中的注释。该javafx.concurent.Task
班由合并更新其避免了这个message
,progress
和value
性能。当前,这是通过使用AtomicReference
战略性的获取和设置操作来实现的。如果有兴趣,您可以看一下实现(JavaFX是开源的)。
java.util.Timer
。我用示例更新了答案。