在我的JavaFX项目中,我使用了大量的形状(例如1000000)来表示地理数据(例如地块轮廓、街道等)。它们存储在一个组中,有时我必须清除它们(例如,当我加载一个新的地理数据文件时)。问题是:清除/移除它们需要很多时间。所以我的想法是在一个单独的线程中删除这些形状,这显然是行不通的,因为JavaFX是单线程的。
下面是一个我正在尝试做的简化代码:
HelloApplication.java
package com.example.javafxmultithreading;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
import java.io.IOException;
public class HelloApplication extends Application {
public static Group group = new Group();
@Override
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
Scene scene = new Scene(fxmlLoader.load());
stage.setTitle("Hello!");
stage.setScene(scene);
stage.show();
for (int i = 0; i < 1000000; i++) {
group.getChildren().add(new Line(100, 200, 200, 300));
}
HelloController.helloController = fxmlLoader.getController();
HelloController.helloController.pane.getChildren().addAll(group);
}
public static void main(String[] args) {
launch();
}
}
HelloController.java
public class HelloController {
public static HelloController helloController;
@FXML
public Pane pane;
public VBox vbox;
@FXML
public void onClearShapes() throws InterruptedException {
double start = System.currentTimeMillis();
HelloApplication.group.getChildren().clear();
System.out.println(System.currentTimeMillis() - start);
Service<Boolean> service = new Service<>() {
@Override
protected Task<Boolean> createTask() {
return new Task<>() {
@Override
protected Boolean call() {
// Try to clear the children of the group in this thread
return true;
}
};
}
};
service.setOnSucceeded(event -> {
System.out.println("Success");
});
service.start();
}
}
hello-view.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<VBox fx:id="vbox" alignment="CENTER" prefHeight="465.0" prefWidth="711.0" spacing="20.0"
xmlns="http://javafx.com/javafx/11.0.2" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.example.javafxmultithreading.HelloController">
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/>
</padding>
<Pane fx:id="pane" prefHeight="200.0" prefWidth="200.0"/>
<Button mnemonicParsing="false" onAction="#onClearShapes" text="Clear shapes"/>
</VBox>
我用group.getChildren().clear()
测量了从组中删除不同数量的孩子所花费的时间:
amount of children | time
100 2ms = 0,002s
1 000 4ms = 0,004s
10 000 38ms = 0,038s
100 000 1273ms = 1,2s
1 000 000 149896ms = 149,896s = ~2,5min
正如你所看到的,所需的时间呈指数级增长。现在想象一下,你必须清除UI中的子项,用户必须在应用程序冻结时等待2.5分钟。此外,在这个简化的例子中,它只是一条简单的线,在“真实的”的应用程序中,它是一个更复杂的几何形状->需要更多的时间。
因此,另一个想法是将组与其父窗格“解除绑定”。因为当它解绑时,我可以在另一个线程中删除它。这意味着1。UI不冻结,以及2.会更快。这是尝试:
pane.getChildren().remove(group); // or clear()
// and then clear the group in another thread like above
问题是:这种“解绑”也花费大量时间。不是2,5分钟,而是像0,5分钟,这仍然是太多了。
另一个想法是创建多个组,因为正如您所看到的,具有10 000或100 000个元素的组被清除得更快。这也失败了,因为有几个组突然需要更长的时间,并且删除的速度呈指数级增长。例如,第一个需要20秒,第二个需要10秒,第三个需要5秒,等等。
长话短说
有没有可能在单独的线程中删除组的子线程,或者比group.getChildren().clear()
更快?我试过了所有我能想到的方法...
而如果我在删除的时候只能显示一个加载栏,那就比只是冻结表面等待2分钟要好...
我很感激每一个想法/帮助。
编辑,见注解没有FXML的简单示例:
import javafx.scene.Group;
import javafx.scene.shape.Line;
public class Test {
public static void main(String[] args) {
Group group = new Group();
System.out.println("adding lines");
for (int i = 0; i < 1000000; i++) {
group.getChildren().add(new Line(100, 200, 200, 300));
}
System.out.println("adding done");
System.out.println("removing starts");
double start = System.currentTimeMillis();
group.getChildren().clear();
System.out.println("removing done, needed time: " + (System.currentTimeMillis() - start));
}
}
1条答案
按热度按时间ffscu2ro1#
执行时间长是因为
Parent
的每个子进程都用Parent
的disabled
和treeVisible
属性注册了一个侦听器。按照JavaFX当前实现的方式,这些侦听器存储在一个数组中(即列表结构)。* 添加 * 监听器的成本相对较低,因为新的监听器只是插入到数组的末尾,偶尔会调整数组的大小。但是,当您从Parent
中删除一个子元素,并且删除了监听器时,需要线性搜索数组,以便找到并删除正确的监听器。每个被移除的孩子都会单独发生这种情况。因此,当您清除
Group
的子列表时,您将触发对这两个属性的1,000,000次线性搜索,从而导致总共2,000,000次线性搜索。更糟糕的是,要删除的监听器要么--取决于删除孩子的顺序--总是在数组的末尾,在这种情况下,有2,000,000个 * 最坏情况 * 线性搜索,要么总是在数组的开头,在这种情况下,有2,000,000个最好情况线性搜索,但是其中每个单独的移除导致 * 所有剩余的元素 * 必须移位一。至少有两种解决方案/变通方法:
1.不显示1,000,000个节点。如果可以,请尝试只显示用户实际可以看到的数据的节点。例如,虚拟化控件(如
ListView
和TableView
)在任何给定时间仅显示约1-100个单元格(取决于各种因素,包括窗口大小、屏幕大小等)。1.不要清除
Group
的子项。只需将旧的Group
替换为新的Group
即可。如果需要,可以在后台线程中准备新的Group
。这样做,在我的计算机上花了3.5秒的时间创建另一个具有1,000,000个子节点的
Group
,然后用新的Group
替换旧的Group
。但是,由于需要同时渲染所有新节点,因此仍然存在一点滞后峰值。如果你不需要填充新的
Group
,那么你甚至不需要线程。在这种情况下,交换在我的电脑上花了大约0.27秒。