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

时间:2018-09-17 02:53:15

标签: java javafx javafx-8

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

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.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;
    }
}

1 个答案:

答案 0 :(得分:3)

@kleopatra指出,此问题是由于在选择一个或多个项目时添加了个孩子而引起的。解决此问题的一种方法是尝试实施自己的FocusModel,因为默认的FocusModel似乎是问题的根源。在我看来,另一种方法(更容易实现)是一种变通方法,即在添加大量子项之前先清除选择。之后,您可以重新选择先前选择的项目。

我这样做的方法是通过自定义TreeModificationEvent触发EventType。另外,我决定不覆盖懒惰的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();
  }

}

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() {}
}

根据实现,使用LoadingTreeItem中的TreeView来与安装解决方法的实用程序方法绑定。我想不出一种使解决方案具有足够通用性以适用于任意TreeView的好方法;为此,我认为创建自定义FocusModel是必要的。

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