清除具有大量形状/多线程的组的最快方法

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 都使用 ParentdisabledtreeVisible 属性注册了一个侦听器]. JavaFX 目前的实现方式是,这些侦听器存储在一个数组中(即列表结构)。 添加 监听器的成本相对较低,因为新监听器只是简单地插入到数组的末尾,偶尔会调整数组的大小。但是,当您从其 Parent 删除 一个 child 并删除侦听器时,需要线性搜索数组以便找到并删除正确的侦听器.对于每个单独删除的 child,都会发生这种情况。

因此,当您清除 Group 的 children 列表时,您将触发对这两个属性的 1,000,000 次线性搜索,从而导致总共 2,000,000 次线性搜索。更糟糕的是,要删除的侦听器是——取决于删除 children 的顺序——总是在数组的末尾,在这种情况下,有 2,000,000 最坏的情况 线性搜索,或者总是在数组的开头,在这种情况下,有 2,000,000 次最佳情况线性搜索,但是每个单独的删除都会导致 所有剩余元素 必须是移动了一位。

至少有两个solutions/workarounds:

  1. 不显示 1,000,000 个节点。如果可以,请尝试只显示用户实际可以看到的数据的节点。例如,ListViewTableView 等虚拟化控件在任何给定时间仅显示大约 1-20 个单元格。

  2. 不要清除Group的children。相反,只需将旧的 Group 替换为新的 Group。如果需要,您可以在后台线程中准备新的 Group

    这样做,在我的电脑上用了 3.5 秒创建了另一个 Group 1,000,000 children,然后用新的 Group 替换旧的 Group ].但是,由于需要同时渲染所有新节点,仍然存在一些延迟峰值。

    如果您不需要填充新的 Group,那么您甚至不需要线程。在那种情况下,交换在我的计算机上花费了大约 0.27 秒。