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

yv5phkfx  于 2023-09-29  发布在  Java
关注(0)|答案(1)|浏览(87)

在我的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));
    }
}
ffscu2ro

ffscu2ro1#

执行时间长是因为Parent的每个子进程都用ParentdisabledtreeVisible属性注册了一个侦听器。按照JavaFX当前实现的方式,这些侦听器存储在一个数组中(即列表结构)。* 添加 * 监听器的成本相对较低,因为新的监听器只是插入到数组的末尾,偶尔会调整数组的大小。但是,当您从Parent中删除一个子元素,并且删除了监听器时,需要线性搜索数组,以便找到并删除正确的监听器。每个被移除的孩子都会单独发生这种情况。
因此,当您清除Group的子列表时,您将触发对这两个属性的1,000,000次线性搜索,从而导致总共2,000,000次线性搜索。更糟糕的是,要删除的监听器要么--取决于删除孩子的顺序--总是在数组的末尾,在这种情况下,有2,000,000个 * 最坏情况 * 线性搜索,要么总是在数组的开头,在这种情况下,有2,000,000个最好情况线性搜索,但是其中每个单独的移除导致 * 所有剩余的元素 * 必须移位一。
至少有两种解决方案/变通方法:
1.不显示1,000,000个节点。如果可以,请尝试只显示用户实际可以看到的数据的节点。例如,虚拟化控件(如ListViewTableView)在任何给定时间仅显示约1-100个单元格(取决于各种因素,包括窗口大小、屏幕大小等)。
1.不要清除Group的子项。只需将旧的Group替换为新的Group即可。如果需要,可以在后台线程中准备新的Group
这样做,在我的计算机上花了3.5秒的时间创建另一个具有1,000,000个子节点的Group,然后用新的Group替换旧的Group。但是,由于需要同时渲染所有新节点,因此仍然存在一点滞后峰值。
如果你不需要填充新的Group,那么你甚至不需要线程。在这种情况下,交换在我的电脑上花了大约0.27秒。

相关问题