JavaFX在TreeView上优化了TreeItem的异步延迟加载

问题描述 投票:5回答:1

我有一个应用程序,我有一个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 8和JAVA 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);
    }
}

lazy tree item.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;
    }
}

item loader.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);
                }
            }
        }
    }
}

Model(Item.java,SingleItem.java,MultipleItem.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;
    }
}
java javafx javafx-8
1个回答
4
投票

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

我这样做的方法是用自定义TreeModificationEvents发射EventTypes。另外,我决定不在我懒惰的isLeaf()中覆盖TreeItem。当父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();
  }

}

directory loader.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);
  }

}

loading tree item.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);
      }
    }

  }

}

lazy tree cell.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));
    }
  }

}

TreeView U替LS.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() {}
}

实现时,安装变通方法的实用方法与LoadingTreeItem中的TreeViews绑定。我想不出一个好的方法来使解决方案足够普遍适用于任何任意的TreeView;为此,我相信创建一个自定义FocusModel将是必要的。

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

© www.soinside.com 2019 - 2024. All rights reserved.