0

我正在尝试实现我自己的 VirtualFlow,但我遇到了一些性能问题,我不明白瓶颈可能在哪里以及我可以做些什么来改进它。
这是一个带有两个列表的展示,一个带有简单的单元格,一个带有复选框,两者都有 100_000 个项目(抱歉 gif 质量):

在此处输入图像描述

正如你所看到的,我在简单的单元格中滚动只有一个标签没有问题,但是带有复选框的列表在开始时可能会非常滞后。

为了达到这样的性能(否则会更糟)我记住了我的细胞工厂功能,以便所有细胞在需要时都准备好在内存中,我想知道是否还有其他东西可以让它更快/更顺畅。

我用来显示单元格的代码是:

long start = System.currentTimeMillis();
builtNodes.clear();
for (int i = from; i <= to; i++) {
    C cell = cellForIndex.apply(i);
    Node node = cell.getNode();
    builtNodes.add(node);
}
manager.virtualFlow.container.getChildren().setAll(builtNodes);
long elapsed = System.currentTimeMillis() - start;
System.out.println("Show elapsed: " + elapsed);

我什至添加了这个小日志,最重的部分似乎是当我调用时getChildren().setAll(...),单元格构建和布局几乎都是立即的

from哦,关于andto参数的另一个注释。当我滚动时,我获取滚动条的值并计算第一个和最后一个可见索引,如下所示:

public int firstVisible() {
    return (int) Math.floor(scrolled / cellHeight);
}

public int lastVisible() {
    return (int) Math.ceil((scrolled + virtualFlow.getHeight()) / cellHeight - 1);
}

编辑以回答一些问题:

为什么要实现我自己的 VirtualFlow?

最终目标是制作我自己的 ListView 实现,为此我还需要一个 VirtualFlow,而且我知道这是一项相当低级和艰巨的任务,但我认为这将是了解更多关于编程的好方法一般以及关于JavaFX

为什么要记忆?

通过将单元工厂结果缓存在内存中,它使后续滚动更快,因为节点已经构建,它们只需要布局,这相当容易。问题出在开始,因为由于某种原因,VirtualFlow 滞后于更复杂的单元,我不明白为什么因为流行的库 Flowless 也使用 memoization 并将节点缓存在内存中,但它从一开始就非常流畅. JavaFX 的 VirtualFlow(从 JFX16 修订)在没有记忆的情况下也非常高效,但理解起来要复杂得多

4

1 回答 1

3

我终于找到了一个既快速又高效的解决方案。
不是每次视图滚动并更新超级昂贵的子列表时都构建新单元格,而是构建所需数量的单元格来填充视口。在滚动时,单元格被布置和更新。更新单元格而不是创建和更改子列表要快得多。

因此,新的实现与 Android 的 RecyclerView 非常相似,也可能与 JavaFX 的 VirtualFlow 非常相似,唯一的区别是 Cell 是愚蠢的,引用 Flowless:

这是最重要的区别。对于 Flowless,cell 只是 Node,不封装任何关于虚拟流的逻辑。一个单元格甚至不一定存储它正在显示的项目的索引。这允许 VirtualFlow 完全控制何时创建和/或更新单元。

每个人都可以扩展接口并定义自己的逻辑。
显示一点代码:

细胞界面:

public interface Cell<T> {

    /**
     * Returns the cell's node.
     * The ideal way to implement a cell would be to extend a JavaFX's pane/region
     * and override this method to return "this".
     */
    Node getNode();

    /**
     * Automatically called by the VirtualFlow
     * <p>
     * This method must be implemented to correctly
     * update the Cell's content on scroll.
     * <p>
     * <b>Note:</b> if the Cell's content is a Node this method should
     * also re-set the Cell's children because (quoting from JavaFX doc)
     * `A node may occur at most once anywhere in the scene graph` and it's
     * possible that a Node may be removed from a Cell to be the content
     * of another Cell.
     */
    void updateItem(T item);

    /**
     * Automatically called by the VirtualFlow.
     * <p>
     * Cells are dumb, they have no logic, no state.
     * This method allow cells implementations to keep track of a cell's index.
     * <p>
     * Default implementation is empty.
     */
    default void updateIndex(int index) {}

    /**
     * Automatically called after the cell has been laid out.
     * <p>
     * Default implementation is empty.
     */
    default void afterLayout() {}

    /**
     * Automatically called before the cell is laid out.
     * <p>
     * Default implementation is empty.
     */
    default void beforeLayout() {}
}

CellsManager 类,负责显示和更新单元格:

    /*
     * Initialization, creates num cells
     */
    protected void initCells(int num) {
        int diff = num - cellsPool.size();
        for (int i = 0; i <= diff; i++) {
            cellsPool.add(cellForIndex(i));
        }

        // Add the cells to the container
        container.getChildren().setAll(cellsPool.stream().map(C::getNode).collect(Collectors.toList()));

        // Ensure that cells are properly updated
        updateCells(0, num);
    }

    /*
     * Update the cells in the given range.
     * CellUpdate is a simple bean that contains the cell, the item and the item's index.
     * The update() method calls updateIndex() and updateItem() of the Cell's interface
     */
    protected void updateCells(int start, int end) {
        // If the items list is empty return immediately
        if (virtualFlow.getItems().isEmpty()) return;

        // If the list was cleared (so start and end are invalid) cells must be rebuilt
        // by calling initCells(numOfCells), then return since that method will re-call this one
        // with valid indexes.
        if (start == -1 || end == -1) {
            int num = container.getLayoutManager().lastVisible();
            initCells(num);
            return;
        }

        // If range not changed or update is not triggered by a change in the list, return
        NumberRange<Integer> newRange = NumberRange.of(start, end);
        if (lastRange.equals(newRange) && !listChanged) return;

        // If there are not enough cells build them and add to the container
        Set<Integer> itemsIndexes = NumberRange.expandRangeToSet(newRange);
        if (itemsIndexes.size() > cellsPool.size()) {
            supplyCells(cellsPool.size(), itemsIndexes.size());
        } else if (itemsIndexes.size() < cellsPool.size()) {
            int overFlow = cellsPool.size() - itemsIndexes.size();
            for (int i = 0; i < overFlow; i++) {
                cellsPool.remove(0);
                container.getChildren().remove(0);
            }
        }

        // Items index can go from 0 to size() of items list,
        // cells can go from 0 to size() of the cells pool,
        // Use a counter to get the cells and itemIndex to
        // get the correct item and index to call
        // updateIndex() and updateItem() later
        updates.clear();
        int poolIndex = 0;
        for (Integer itemIndex : itemsIndexes) {
            T item = virtualFlow.getItems().get(itemIndex);
            CellUpdate update = new CellUpdate(item, cellsPool.get(poolIndex), itemIndex);
            updates.add(update);
            poolIndex++;
        }

        // Finally, update the cells, the layout and the range of items processed
        updates.forEach(CellUpdate::update);
        processLayout(updates);
        lastRange = newRange;
    }

也许它仍然不完美,但它有效,我仍然需要对它进行适当的测试,以应对单元格过多、单元格不足、容器大小发生变化等情况,因此必须重新计算单元格的数量......

现在的表现:

表现

于 2021-09-15T14:34:00.610 回答