更新TableView行的颜色会占用太多CPU

时间:2014-11-03 11:12:19

标签: javafx-8

我正在制作一个接收警报的应用程序。

警报可以有4种可能的状态:

  • Unresolved_New_0
  • Unresolved_New_1
  • Unresolved_Old
  • 解决

收到警报时,它处于Unresolved_New_0状态。持续10秒,每0.5秒,状态从Unresolved_New_0变为Unresolved_New_1,反之亦然。根据状态I,为表格行设置不同的背景颜色(使其闪烁10秒)。 当10s通过时,警报转换为Unresolved_Old状态。这会导致其颜色停止变化。

为了实现这一点,我有一个ScheduledThreadPoolExecutor,用于提交Runnable的实现,有时使用Platform.runLater执行runnable。

static class FxTask extends Runnable {

    /**
     * 
     * @param runnableDuring Runnable to be run while the task is active (run on the JavaFX application thread).
     * @param runnableAfter Runnable to be run after the given duration is elapsed (run on the JavaFX application thread).
     * @param duration Duration to run this task for.
     * @param unit Time unit.
     */
    public static FxTask create(final Runnable runnableDuring, final Runnable runnableAfter, final long duration, final TimeUnit unit) {

        return new FxTask(runnableDuring, runnableAfter, duration, unit);
    }

    @Override
    public void run() {

        if (System.nanoTime() - mTimeStarted >= mTimeUnit.toNanos(mDuration) )
        {
            cancel();
            Platform.runLater(mRunnableAfter);
        }
        else
            Platform.runLater(mRunnableDuring);
    }

    private FxTask(final Runnable during, final Runnable after, final long duration, final TimeUnit unit) {

        mRunnableDuring = during;
        mRunnableAfter = after;

        mDuration = duration;
        mTimeUnit = unit;

        mTimeStarted = System.nanoTime();
    }

    private final Runnable mRunnableDuring;
    private final Runnable mRunnableAfter;
    private final long mDuration;
    private final TimeUnit mTimeUnit;

    private final long mTimeStarted;
}

我使用Runnable安排警报如下:

final Alert alert = new Alert(...);

scheduler.scheduleAtFixedRate(FxTask.create(
    () -> {
        switch (alert.alertStateProperty().get()) {

            case UNRESOLVED_NEW_0:
                alert.alertStateProperty().set(Alert.State.UNRESOLVED_NEW_1);
                refreshTable(mAlertsTable);
                break;

            case UNRESOLVED_NEW_1:
                alert.alertStateProperty().set(Alert.State.UNRESOLVED_NEW_0);
                refreshTable(mAlertsTable);
                break;
        }
    },
    () -> { // This is run at the end
        if (equalsAny(alert.alertStateProperty().get(), Alert.State.UNRESOLVED_NEW_0, SpreadAlert.State.UNRESOLVED_NEW_1)) {
            alert.alertStateProperty().set(Alert.State.UNRESOLVED_OLD);
            refreshTable(mAlertsTable);
        }
    },
    10, TimeUnit.SECONDS), 0, 500, TimeUnit.MILLISECONDS
);

注意:TableView上没有显示alertStateProperty()(它没有绑定到任何列)。 所以为了强制JavaFx重绘,我必须使用refreshTable(),遗憾的是重绘整个表(?)。

public static <T> void refreshTable(final TableView<T> table) {

        table.getColumns().get(0).setVisible(false);
        table.getColumns().get(0).setVisible(true);
}

问题在于,即使我同时创建少量警报,CPU使用率也非常高:有时从20%到84%,平均值约为40%。当10s通过所有警报时,CPU消耗将返回0%。如果我注释掉refreshTable(),则CPU保持在0%附近,这表明这是问题所在。

为什么要使用这么多CPU? (顺便说一下,我有8个核心)。 还有另一种方法可以重绘一行而不重绘整个表吗?

我甚至尝试了一种'hacky'方法 - 更改警报的所有值,然后重新设置它们以使JavaFx检测到更改并重新绘制,但CPU再次处于相同的级别。

