TableCell自定义Skin运行14小时后触发NPE问题排查求助
问题排查:TableView长时间运行后TableCell Skin空指针异常
我在应用中实现了TableView,为TableCell创建了自定义Skin,并在TableCell的createDefaultSkin()方法中初始化该自定义Skin,前期运行正常。但应用连续运行14小时后崩溃,抛出如下NullPointerException:
java.lang.NullPointerException: Cannot invoke "javafx.scene.control.Skin.dispose()" because the return value of "javafx.scene.control.IndexedCell.getSkin()" is null at javafx.scene.control.skin.TableRowSkinBase.recreateCells(TableRowSkinBase.java:713) at javafx.scene.control.skin.TableRowSkinBase.updateCells(TableRowSkinBase.java:496) at javafx.scene.control.skin.TableRowSkinBase.checkState(TableRowSkinBase.java:640) at javafx.scene.control.skin.TableRowSkinBase.layoutChildren(TableRowSkinBase.java:247) at javafx.scene.control.Control.layoutChildren(Control.java:601) at javafx.scene.control.Cell.layoutChildren(Cell.java:636) at javafx.scene.Parent.layout(Parent.java:1207) at javafx.scene.Parent.layout(Parent.java:1214) at javafx.scene.Parent.layout(Parent.java:1214) at javafx.scene.Parent.layout(Parent.java:1214) at javafx.scene.Parent.layout(Parent.java:1214) at javafx.scene.Parent.layout(Parent.java:1214) at javafx.scene.Parent.layout(Parent.java:1214) at javafx.scene.Parent.layout(Parent.java:1214) at javafx.scene.Parent.layout(Parent.java:1214) at javafx.scene.Scene.doLayoutPass(Scene.java:579) at javafx.scene.Scene$ScenePulseListener.pulse(Scene.java:2499) at com.sun.javafx.tk.Toolkit.lambda$runPulse$2(Toolkit.java:405) at java.base/java.security.AccessController.doPrivileged(AccessController.java:399) at com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:404) at com.sun.javafx.tk.Toolkit.firePulse(Toolkit.java:434) at com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:575) at com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:555) at com.sun.javafx.tk.quantum.QuantumToolkit.pulseFromQueue(QuantumToolkit.java:548) at com.sun.javafx.tk.quantum.QuantumToolkit.lambda$runToolkit$11(QuantumToolkit.java:352) at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96) at com.sun.glass.ui.gtk.GtkApplication._runLoop(Native Method) at com.sun.glass.ui.gtk.GtkApplication.lambda$runLoop$11(GtkApplication.java:316) at java.base/java.lang.Thread.run(Thread.java:833)
TableRowSkinBase中recreateCells()方法的相关代码如下:
/* * A map that maps from TableColumn to TableCell (i.e. model to view). * This is recreated whenever the leaf columns change, however to increase * efficiency we create cells for all columns, even if they aren't visible, * and we only create new cells if we don't already have it cached in this * map. * * Note that this means that it is possible for this map to therefore be * a memory leak if an application uses TableView and is creating and removing * a large number of tableColumns. This is mitigated in the recreateCells() * function below - refer to that to learn more. */ WeakHashMap<TableColumnBase, Reference<R>> cellsMap; private void recreateCells() { if (cellsMap != null) { Collection<Reference<R>> cells = cellsMap.values(); Iterator<Reference<R>> cellsIter = cells.iterator(); while (cellsIter.hasNext()) { Reference<R> cellRef = cellsIter.next(); R cell = cellRef.get(); if (cell != null) { cell.updateIndex(-1); cell.getSkin().dispose(); // THIS IS WHERE THE ISSUE OCCURS cell.setSkin(null); } } cellsMap.clear(); } ObservableList<? extends TableColumnBase/*<T,?>*/> columns = getVisibleLeafColumns(); cellsMap = new WeakHashMap<>(columns.size()); fullRefreshCounter = DEFAULT_FULL_REFRESH_COUNTER; getChildren().clear(); for (TableColumnBase col : columns) { if (cellsMap.containsKey(col)) { continue; } // create a TableCell for this column and store it in the cellsMap // for future use createCellAndCache(col); } }
自定义Skin逻辑简单,仅包含自定义行为实现:
import javafx.beans.property.ReadOnlyObjectProperty; import javafx.scene.control.IndexedCell; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumnBase; import javafx.scene.control.skin.TableCellSkinBase; /** * Custom table cell skin. * * @param <S> The type of the TableView generic type (i.e. S == TableView<S>) * @param <T> The type of the content in all cells in this TableColumn * @param <C> the cell type */ public final class CustomTableCellSkin<S, T, C extends IndexedCell<T>> extends TableCellSkinBase<S, T, C> { /** Cell behaviour. */ private final CustomTableCellBehavior<S, T> behaviour; /** * Constructor. * * @param cell skinnable cell */ @SuppressWarnings("unchecked") public CustomTableCellSkin(final TableCell<S, T> cell) { super((C) cell); behaviour = new CustomTableCellBehavior<>(cell); } /** * Return the cell behaviour. * * @return behaviour */ public final CustomTableCellBehavior<S, T> getBehaviour() { return behaviour; } @Override public final ReadOnlyObjectProperty<? extends TableColumnBase<S, T>> tableColumnProperty() { @SuppressWarnings("unchecked") final TableCell<S, T> cell = (TableCell<S, T>) getSkinnable(); return cell.tableColumnProperty(); } }
排查线索
1. 单元格Skin会在哪些情况下被清空?
- 手动调用
cell.setSkin(null):业务代码或第三方逻辑中若存在该调用,会直接清空Skin - Skin被垃圾回收:若Skin仅被弱引用持有,且无其他强引用关联,GC时可能被回收,导致
getSkin()返回null - JavaFX内部Skin替换:控件皮肤被重新创建(如主题切换、控件重新初始化)时,旧Skin会被清理,极端场景下可能出现状态不一致
- 单元格复用异常:TableView的单元格复用机制在极端场景(大量数据滚动、列频繁调整)下,可能出现Skin状态异常,导致Skin被意外清空
2. 为何recreateCells()方法中未在调用dispose()前做空检查?
这是JavaFX控件库的潜在bug:
- 开发人员默认认为
cell != null时Skin必然存在,但长时间运行的复杂场景中,可能出现Skin被提前释放的情况 - cellsMap使用WeakHashMap弱引用存储单元格,单元格对象被GC前,Skin可能已被意外清空
- 该场景属于边缘case,常规测试难以复现,因此官方代码未添加空判断
临时修复建议
- 在自定义TableCell中重写
setSkin()方法,确保Skin被正确持有 - 自定义TableRowSkinBase子类,在调用
dispose()前添加空检查(需注意JavaFX内部API的访问限制) - 避免在业务代码中直接操作单元格的Skin对象
内容的提问来源于stack exchange,提问作者Sai Dandem
相关产品推荐
相关产品推荐

