如何使用 javax.swing.text.AsyncBoxView 将 JTextPane 中的文本布局委托给非 EDT 线程?

pre*_*edi 8 java swing multithreading jtextpane event-dispatch-thread

我在尝试实现控制台样式组件时遇到了 JTextPane 的性能限制。在大多数情况下,我的控制台运行良好,但是尝试使用大量非空格分隔文本向它发送垃圾邮件最终会完全冻结 GUI。我想避免这种情况,或者至少提供一个以正常方式点击停止按钮的机会。

一些快速分析显示,EDT 大部分时间都被困在 JTextPane 中布置文本(将 LabelViews 作为其 EditorKit 实现的一部分进行布置) - 由于 Swing 的东西应该在 EDT 上完成,我以为我被搞砸了。但随即又是一丝希望。经过一番研究,我偶然发现了一些丢失的摇摆艺术。即,Timothy Prinzing撰写的这篇文章

这篇(现已完全失效)文章描述了如何将困扰我(布局)的问题从 EDT 中排除,定义了一个名为 的类AsyncBoxView,令我惊讶的是,它现在是 Swing 的一部分。但...

在修改我的编辑器工具包以创建 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=\"jseller@www.axl.com\">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);
            }
        });
    }
}
Run Code Online (Sandbox Code Playgroud)

此代码抛出:

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)
    ...
Run Code Online (Sandbox Code Playgroud)

试图找到描述如何正确执行此操作的资源已被证明是非常困难的。如果有人能描述如何使用 AsyncBoxView 来提高 EDT 响应能力,我将不胜感激。

注意:如果您将 USE_ASYNC_BOX_VIEW 设置为 false,您可以理解我所说的性能限制是什么意思,尽管与这个简单示例相比,我的实际用例的性能要差得多。

编辑:

文件 AsyncBoxView.java 中的第 511 行是哪一行?

cs.preferenceChanged(width, height);下面(JDK 1.8)抛出异常。

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)
    ...
Run Code Online (Sandbox Code Playgroud)

编辑:

我设法通过在 init 期间更改调用顺序并确保设置编辑器工具包不会更改来自 JTextPane 构造函数调用的原始文档来使我的示例工作(我覆盖StyledEditorKit.createDefaultDocument()并使其返回 DefaultStyledDocument 的相同原始实例)。之后它仍然抛出了一个 NPE,JTextPane.setEditable(false)所以我在设置编辑器工具包之前设置了它。

    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);
        }
    }
Run Code Online (Sandbox Code Playgroud)
        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));
Run Code Online (Sandbox Code Playgroud)

不幸的是,这不是我实际用例的选项,因为在那里切换可编辑属性是必须的。此外,除非在设置编辑器工具包实例之前完成,否则它似乎也会将 NPE 用于其他 JTextPane 属性更改,例如 JTextPane.setFont(Font)。所以我的问题仍然存在。你如何使用 AsyncBoxView?

编辑:

即使在简单地将文本插入 JTextPane 之后,我现在也经历了相同的 NPE,因此解决上面编辑中描述的问题毫无意义。

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)
    ...
Run Code Online (Sandbox Code Playgroud)

pre*_*edi 1

可能是错误JDK-6740328的表现。javax.swing.text.AsyncBoxView无法使用,自 2006 年以来一直如此。