【问题标题】:JavaFX Optimized Asynchronous Lazy Loading of TreeItems on TreeViewJavaFX 优化了 TreeView 上 TreeItems 的异步延迟加载
【发布时间】:2018-09-17 02:53:15
【问题描述】:

我有一个应用程序,其中有一个 TreeView,其中 TreeItems 将拥有 大量 叶 TreeItems。在树视图中拥有大量 TreeItem 会明显损害应用程序的性能,为避免这种情况,我将做的是,我一次只允许展开一个非叶 TreeItem,并且一旦折叠 TreeItem,我会清除它的子元素,并在需要时异步加载它们(当用户展开 TreeItem 时)。

奇怪的问题是,在下面的这个测试中,当我第一次单击树项上的展开箭头时,子项加载正常,如果我折叠它(这将清除子项)并再次展开它,有时它会起作用,其他program hogs and starts consuming 30% of the cpu for a couple of minutes 然后重新运行。 更奇怪的是如果我双击 TreeItem 来展开它(不使用箭头),即使在第一次程序启动时,猪也会立即启动。

我在这里可能做错了什么?

PS:

  • LazyTreeItem 类中的一些代码灵感来自 James_D 的回答 Here

  • 我尝试在 fx 线程上运行 loadItems 任务(不使用 ItemLoader),但没有任何区别。

  • 同时使用 JAVA 8JAVA 9

  • 会出现同样的问题

App.java

public class App extends Application {

    private TreeView<Item> treeView = new TreeView<>();

    @Override
    public void start(Stage primaryStage) throws Exception {
        primaryStage.setTitle("TreeView Lazy Load");
        primaryStage.setScene(new Scene(new StackPane(treeView), 300, 275));
        initTreeView();
        primaryStage.show();
    }

    private void initTreeView() {
        treeView.setShowRoot(false);
        treeView.setRoot(new TreeItem<>(null));

        List<SingleItem> items = new ArrayList<>();
        for (int i = 0; i < 100000; i++) {
            items.add(new SingleItem(String.valueOf(i)));
        }
        TreeItem<Item> parentItem = new TreeItem<>(new Item());
        parentItem.getChildren().add(new LazyTreeItem(new MultipleItem(items)));

        treeView.getRoot().getChildren().add(parentItem);
    }

    public static void main(String[] args) {
        launch(args);
    }
}

LazyTreeItem.java

public class LazyTreeItem extends TreeItem<Item> {
    private boolean childrenLoaded = false;
    private boolean isLoadingItems = false;

    public LazyTreeItem(Item value) {
        super(value);
        // Unload data on folding to reduce memory
        expandedProperty().addListener((observable, oldValue, newValue) -> {
            if (!newValue) {
                flush();
            }
        });
    }

    @Override
    public ObservableList<TreeItem<Item>> getChildren() {
        if (childrenLoaded || !isExpanded()) {
            return super.getChildren();
        }
        if (super.getChildren().size() == 0) {
            // Filler node (will translate into loading icon in the
            // TreeCell factory)
            super.getChildren().add(new TreeItem<>(null));
        }
        if (getValue() instanceof MultipleItem) {
            if (!isLoadingItems) {
                loadItems();
            }
        }
        return super.getChildren();
    }

    public void loadItems() {
        Task<List<TreeItem<Item>>> task = new Task<List<TreeItem<Item>>>() {
            @Override
            protected List<TreeItem<Item>> call() {
                isLoadingItems = true;
                List<SingleItem> downloadSet = ((MultipleItem) LazyTreeItem.this.getValue()).getEntries();
                List<TreeItem<Item>> treeNodes = new ArrayList<>();
                for (SingleItem download : downloadSet) {
                    treeNodes.add(new TreeItem<>(download));
                }
                return treeNodes;
            }
        };
        task.setOnSucceeded(e -> {
            Platform.runLater(() -> {
                super.getChildren().clear();
                super.getChildren().addAll(task.getValue());
                childrenLoaded = true;
                isLoadingItems = false;
            });
        });
        ItemLoader.getSingleton().load(task);
    }

    private void flush() {
        childrenLoaded = false;
        super.getChildren().clear();
    }

    @Override
    public boolean isLeaf() {
        if (childrenLoaded) {
            return getChildren().isEmpty();
        }
        return false;
    }
}

ItemLoader.java

public class ItemLoader implements Runnable {
    private static ItemLoader instance;
    private List<Task> queue = new ArrayList<>();
    private Task prevTask = null;

    private ItemLoader() {
        Thread runner = new Thread(this);
        runner.setName("ItemLoader thread");
        runner.setDaemon(true);
        runner.start();
    }

