Swing组件更新性能骤降问题:原因与无栈溢出疑问
最小可复现示例(MRE)
import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.WindowConstants; import javax.swing.border.Border; import javax.swing.border.EmptyBorder; import javax.swing.plaf.basic.BasicTextFieldUI; import java.awt.Component; import java.awt.Container; import java.awt.Graphics; public class TextFieldDemo { public static void main(String[] args) { Container mainPanel = createMainPanel(); JFrame frame = new JFrame("Text field demo"); frame.setContentPane(mainPanel); frame.setLocationRelativeTo(null); frame.pack(); frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); frame.setVisible(true); } private static Container createMainPanel() { JPanel panel = new JPanel(); panel.add(createTextField()); return panel; } private static Component createTextField() { JTextField textField = new JTextField(10); textField.setUI(new BasicTextFieldUI() { int counter = 1; @Override public void update(Graphics g, JComponent c) { super.update(g, c); textField.setBorder(textField.getBorder()); // textField.setBorder(getBorder()); System.out.printf("UPDATING %d...%n", counter++); } private Border getBorder() { EmptyBorder border = new EmptyBorder(0, 3, 0, 3); return border; } }); return textField; } }
测试1
直接运行代码并将光标置于文本框内,Swing会无明显原因地持续更新该字段(推测与插入符有关),控制台输出如下:
UPDATING 26... UPDATING 27... UPDATING 28... UPDATING 29... UPDATING 30...
此时CPU占用正常(截图显示CPU使用率为0),转移焦点后更新停止。
测试2
取消注释以下代码并注释原行:
// textField.setBorder(textField.getBorder()); textField.setBorder(getBorder());
此时更新频率大幅提升,控制台计数飞速增长:
UPDATING 39745... UPDATING 39746... UPDATING 39747... UPDATING 39748...
CPU占用显著升高(截图显示CPU使用率达20%+),转移焦点至其他窗口后更新仍不停止,CPU占用进一步升至30%+。
用户推测
在UI的update方法中设置新的Border实例会触发无限循环——setBorder会调用repaint(),进而再次触发update方法,相关源码如下:
// javax.swing.JComponent#setBorder public void setBorder(Border border) { Border oldBorder = this.border; this.border = border; firePropertyChange("border", oldBorder, border); if (border != oldBorder) { if (border == null || oldBorder == null || !(border.getBorderInsets(this).equals(oldBorder.getBorderInsets(this)))) { revalidate(); } repaint(); } }
疑问
- 性能骤降的原因是什么?该猜测是否正确?
- 为何不会出现
StackOverflowError?
解答
问题1:性能骤降的原因及猜测正确性
你的猜测完全正确。当你在update方法里调用setBorder(getBorder())时,每次都会创建一个新的EmptyBorder实例,传递给setBorder后,方法里会判断border != oldBorder(新实例和旧实例引用不同,条件成立),然后调用repaint()。repaint()会触发Swing的绘制流程,最终又会调用update方法,形成一个无限的事件循环:update → setBorder → repaint → update...
而测试1里调用textField.setBorder(textField.getBorder())时,因为传递的是当前已有的Border实例,setBorder里判断border == oldBorder,不会执行repaint(),所以只有插入符闪烁触发的正常更新,CPU占用自然不高。
问题2:为何不会出现StackOverflowError
因为Swing的绘制事件是在**事件调度线程(EDT)**里通过队列处理的,不是同步递归调用。repaint()并不会立即执行update方法,而是向EDT的事件队列里添加一个绘制请求。当前的update方法执行完毕后,EDT才会取出队列里的下一个绘制请求,调用update。整个过程是队列式的循环,不是方法的嵌套递归,所以不会出现栈溢出。
内容的提问来源于stack exchange,提问作者Sergey Zolotarev