1 个答案:

答案 0 :(得分:1)

更改表行颜色的最有效方法可能是使用表行工厂,让它创建的表行观察相应的属性,并根据需要更新一个或多个CSS PseudoClass状态。然后只需在外部css文件中定义颜色。

这是您描述的应用程序的独立版本。我只是用Timeline来执行&#34;闪烁的新警报&#34;,这是更少的代码;但如果您愿意,可以使用执行程序。这里的关键思想是表行工厂,以及通过观察属性来操作的伪类状态。在我的系统上,如果我用新的(闪烁的)行填充整个表格,CPU不会超过35%(一个核心的百分比),这似乎是完全可以接受的。

请注意,在Java 8中引入了PseudoClass。在早期版本的JavaFX中,您可以通过操作样式类来实现相同的目的,但是您必须注意不要复制任何样式类,因为它们存储为一个List。有趣的是,伪类方法更有效。

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener.Change;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.stage.Stage;
import javafx.util.Duration;

public class AlertTableDemo extends Application {

    @Override
    public void start(Stage primaryStage) {
        TableView<Alert> table = new TableView<>();
        table.getColumns().add(createColumn("Name", Alert::nameProperty));
        table.getColumns().add(createColumn("Value", Alert::valueProperty));
        TableColumn<Alert, Alert> resolveCol = 
                createColumn("Resolve", ReadOnlyObjectWrapper<Alert>::new);
        resolveCol.setCellFactory(this::createResolveCell);
        table.getColumns().add(resolveCol);

        // just need a wrapper really, don't need the atomicity...
        AtomicInteger alertCount = new AtomicInteger(); 
        Random rng = new Random();

        Button newAlertButton = new Button("New Alert");
        newAlertButton.setOnAction( event -> 
            table.getItems().add(new Alert("Alert "+alertCount.incrementAndGet(), 
                    rng.nextInt(20)+1)));

        // set psuedo-classes on table rows depending on alert state:
        table.setRowFactory(tView -> {

            TableRow<Alert> row = new TableRow<>();

            ChangeListener<Alert.State> listener = (obs, oldState, newState) -> 
                updateTableRowPseudoClassState(row, row.getItem().getState());

            row.itemProperty().addListener((obs, oldAlert, newAlert) -> {
                if (oldAlert != null) {
                    oldAlert.stateProperty().removeListener(listener);
                }
                if (newAlert == null) {
                    clearTableRowPseudoClassState(row);
                } else {
                    updateTableRowPseudoClassState(row, row.getItem().getState());
                    newAlert.stateProperty().addListener(listener);
                }
            });

            return row ;
        });

        // flash new alerts:
        table.getItems().addListener((Change<? extends Alert> change) -> {
            while (change.next()) {
                if (change.wasAdded()) {
                    List<? extends Alert> newAlerts = 
                            new ArrayList<>(change.getAddedSubList());
                    flashAlerts(newAlerts);
                }
            }
        });


        HBox controls = new HBox(5, newAlertButton);
        controls.setPadding(new Insets(10));
        controls.setAlignment(Pos.CENTER);

        BorderPane root = new BorderPane(table, null, null, controls, null);
        Scene scene = new Scene(root, 800, 600);
        scene.getStylesheets().add(
                getClass().getResource("alert-table.css").toExternalForm());
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private void flashAlerts(List<? extends Alert> newAlerts) {
        Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(0.5), 
                event -> {
                    for (Alert newAlert : newAlerts) {
                        if (newAlert.getState()==Alert.State.UNRESOLVED_NEW_0) {
                            newAlert.setState(Alert.State.UNRESOLVED_NEW_1);
                        } else if (newAlert.getState() == Alert.State.UNRESOLVED_NEW_1){
                            newAlert.setState(Alert.State.UNRESOLVED_NEW_0);
                        }
                    }
                }));
        timeline.setOnFinished(event -> {
            for (Alert newAlert : newAlerts) {
                if (newAlert.getState() != Alert.State.RESOLVED) {
                    newAlert.setState(Alert.State.UNRESOLVED_OLD);
                }
            }
        });
        timeline.setCycleCount(20);
        timeline.play();
    }

    private void clearTableRowPseudoClassState(Node node) {
        node.pseudoClassStateChanged(PseudoClass.getPseudoClass("unresolved-new"), false);
        node.pseudoClassStateChanged(PseudoClass.getPseudoClass("unresolved-new-alt"), false);
        node.pseudoClassStateChanged(PseudoClass.getPseudoClass("unresolved-old"), false);
        node.pseudoClassStateChanged(PseudoClass.getPseudoClass("resolved"), false);
    }

    private void updateTableRowPseudoClassState(Node node, Alert.State state) {
        node.pseudoClassStateChanged(PseudoClass.getPseudoClass("unresolved-new"), 
                state==Alert.State.UNRESOLVED_NEW_0);
        node.pseudoClassStateChanged(PseudoClass.getPseudoClass("unresolved-new-alt"), 
                state==Alert.State.UNRESOLVED_NEW_1);
        node.pseudoClassStateChanged(PseudoClass.getPseudoClass("unresolved-old"), 
                state==Alert.State.UNRESOLVED_OLD);
        node.pseudoClassStateChanged(PseudoClass.getPseudoClass("resolved"), 
                state==Alert.State.RESOLVED);       
    }

    private TableCell<Alert, Alert> createResolveCell(TableColumn<Alert, Alert> col) {
        TableCell<Alert, Alert> cell = new TableCell<>();
        Button resolveButton = new Button("Resolve");

        resolveButton.setOnAction(event -> 
            cell.getItem().setState(Alert.State.RESOLVED));

        cell.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
        cell.setAlignment(Pos.CENTER);
        cell.graphicProperty().bind(
                Bindings.when(cell.emptyProperty())
                .then((Node)null)
                .otherwise(resolveButton));
        return cell ;
    }

    private <S, T> TableColumn<S, T> createColumn(String title, 
            Function<S, ObservableValue<T>> propertyMapper) {
        TableColumn<S,T> col = new TableColumn<>(title);
        col.setCellValueFactory(cellData -> propertyMapper.apply(cellData.getValue()));
        col.setMinWidth(Region.USE_PREF_SIZE);
        col.setPrefWidth(150);
        return col ;
    }

    public static class Alert {
        public enum State { 
            UNRESOLVED_NEW_0, UNRESOLVED_NEW_1, UNRESOLVED_OLD, RESOLVED 
        }

        private final ObjectProperty<State> state = new SimpleObjectProperty<>();
        private final StringProperty name = new SimpleStringProperty();
        private final IntegerProperty value = new SimpleIntegerProperty();
        public final ObjectProperty<State> stateProperty() {
            return this.state;
        }
        public final AlertTableDemo.Alert.State getState() {
            return this.stateProperty().get();
        }
        public final void setState(final AlertTableDemo.Alert.State state) {
            this.stateProperty().set(state);
        }
        public final StringProperty nameProperty() {
            return this.name;
        }
        public final java.lang.String getName() {
            return this.nameProperty().get();
        }
        public final void setName(final java.lang.String name) {
            this.nameProperty().set(name);
        }
        public final IntegerProperty valueProperty() {
            return this.value;
        }
        public final int getValue() {
            return this.valueProperty().get();
        }
        public final void setValue(final int value) {
            this.valueProperty().set(value);
        }

        public Alert(String name, int value) {
            setName(name);
            setValue(value);
            setState(State.UNRESOLVED_NEW_0);
        }
    }

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

警报-table.css:

.table-row-cell:resolved {
    -fx-background: green ;
}
.table-row-cell:unresolved-old {
    -fx-background: red ;
}
.table-row-cell:unresolved-new {
    -fx-background: blue ;
}
.table-row-cell:unresolved-new-alt {
    -fx-background: yellow ;
}