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

问题描述 投票:3回答:1

当我试图实现一个控制台风格的组件时,我遇到了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)
    ...
java multithreading swing jtextpane event-dispatch-thread
1个回答
0
投票

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

© www.soinside.com 2019 - 2024. All rights reserved.