清除具有大量形状/多线程的组的最快方法
Fastest way to clear group with a lot of shapes / multithreading
在我的 JavaFX 项目中,我使用了很多形状(例如 1 000 000)来表示地理数据(例如地块轮廓、街道等)。它们存储在一个组中,有时我必须清除它们(例如,当我加载一个包含新地理数据的新文件时)。
问题:清除/删除它们需要很多时间。
所以我的想法是在一个单独的线程中删除形状,这显然因为 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()
:
组中移除不同数量的 children 所花费的时间
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
如您所见,required 的时间呈指数增长。
现在假设您必须清除 UI 中的 children 并且用户必须在应用程序冻结时等待 2.5 分钟。
此外,在这个简化的示例中它只是一条简单的线,在“真实”应用程序中它是一个更复杂的几何图形 -> 需要更多时间。
所以另一个想法是 'unbind' 小组来自它的 parent,窗格。
因为当它解除绑定时,我可以在另一个线程中将其删除。这意味着 1. ui 不会冻结并且 2. 它会更快。
这是尝试:
pane.getChildren().remove(group); // or clear()
// and then clear the group in another thread like above
问题:这个 'unbinding' 也需要很多时间。不是 2.5 分钟,而是像 0.5 分钟,这仍然太多了。
另一个想法是创建多个组,因为如您所见,具有 10 000 或 100 000 个元素的组被清除得更快。
这也失败了,因为几个组突然需要更长的时间并且被删除的速度呈指数级增长。
比如第一个20秒,第二个10秒,第三个5秒,等等
长话短说
是否有机会在单独的线程中或比使用 group.getChildren().clear()
更快地删除组的 children?我尝试了我想到的一切...
而且,如果我在删除时只能显示一个加载栏,那比只是冻结表面并等待 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));
}
}
执行时间长是因为 Parent
的每个 child 都使用 Parent
的 disabled
和 treeVisible
属性注册了一个侦听器]. JavaFX 目前的实现方式是,这些侦听器存储在一个数组中(即列表结构)。 添加 监听器的成本相对较低,因为新监听器只是简单地插入到数组的末尾,偶尔会调整数组的大小。但是,当您从其 Parent
中 删除 一个 child 并删除侦听器时,需要线性搜索数组以便找到并删除正确的侦听器.对于每个单独删除的 child,都会发生这种情况。
因此,当您清除 Group
的 children 列表时,您将触发对这两个属性的 1,000,000 次线性搜索,从而导致总共 2,000,000 次线性搜索。更糟糕的是,要删除的侦听器是——取决于删除 children 的顺序——总是在数组的末尾,在这种情况下,有 2,000,000 最坏的情况 线性搜索,或者总是在数组的开头,在这种情况下,有 2,000,000 次最佳情况线性搜索,但是每个单独的删除都会导致 所有剩余元素 必须是移动了一位。
至少有两个solutions/workarounds:
不显示 1,000,000 个节点。如果可以,请尝试只显示用户实际可以看到的数据的节点。例如,ListView
和 TableView
等虚拟化控件在任何给定时间仅显示大约 1-20 个单元格。
不要清除Group
的children。相反,只需将旧的 Group
替换为新的 Group
。如果需要,您可以在后台线程中准备新的 Group
。
这样做,在我的电脑上用了 3.5 秒创建了另一个 Group
1,000,000 children,然后用新的 Group
替换旧的 Group
].但是,由于需要同时渲染所有新节点,仍然存在一些延迟峰值。
如果您不需要填充新的 Group
,那么您甚至不需要线程。在那种情况下,交换在我的计算机上花费了大约 0.27 秒。
在我的 JavaFX 项目中,我使用了很多形状(例如 1 000 000)来表示地理数据(例如地块轮廓、街道等)。它们存储在一个组中,有时我必须清除它们(例如,当我加载一个包含新地理数据的新文件时)。 问题:清除/删除它们需要很多时间。 所以我的想法是在一个单独的线程中删除形状,这显然因为 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
如您所见,required 的时间呈指数增长。 现在假设您必须清除 UI 中的 children 并且用户必须在应用程序冻结时等待 2.5 分钟。 此外,在这个简化的示例中它只是一条简单的线,在“真实”应用程序中它是一个更复杂的几何图形 -> 需要更多时间。
所以另一个想法是 'unbind' 小组来自它的 parent,窗格。 因为当它解除绑定时,我可以在另一个线程中将其删除。这意味着 1. ui 不会冻结并且 2. 它会更快。 这是尝试:
pane.getChildren().remove(group); // or clear()
// and then clear the group in another thread like above
问题:这个 'unbinding' 也需要很多时间。不是 2.5 分钟,而是像 0.5 分钟,这仍然太多了。
另一个想法是创建多个组,因为如您所见,具有 10 000 或 100 000 个元素的组被清除得更快。 这也失败了,因为几个组突然需要更长的时间并且被删除的速度呈指数级增长。 比如第一个20秒,第二个10秒,第三个5秒,等等
长话短说
是否有机会在单独的线程中或比使用 group.getChildren().clear()
更快地删除组的 children?我尝试了我想到的一切...
而且,如果我在删除时只能显示一个加载栏,那比只是冻结表面并等待 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));
}
}
执行时间长是因为 Parent
的每个 child 都使用 Parent
的 disabled
和 treeVisible
属性注册了一个侦听器]. JavaFX 目前的实现方式是,这些侦听器存储在一个数组中(即列表结构)。 添加 监听器的成本相对较低,因为新监听器只是简单地插入到数组的末尾,偶尔会调整数组的大小。但是,当您从其 Parent
中 删除 一个 child 并删除侦听器时,需要线性搜索数组以便找到并删除正确的侦听器.对于每个单独删除的 child,都会发生这种情况。
因此,当您清除 Group
的 children 列表时,您将触发对这两个属性的 1,000,000 次线性搜索,从而导致总共 2,000,000 次线性搜索。更糟糕的是,要删除的侦听器是——取决于删除 children 的顺序——总是在数组的末尾,在这种情况下,有 2,000,000 最坏的情况 线性搜索,或者总是在数组的开头,在这种情况下,有 2,000,000 次最佳情况线性搜索,但是每个单独的删除都会导致 所有剩余元素 必须是移动了一位。
至少有两个solutions/workarounds:
不显示 1,000,000 个节点。如果可以,请尝试只显示用户实际可以看到的数据的节点。例如,
ListView
和TableView
等虚拟化控件在任何给定时间仅显示大约 1-20 个单元格。不要清除
Group
的children。相反,只需将旧的Group
替换为新的Group
。如果需要,您可以在后台线程中准备新的Group
。这样做,在我的电脑上用了 3.5 秒创建了另一个
Group
1,000,000 children,然后用新的Group
替换旧的Group
].但是,由于需要同时渲染所有新节点,仍然存在一些延迟峰值。如果您不需要填充新的
Group
,那么您甚至不需要线程。在那种情况下,交换在我的计算机上花费了大约 0.27 秒。