    public static ItemLoader getSingleton() {
        if (instance == null) {
            instance = new ItemLoader();
        }
        return instance;
    }

    public <T> void load(Task task) {
        if (queue.size() < 1) {
            queue.add(task);
        }
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if (!queue.isEmpty()) {
                Task task = queue.get(0);
                if (task != prevTask) {
                    prevTask = task;
                    task.run();
                    queue.remove(task);
                }
            }
        }
    }
}

模型(Item.javaSingleItem.javaMultipleItem.java

public class Item {

}
/****************************************************************
 **********                  SingleItem              ************
 ****************************************************************/
public class SingleItem extends Item {
    private String id;

    public SingleItem(String id) {
        this.id = id;
    }

    public void setId(String id) {
        this.id = id;
    }
}
/****************************************************************
 **********                  MultipleItem            ************
 ****************************************************************/
public class MultipleItem extends Item {

    private List<SingleItem> entries = new ArrayList<>();

    public MultipleItem(List<SingleItem> entries) {
        this.entries = entries;
    }

    public List<SingleItem> getEntries() {
        return entries;
    }

    public void setEntries(List<SingleItem> entries) {
        this.entries = entries;
    }
}

【问题讨论】:

  • 我可以在 fx 10 中重现该问题(双击该项目时) - 不是在三角形上展开时。不知道会发生什么,但是:leaf/leafProperty 没有同步(由于 api 错误,正如 James 指出的那样),所以一些内部组件 - 无论如何都很脏 - 可能会遇到更多麻烦。在你的鞋子里,我会进入一个分析轮:用分析器找到瓶颈,然后从那里开始。
  • okay .. 似乎与选择有关:如果未选择扩展项,则一切正常 - 如果选择它,它会挂起...
  • 它挂在 TreeViewFocusModel 中,这在检查每个添加项目的行时有问题(与查询最后添加到块中的行相比,就像 TreeViewSelectionModel 那样) - 不知道如何破解,实现自定义 FocusModel 可能是唯一的出路(讨厌 .. 一切都非常隐藏且难以扩展)
  • 绝对是一个错误(将很快提交),并且绝对(模缺失测试;)可以被自定义 FocusModel 破解,该模型仅在focusedIndex> sourceRow(c&p默认值-通过调整 treeItem 和 treeView 的内部状态是愚蠢的......这反过来又需要脏反射访问)然后比默认的 impl 做得更好,没有理由查询每个添加的项目的行,这样做是为了添加的最后一堆应该没问题
  • @kleopatra 如果/当您提交错误时,您应该包含TreeTableViewFocusModel,因为它的作用几乎相同。

标签: java javafx javafx-8


【解决方案1】:

正如@kleopatra 所指出的,这个问题是由于在选择一个或多个项目时添加了大量 数量的子项引起的。解决此问题的一种方法是尝试实现自己的FocusModel,因为默认的FocusModel 似乎是问题的根源。另一种在我看来更简单的解决方法是在添加一大群孩子之前清除选择;之后,您可以重新选择之前选择的项目。

我这样做的方法是使用自定义EventTypes 触发TreeModificationEvents。另外,我决定不在我懒惰的TreeItem 中覆盖isLeaf()。当父 TreeItem 是一个惰性分支时,我发现使用占位符 TreeItem 更容易。由于有一个占位符,父级将自动注册为分支。

这是一个浏览默认FileSystem 的示例。为了测试解决方案是否有效,我创建了一个包含 100,000 个文件的目录并打开它;我没有悬念。希望这意味着这可以适应您的代码。

注意:当分支折叠时,此示例确实会删除子项,就像您在代码中所做的那样。


App.java

import java.nio.file.FileSystems;
import java.nio.file.Path;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.stage.Stage;

public class App extends Application {

  private static String pathToString(Path p) {
    if (p == null) {
      return "null";
    } else if (p.getFileName() == null) {
      return p.toString();
    }
    return p.getFileName().toString();
  }

  @Override
  public void start(Stage primaryStage) {
    TreeView<Path> tree = new TreeView<>(new TreeItem<>());
    tree.setShowRoot(false);
    tree.setCellFactory(LazyTreeCell.forTreeView("Loading...", App::pathToString));
    TreeViewUtils.installSelectionBugWorkaround(tree);

    for (Path fsRoot : FileSystems.getDefault().getRootDirectories()) {
      tree.getRoot().getChildren().add(new LoadingTreeItem<>(fsRoot, new DirectoryLoader(fsRoot)));
    }

    primaryStage.setScene(new Scene(tree, 800, 600));
    primaryStage.show();
  }

}

DirectoryLoader.java

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;
import javafx.scene.control.TreeItem;

public class DirectoryLoader implements Callable<List<? extends TreeItem<Path>>> {

  private static final Comparator<Path> COMPARATOR = (left, right) -> {
    boolean leftIsDir = Files.isDirectory(left);
    if (leftIsDir ^ Files.isDirectory(right)) {
      return leftIsDir ? -1 : 1;
    }
    return left.compareTo(right);
  };

  private final Path directory;

  public DirectoryLoader(Path directory) {
    this.directory = directory;
  }

  @Override
  public List<? extends TreeItem<Path>> call() throws Exception {
    try (Stream<Path> stream = Files.list(directory)) {
      return stream.sorted(COMPARATOR)
          .map(this::toTreeItem)
          .collect(Collectors.toList());
    }
  }

  private TreeItem<Path> toTreeItem(Path path) {
    return Files.isDirectory(path)
           ? new LoadingTreeItem<>(path, new DirectoryLoader(path))
           : new TreeItem<>(path);
  }

}

LoadingTreeItem.java

import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.function.Supplier;
import javafx.application.Platform;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.event.EventType;
import javafx.scene.control.TreeItem;

public class LoadingTreeItem<T> extends TreeItem<T> {

  private static final EventType<?> PRE_ADD_LOADED_CHILDREN
      = new EventType<>(treeNotificationEvent(), "PRE_ADD_LOADED_CHILDREN");
  private static final EventType<?> POST_ADD_LOADED_CHILDREN
      = new EventType<>(treeNotificationEvent(), "POST_ADD_LOADED_CHILDREN");

  @SuppressWarnings("unchecked")
  static <T> EventType<TreeModificationEvent<T>> preAddLoadedChildrenEvent() {
    return (EventType<TreeModificationEvent<T>>) PRE_ADD_LOADED_CHILDREN;
  }

  @SuppressWarnings("unchecked")
  static <T> EventType<TreeModificationEvent<T>> postAddLoadedChildrenEvent() {
    return (EventType<TreeModificationEvent<T>>) POST_ADD_LOADED_CHILDREN;
  }

  private final Callable<List<? extends TreeItem<T>>> callable;
  private boolean needToLoadData = true;

  private CompletableFuture<?> future;

  public LoadingTreeItem(T value, Callable<List<? extends TreeItem<T>>> callable) {
    super(value);
    this.callable = callable;
    super.getChildren().add(new TreeItem<>());
    addExpandedListener();
  }

  @SuppressWarnings("unchecked")
  private void addExpandedListener() {
    expandedProperty().addListener((observable, oldValue, newValue) -> {
      if (!newValue) {
        needToLoadData = true;
        if (future != null) {
          future.cancel(true);
        }
        super.getChildren().setAll(new TreeItem<>());
      }
    });
  }

  @Override
  public ObservableList<TreeItem<T>> getChildren() {
    if (needToLoadData) {
      needToLoadData = false;
      future = CompletableFuture.supplyAsync(new CallableToSupplierAdapter<>(callable))
          .whenCompleteAsync(this::handleAsyncLoadComplete, Platform::runLater);
    }
    return super.getChildren();
  }

  private void handleAsyncLoadComplete(List<? extends TreeItem<T>> result, Throwable th) {
    if (th != null) {
      Thread.currentThread().getUncaughtExceptionHandler()
          .uncaughtException(Thread.currentThread(), th);
    } else {
      Event.fireEvent(this, new TreeModificationEvent<>(preAddLoadedChildrenEvent(), this));
      super.getChildren().setAll(result);
      Event.fireEvent(this, new TreeModificationEvent<>(postAddLoadedChildrenEvent(), this));
    }
    future = null;
  }

  private static class CallableToSupplierAdapter<T> implements Supplier<T> {

    private final Callable<T> callable;

    private CallableToSupplierAdapter(Callable<T> callable) {
      this.callable = callable;
    }

    @Override
    public T get() {
      try {
        return callable.call();
      } catch (Exception ex) {
        throw new CompletionException(ex);
      }
    }

  }

}

LazyTreeCell.java

import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeView;
import javafx.util.Callback;

public class LazyTreeCell<T> extends TreeCell<T> {

  public static <T> Callback<TreeView<T>, TreeCell<T>> forTreeView(String placeholderText,
                                                                   Callback<? super T, String> toStringCallback) {
    return tree -> new LazyTreeCell<>(placeholderText, toStringCallback);
  }

  private final String placeholderText;
  private final Callback<? super T, String> toStringCallback;

  public LazyTreeCell(String placeholderText, Callback<? super T, String> toStringCallback) {
    this.placeholderText = placeholderText;
    this.toStringCallback = toStringCallback;
  }

  /*
   * Assumes that if "item" is null **and** the parent TreeItem is an instance of
   * LoadingTreeItem that this is a "placeholder" cell.
   */
  @Override
  protected void updateItem(T item, boolean empty) {
    super.updateItem(item, empty);
    if (empty) {
      setText(null);
      setGraphic(null);
    } else if (item == null && getTreeItem().getParent() instanceof LoadingTreeItem) {
      setText(placeholderText);
    } else {
      setText(toStringCallback.call(item));
    }
  }

}

TreeViewUtils.java

import java.util.ArrayList;
import java.util.List;
import javafx.beans.value.ChangeListener;
import javafx.event.EventHandler;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeItem.TreeModificationEvent;
import javafx.scene.control.TreeView;

public class TreeViewUtils {

  public static <T> void installSelectionBugWorkaround(TreeView<T> tree) {
    List<TreeItem<T>> selected = new ArrayList<>(0);
    EventHandler<TreeModificationEvent<T>> preAdd = event -> {
      event.consume();
      selected.addAll(tree.getSelectionModel().getSelectedItems());
      tree.getSelectionModel().clearSelection();
    };
    EventHandler<TreeModificationEvent<T>> postAdd = event -> {
      event.consume();
      selected.forEach(tree.getSelectionModel()::select);
      selected.clear();
    };
    ChangeListener<TreeItem<T>> rootListener = (observable, oldValue, newValue) -> {
      if (oldValue != null) {
        oldValue.removeEventHandler(LoadingTreeItem.preAddLoadedChildrenEvent(), preAdd);
        oldValue.removeEventHandler(LoadingTreeItem.postAddLoadedChildrenEvent(), postAdd);
      }
      if (newValue != null) {
        newValue.addEventHandler(LoadingTreeItem.preAddLoadedChildrenEvent(), preAdd);
        newValue.addEventHandler(LoadingTreeItem.postAddLoadedChildrenEvent(), postAdd);
      }
    };
    rootListener.changed(tree.rootProperty(), null, tree.getRoot());
    tree.rootProperty().addListener(rootListener);
  }

  private TreeViewUtils() {}
}

在实施时,安装解决方法的实用程序方法与您使用 TreeView 中的 LoadingTreeItems 相关联。我想不出一个好方法来使解决方案足够通用以适用于任意TreeView;为此,我相信创建自定义 FocusModel 是必要的。

可能有更好的方法来实现LazyTreeCell,方法是使用一个类来包装真实数据——就像你对Item 所做的那样。然后你可以有一个实际的占位符 Item 实例,它告诉 TreeCell 它是一个占位符,而不是依赖于父 TreeItem 的类型。事实上,我的实现可能很脆弱。

【讨论】:

  • 感谢您抽出宝贵时间撰写此答案。抱歉,我忘了提到只有在需要加载大量树项时才会发生挂起,在我的情况下为 20,000 及以上。我删除了整个 isLeaf 方法,但它没有解决它。当一个目录有 100,000 个文件时,您的解决方案是否有效?
  • 我现在就尝试做测试。
  • 我在目录加载器 int count = 0; while (++count &lt; 100000) { list.add(TreeData.createLeaf(fo)); } 的 else 语句中添加了这个,并扩展了一个只有一个叶子文件的驱动器,并且发生了同样的挂起,@Kleopatra 在上面的回复中发现它与被选择/聚焦的项目,在您的示例应用程序中,第一个树单元始终如蓝色边框所示聚焦,因此一旦树项目展开,就会立即产生挂起。
  • arrgghh ...将单元格添加到混合中是..邪恶的...无论如何,问题与延迟加载无关,只要添加了许多孩子就会发生
  • @DarkMental 没有真正的优势。使用Task 可以被认为是“更多JavaFX 方式”。我使用CompletableFuture 的原因是(1)对于一个小例子来说似乎更简单,(2)我没有显示视觉进度。不,调用cancel 不会立即停止执行。事实上,CompletableFuture.cancel 的行为与“正常”Future 不同;它不会中断Thread。即使这样做了,我的代码也不会检查中断,因此它不会停止执行。
猜你喜欢
  • 1970-01-01
  • 2012-05-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-22
  • 2013-08-01
  • 1970-01-01
  • 2014-10-31
相关资源
最近更新 更多