我有一个应用程序,其中有一个 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;
}
}
答案 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
的类型。实际上,我的实现可能很脆弱。