当我试图实现一个控制台风格的组件时,我遇到了JTextPane的性能限制。在大多数情况下,我的控制台表现得很好,但是试图用大量的非空格分隔的文本来填充它,最终完全冻结了GUI。我想避免这种情况,或者至少提供一个以正常方式点击停止按钮的机会。
一些快速的剖析发现,EDT大部分时间都停留在JTextPane中布局文本(布局LabelViews是其EditorKit实现的一部分)--由于Swing的东西应该在EDT上完成,我以为我完蛋了。但后来还是看到了一丝希望。经过一番研究,我偶然发现了一些失传的Swing艺术。也就是 本文 由Timothy Prinzing撰写。
(现在已经完全坏掉了)这篇文章描述了如何将纠缠我的问题(布局)从EDT上推掉,定义了一个叫作 AsyncBoxView
,让我惊讶的是,现在 摇摆的一部分. 但是...
在修改了我的编辑器套件,创建AsyncBoxView而不是通常的 BoxView
我马上遇到了一个问题 - 它在初始化过程中抛出了一个NPE。下面是一些代码。
package com.stackoverflow
import java.awt.*;
import java.awt.event.*;
import java.util.concurrent.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
public class ConsoleTest extends JFrame {
public static final boolean USE_ASYNC_BOX_VIEW = true;
public static final int MAX_CHARS = 1000000;
public static final int MAX_LINES = 100;
private static final String LONG_TEXT;
static {
StringBuilder sb = new StringBuilder();
String tmp = ""
+ "<?xml version = \"1.0\" encoding = \"utf-8\"?><!-- planes.xml"
+ " - A document that lists ads for used airplanes --><!DOCTYPE "
+ "planes_for_sale SYSTEM \"planes.dtd\"><planes_for_sale><ad>"
+ "<year> 1977 </year><make> &c; </make><model> Skyhawk </model>"
+ "<color> Light blue and white </color><description> New paint,"
+ " nearly new interior, 685 hours SMOH, full IFR King avionics"
+ " </description><price> 23,495 </price><seller phone = \"555-"
+ "222-3333\"> Skyway Aircraft </seller><location><city> Rapid "
+ "City, </city><state> South Dakota </state></location></ad>"
+ "<ad><year>1965</year><make>&p;</make><model>Cherokee</model>"
+ "<color>Gold</color><description>240 hours SMOH, dual NAVCOMs"
+ ", DME, new Cleveland brakes, great shape</description><sell"
+ "er phone=\"555-333-2222\" email=\"[email protected]\">John"
+ " Seller</seller><location><city>St. Joseph,</city><state>Mi"
+ "ssouri</state></location></ad></planes_for_sale>";
// XML obtained from:
// https://www.cs.utexas.edu/~mitra/csFall2015/cs329/lectures/xml.html
for (int i = 0; i < 1000 * 10 * 2; i++) { // ~15 MB of data?
sb.append(tmp);
}
LONG_TEXT = sb.toString();
}
public ConsoleTest() {
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLayout(new BorderLayout());
setTitle("Console Spammer");
// the console
final JTextPane console = new JTextPane();
console.setFont(new Font("Monospaced", Font.PLAIN, 12));
console.setEditorKit(new ConsoleEditorKit());
console.setEditable(false);
JScrollPane scroll = new JScrollPane(console);
scroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
add(scroll, BorderLayout.CENTER);
// make a style rainbow
final Style[] styles = new Style[]{
console.addStyle("0", null),
console.addStyle("1", null),
console.addStyle("2", null),
console.addStyle("3", null),
console.addStyle("4", null),
console.addStyle("5", null)
};
StyleConstants.setForeground(styles[0], Color.red);
StyleConstants.setForeground(styles[1], Color.blue);
StyleConstants.setForeground(styles[2], Color.green);
StyleConstants.setForeground(styles[3], Color.orange);
StyleConstants.setForeground(styles[4], Color.black);
StyleConstants.setForeground(styles[5], Color.yellow);
// simulate spam comming from non-EDT thread
final DefaultStyledDocument document = (DefaultStyledDocument) console.getDocument();
final Timer spamTimer = new Timer(100, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
SwingWorker<Void, Void> worker = new SwingWorker<Void, Void>() {
@Override
protected Void doInBackground() throws Exception {
final int chunkSize = 16384;
int remaining = LONG_TEXT.length();
int position = 0;
while (remaining > 0) {
final String chunk;
if (remaining - chunkSize > 0) {
remaining -= chunkSize;
position += chunkSize;
chunk = LONG_TEXT.substring(position - chunkSize, position);
} else {
chunk = LONG_TEXT.substring(position, position + remaining);
remaining = 0;
}
// perform all writes on the same thread (EDT)
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
try {
performSpam(document, styles, chunk);
} catch (BadLocationException ex) {
ex.printStackTrace();
}
}
});
}
return null;
}
@Override
protected void done() {
try {
get();
} catch (InterruptedException | ExecutionException ex) {
ex.printStackTrace();
}
}
};
worker.execute();
}
});
spamTimer.setRepeats(true);
// the toggle
JToggleButton spam = new JToggleButton("Spam");
spam.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
if (e.getStateChange() == ItemEvent.SELECTED) {
spamTimer.restart();
} else {
spamTimer.stop();
}
}
});
add(spam, BorderLayout.PAGE_END);
// limit number of lines (not that it matters)
DocumentListener limitLinesDocListener = new DocumentListener() {
@Override
public void insertUpdate(final DocumentEvent e) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
Element root = document.getDefaultRootElement();
while (root.getElementCount() > MAX_LINES) {
Element line = root.getElement(0);
int end = line.getEndOffset();
try {
document.remove(0, end);
} catch (BadLocationException ex) {
break;
} finally {
}
}
}
});
}
@Override
public void removeUpdate(DocumentEvent e) {
}
@Override
public void changedUpdate(DocumentEvent e) {
}
};
document.addDocumentListener(limitLinesDocListener);
setSize(640, 480);
setLocationRelativeTo(null);
}
private void performSpam(
DefaultStyledDocument document, Style[] styles, String chunk) throws BadLocationException {
System.out.println(
String.format("chunk-len:%d\t\tdoc-len:%d",
chunk.length(), document.getLength(),
document.getDefaultRootElement().getElementCount()));
document.insertString(
document.getLength(), chunk,
styles[ThreadLocalRandom.current().nextInt(0, 5 + 1)]);
while (document.getLength() > MAX_CHARS) { // limit number of chars or we'll have a bad time
document.remove(0, document.getLength() - MAX_CHARS);
}
}
public static class ConsoleEditorKit extends StyledEditorKit {
public ViewFactory getViewFactory() {
return new MyViewFactory();
}
static class MyViewFactory implements ViewFactory {
public View create(Element elem) {
String kind = elem.getName();
if (kind != null) {
if (kind.equals(AbstractDocument.ContentElementName)) {
return new WrapLabelView(elem);
} else if (kind.equals(AbstractDocument.ParagraphElementName)) {
return new CustomParagraphView(elem);
} else if (kind.equals(AbstractDocument.SectionElementName)) {
return USE_ASYNC_BOX_VIEW ? new AsyncBoxView(elem, View.Y_AXIS) : new BoxView(elem, View.Y_AXIS);
} else if (kind.equals(StyleConstants.ComponentElementName)) {
return new ComponentView(elem);
} else if (kind.equals(StyleConstants.IconElementName)) {
return new IconView(elem);
}
}
return new LabelView(elem);
}
}
static class WrapLabelView extends LabelView {
public WrapLabelView(Element elem) {
super(elem);
}
public float getMinimumSpan(int axis) {
switch (axis) {
case View.X_AXIS:
return 0;
case View.Y_AXIS:
return super.getMinimumSpan(axis);
default:
throw new IllegalArgumentException("Invalid axis: " + axis);
}
}
}
static class CustomParagraphView extends ParagraphView {
public static int MAX_VIEW_SIZE = 100;
public CustomParagraphView(Element elem) {
super(elem);
strategy = new MyFlowStrategy();
}
public int getResizeWeight(int axis) {
return 0;
}
public static class MyFlowStrategy extends FlowView.FlowStrategy {
protected View createView(FlowView fv, int startOffset, int spanLeft, int rowIndex) {
View res = super.createView(fv, startOffset, spanLeft, rowIndex);
if (res.getEndOffset() - res.getStartOffset() > MAX_VIEW_SIZE) {
res = res.createFragment(startOffset, startOffset + MAX_VIEW_SIZE);
}
return res;
}
}
}
}
public static void main(String[] args)
throws ClassNotFoundException, InstantiationException,
IllegalAccessException, UnsupportedLookAndFeelException {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
new ConsoleTest().setVisible(true);
}
});
}
}
这段代码会抛出:
Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException
at javax.swing.text.AsyncBoxView.preferenceChanged(AsyncBoxView.java:511)
at javax.swing.text.View.preferenceChanged(View.java:288)
at javax.swing.text.BoxView.preferenceChanged(BoxView.java:286)
at javax.swing.text.FlowView$FlowStrategy.insertUpdate(FlowView.java:380)
at javax.swing.text.FlowView.loadChildren(FlowView.java:143)
at javax.swing.text.CompositeView.setParent(CompositeView.java:139)
at javax.swing.text.FlowView.setParent(FlowView.java:289)
at javax.swing.text.AsyncBoxView$ChildState.<init>(AsyncBoxView.java:1211)
at javax.swing.text.AsyncBoxView.createChildState(AsyncBoxView.java:220)
at javax.swing.text.AsyncBoxView.replace(AsyncBoxView.java:374)
at javax.swing.text.AsyncBoxView.loadChildren(AsyncBoxView.java:411)
at javax.swing.text.AsyncBoxView.setParent(AsyncBoxView.java:479)
at javax.swing.plaf.basic.BasicTextUI$RootView.setView(BasicTextUI.java:1328)
at javax.swing.plaf.basic.BasicTextUI.setView(BasicTextUI.java:693)
at javax.swing.plaf.basic.BasicTextUI.modelChanged(BasicTextUI.java:682)
at javax.swing.plaf.basic.BasicTextUI$UpdateHandler.propertyChange(BasicTextUI.java:1794)
at java.beans.PropertyChangeSupport.fire(PropertyChangeSupport.java:335)
at java.beans.PropertyChangeSupport.firePropertyChange(PropertyChangeSupport.java:327)
at java.beans.PropertyChangeSupport.firePropertyChange(PropertyChangeSupport.java:263)
at java.awt.Component.firePropertyChange(Component.java:8434)
at javax.swing.text.JTextComponent.setDocument(JTextComponent.java:443)
at javax.swing.JTextPane.setDocument(JTextPane.java:136)
at javax.swing.JEditorPane.setEditorKit(JEditorPane.java:1055)
at javax.swing.JTextPane.setEditorKit(JTextPane.java:473)
at com.stackoverflow.ConsoleTest.<init>(ConsoleTest.java:53)
...
试图找到资源来描述如何正确地做这件事已经被证明是相当困难的。如果有人能描述如何使用 AsyncBoxView 来提高 EDT 的响应速度,我会非常感激。
注意:如果你把USE_ASYNC_BOX_VIEW设置为false,你就能明白我说的性能限制是什么意思,不过和这个简单的例子相比,我的实际用例的性能要差很多。
编辑:我的实际使用案例与这个简单的例子相比,性能更差。
AsyncBoxView.java文件的第511行是什么?
异常抛出的位置是 cs.preferenceChanged(width, height);
下(JDK 1.8)。
public synchronized void preferenceChanged(View child, boolean width, boolean height) {
if (child == null) {
getParent().preferenceChanged(this, width, height);
} else {
if (changing != null) {
View cv = changing.getChildView();
if (cv == child) {
// size was being changed on the child, no need to
// queue work for it.
changing.preferenceChanged(width, height);
return;
}
}
int index = getViewIndex(child.getStartOffset(),
Position.Bias.Forward);
ChildState cs = getChildState(index);
cs.preferenceChanged(width, height);
LayoutQueue q = getLayoutQueue();
q.addTask(cs);
q.addTask(flushTask);
}
}
编辑。
我设法使我的例子工作,通过改变init期间的调用顺序,并确保设置编辑器工具包不会改变JTextPane构造函数调用的原始文档(我做了一个重载的 StyledEditorKit.createDefaultDocument()
并让它返回同样的DefaultStyledDocument的原始实例)。) 之后,它仍然抛出了一个NPE JTextPane.setEditable(false)
所以我在设置编辑器套件之前设置了这个。
final JTextPane console = new JTextPane();
console.setFont(new Font("Monospaced", Font.PLAIN, 12));
console.setEditable(false);
final DefaultStyledDocument document = (DefaultStyledDocument) console.getDocument();
console.setEditorKit(new ConsoleEditorKit(document));
public static class ConsoleEditorKit extends StyledEditorKit {
final DefaultStyledDocument document;
public ConsoleEditorKit(DefaultStyledDocument document) {
this.document = document;
}
@Override
public Document createDefaultDocument() {
return document;
}
// ...
}
不幸的是,对于我的实际使用案例来说,这不是一个选项,因为切换可编辑属性是必须的。此外,它似乎会在其他JTextPane属性变化上抛出NPE,比如JTextPane.setFont(Font),也是如此,除非在设置编辑器工具包实例之前完成。所以我的问题仍然存在。你是如何使用AsyncBoxView的?
编辑。
我现在经历了同样的NPE,即使是在简单地将文本插入到JTextPane中,所以像上面编辑中描述的那样绕过这个问题是没有意义的。
java.lang.NullPointerException
at javax.swing.text.AsyncBoxView.preferenceChanged(AsyncBoxView.java:511)
at javax.swing.text.View.preferenceChanged(View.java:288)
at javax.swing.text.BoxView.preferenceChanged(BoxView.java:286)
at javax.swing.text.FlowView$FlowStrategy.insertUpdate(FlowView.java:380)
at javax.swing.text.FlowView.loadChildren(FlowView.java:143)
at javax.swing.text.CompositeView.setParent(CompositeView.java:139)
at javax.swing.text.FlowView.setParent(FlowView.java:289)
at javax.swing.text.AsyncBoxView$ChildState.<init>(AsyncBoxView.java:1211)
at javax.swing.text.AsyncBoxView.createChildState(AsyncBoxView.java:220)
at javax.swing.text.AsyncBoxView.replace(AsyncBoxView.java:374)
at javax.swing.text.AsyncBoxView.loadChildren(AsyncBoxView.java:411)
at javax.swing.text.AsyncBoxView.setParent(AsyncBoxView.java:479)
at javax.swing.plaf.basic.BasicTextUI$RootView.setView(BasicTextUI.java:1328)
at javax.swing.plaf.basic.BasicTextUI.setView(BasicTextUI.java:693)
at javax.swing.plaf.basic.BasicTextUI.modelChanged(BasicTextUI.java:682)
at javax.swing.plaf.basic.BasicTextUI$UpdateHandler.insertUpdate(BasicTextUI.java:1862)
at javax.swing.text.AbstractDocument.fireInsertUpdate(AbstractDocument.java:201)
at javax.swing.text.AbstractDocument.handleInsertString(AbstractDocument.java:748)
at javax.swing.text.AbstractDocument.access$200(AbstractDocument.java:99)
at javax.swing.text.AbstractDocument$DefaultFilterBypass.insertString(AbstractDocument.java:3107)
...
可能是一个bug的表现 JDK-6740328. javax.swing.text.AsyncBoxView
无法使用,自2006年以来一直如此。