java学习脚印: JTextPane 快速插入问题
原标题: Faster JTextPane Text Insertion
Faster JTextPane Text Insertion (Part I)
The Swing JTextPane
class provides a flexible way to display styled text, with the ability to control the color, alignment, font, and other attributes of each character or paragraph of the pane’s contents. However, JTextPane
has a well-deserved reputation for being slow. The speed problems are particularly noticeable when inserting a large amount of text with varying styles into a text pane, functionality that is needed for applications such as word processors or code editors that support syntax highlighting. In the first part of this article we look at the sources of the speed hit and describe one simple technique for boosting speed. Part II illustrates a more advanced technique for batch updates.
Why is it slow?
Like most other Swing widgets, text components use a Model-View-Controller design to separate the data being displayed (represented by the Model) from the rendering logic (the View) and the interaction logic (the Controller). In the case of text components, the model is an implementation of thejavax.swing.text.Document
interface. For text components such as JTextPane
that support styled text, the javax.swing.text.StyledDocument
subinterface defines additional methods. The concretejavax.swing.text.DefaultStyledDocument
class (or subclasses thereof) is often used for styled editors.
The slow speed of insertion in a JTextPane
has at least two culprits. The first is simply an effect of the Model-View-Controller architecture: If the document into which strings are being inserted is serving as the model for a visible text pane, each insertion will trigger a re-rendering of the display, which may in turn trigger other user interface updates. For interactive editing, this is exactly the behavior that one would want, since the results of user input should be immediately visible when editing text pane content. For initializing a text pane with a large amount of content, or, in general, making large “batch” edits, the overhead of repeated UI updates is signficant.
The second source of sluggishness is an implementation detail of DefaultStyledDocument
. Unlike most Swing classes, which should only be modified in the AWT thread, DefaultStyledDocument
is designed to be thread-safe. This requires DefaultStyledDocument
to implement locking, so that multiple threads do not try to update the document contents at the same time. This locking imposes a small amount of overhead on each insertion or other document modification, which can add up to a long delay for large and complex text insertions.
Note that if an application does not actually require multiple text styles within the same document, the impact of both of these issues can be mitigated to a large extent by simply inserting a single long string rather than a lot of small strings.
Offscreen updates
Avoiding the first of these pitfalls is trivial: You need only ensure that large updates are only performed on document objects that are not attached to text components at the time that the updates are made. For example, instead of retrieving a document object and modifying it:
...
JTextPane jtp = new JTextPane();
Document doc = jtp.getDocument();
for (... iteration over large chunk of parsed text ...) {
...
doc.insertString(offset, partOfText, attrsForPartOfText);
...
}
...
you should either create a new document and insert into it before telling the text pane to use it:
...
JTextPane jtp = new JTextPane();
Document doc = new DefaultStyledDocument();
for (... iteration over large chunk of parsed text ...) {
...
doc.insertString(offset, partOfText, attrsForPartOfText);
...
}
jtp.setDocument(doc);
...
or (if, for example, the text pane uses a custom document class), swap in a blank document and then restore the original document after the inserts:
...
JTextPane jtp = new JTextPane();
Document doc = jtp.getDocument();
Document blank = new DefaultStyledDocument();
jtp.setDocument(blank);
for (... iteration over large chunk of parsed text ...) {
...
doc.insertString(offset, partOfText, attrsForPartOfText);
...
}
jtp.setDocument(doc);
...
The speed boost from this change can be dramatic. For example, for a roughly 450K document in which each word was a different color and size (therefore requiring different attributes), document initialization time was over three times slower with a document attached to a JTextPane
than with an unattached document. Representative times were 151s vs. 460s on a 500Mhz iBook running OSX 10.3.2. (The test code is in Part II of this article.) Of course, a two and a half minute wait for a document to load suggests that there is still room for improvement. In Part II we look at a solution to the second source of sluggishness, per-insertion coordination overhead within DefaultStyledDocument
.
Faster JTextPane Text Insertion (Part II)
In Part I we briefly examined two of the reasons why inserting large quantities of text with different styles (attributes) into a Swing JTextPane
can be very slow: Each update can trigger a UI refresh, and the thread-safe design of DefaultStyledDocument
imposes a small amount of locking overhead on each update.
As shown in Part I, simply “detaching” the document from its text pane before modifying it avoid the UI refresh problem. This can, for example, improve the speed of initializing a large, multi-style document by a factor of three or more, depending on document complexity. For large documents, however, this may not be enough. A little rummaging through the internals of DefaultStyledDocument
reveals a workaround for the second speed issue, internal locking overhead.
Batch Text Insertion
A common way to initialize multi-styled content in a DefaultStyledDocument
is to parse data from a file (or other external source) into a series of substrings and corresponding Attributes
objects, where the attributes contain the font, color, and other style information for each substring. For example, a code editor for an IDE might provide syntax highlighting by parsing a source file to determine language keywords, variable names, and other relevant constructs, and give each a unique style. Each substring would then be added by calling the insertString(int offset, String str, Attributes attrs)
method on the document object.
Since document objects are thread-safe, insertString(…)
first acquires a write-lock to ensure that only a single thread is modifying the underlying data representation, then makes the update, and finally releases the lock when it is finished. For modifications made by user input from the keyboard, this processs is sufficiently fast. For the kind of batch updates needed to initialize a large document, however, the lock management overhead is significant.
In DefaultStyledDocument
, most of the work of actually updating the document contents when a string is inserted is done by the protected
method insertUpdate(…)
. The string and attributes to be inserted are used to create one or more instances of the ElementSpec
class, which are then used to actually effect the modifications.
ElementSpec
is a static inner class within DefaultStyledDocument
. A quick serach for its uses withinDefaultStyledDocument
reveals the method:
protected void insert(int offset, ElementSpec[] data) throws BadLocationException
This version of insert
is also thread-safe (like insertString(…)
), but processes a list of ElementSpec
objects that are to be inserted at a given offset. Unlike insertString(…)
, the lock is only acquired once, rather than once for each modification. This gives us the tools we need to construct a custom subclass that supports batch inserts. Figure 1 shows a possible implementation of such a Document subclass.
import java.util.ArrayList; import javax.swing.text.Element; import javax.swing.text.AttributeSet; import javax.swing.text.BadLocationException; import javax.swing.text.DefaultStyledDocument; /** * DefaultDocument subclass that supports batching inserts. */ public class BatchDocument extends DefaultStyledDocument { /** * EOL tag that we re-use when creating ElementSpecs */ private static final char[] EOL_ARRAY = { '\n' }; /** * Batched ElementSpecs */ private ArrayList batch = null; public BatchDocument() { batch = new ArrayList(); } /** * Adds a String (assumed to not contain linefeeds) for * later batch insertion. */ public void appendBatchString(String str, AttributeSet a) { // We could synchronize this if multiple threads // would be in here. Since we're trying to boost speed, // we'll leave it off for now. // Make a copy of the attributes, since we will hang onto // them indefinitely and the caller might change them // before they are processed. a = a.copyAttributes(); char[] chars = str.toCharArray(); batch.add(new ElementSpec( a, ElementSpec.ContentType, chars, 0, str.length())); } /** * Adds a linefeed for later batch processing */ public void appendBatchLineFeed(AttributeSet a) { // See sync notes above. In the interest of speed, this // isn't synchronized. // Add a spec with the linefeed characters batch.add(new ElementSpec( a, ElementSpec.ContentType, EOL_ARRAY, 0, 1)); // Then add attributes for element start/end tags. Ideally // we'd get the attributes for the current position, but we // don't know what those are yet if we have unprocessed // batch inserts. Alternatives would be to get the last // paragraph element (instead of the first), or to process // any batch changes when a linefeed is inserted. Element paragraph = getParagraphElement(0); AttributeSet pattr = paragraph.getAttributes(); batch.add(new ElementSpec(null, ElementSpec.EndTagType)); batch.add(new ElementSpec(pattr, ElementSpec.StartTagType)); } public void processBatchUpdates(int offs) throws BadLocationException { // As with insertBatchString, this could be synchronized if // there was a chance multiple threads would be in here. ElementSpec[] inserts = new ElementSpec[batch.size()]; batch.toArray(inserts); // Process all of the inserts in bulk super.insert(offs, inserts); } }
BatchDocument
, a document subclass that supports batch insertion of text with different styles.
Use of this class differs slightly from a normal DefaultStyledDocument
. Strings (and their attributes) that are to be inserted should be added by calling the appendBatchString(…)
method. When a new line should be inserted, appendBatchLinefeed(…)
should be called. Once all of the batched content has been added, processBatchUpdates(…)
should be called to actually insert the text into the document. Note that it would be possible to add methods that would parse arbitrary strings and handle linefeeds automatically.
Testing BatchDocument
Figure 2 shows an example of a test class that initialized a document with a long, multi-format string (using either a standard DefaultStyledDocument
or a BatchDocument
, and making updates while the document is either attached and visible or detached) and computes the time required.
import java.awt.Color; import java.awt.BorderLayout; import javax.swing.JFrame; import javax.swing.JTextPane; import javax.swing.JScrollPane; import javax.swing.text.StyleConstants; import javax.swing.text.SimpleAttributeSet; import javax.swing.text.BadLocationException; import javax.swing.text.DefaultStyledDocument; /** * Demonstration class for BatchDocuments. This class creates a * randomly formatted string and adds it to a document. */ public class Test { public static void main(String[] args) throws BadLocationException { if (args.length != 3) { System.err.println("Please give 3 arguments:"); System.err.println(" [true/false] for use batch " + "(true) vs. use default doc [false]"); System.err.println( " [true/false] for update while visible"); System.err.println( " [int] for number of strings to insert"); System.exit(-1); } boolean useBatch = args[0].toLowerCase().equals("true"); boolean updateWhileVisible = args[1].equals("true"); int iterations = Integer.parseInt(args[2]); System.out.println("Using batch = " + useBatch); System.out.println("Updating while pane visible = " + updateWhileVisible); System.out.println("Strings to insert = " + iterations); JFrame f = new JFrame("Document Speed Test"); f.getContentPane().setLayout(new BorderLayout()); JTextPane jtp = new JTextPane(); f.getContentPane().add( new JScrollPane(jtp), BorderLayout.CENTER); f.setSize(400, 400); f.show(); // Make one of each kind of document. BatchDocument bDoc = new BatchDocument(); DefaultStyledDocument doc = new DefaultStyledDocument(); if (updateWhileVisible) { if (useBatch) jtp.setDocument(bDoc); else jtp.setDocument(doc); } long start = System.currentTimeMillis(); // Make some test data. Normally the text pane // content would come from other source, be parsed, and // have styles applied based on appropriate application // criteria. Here we are interested in the speed of updating // a document, rather than parsing, so we pre-parse the data. String[] str = new String[] { "The ", "quick ", "brown ", "fox ", "jumps ", "over ", "the ", "lazy ", "dog. " }; Color[] colors = new Color[] { Color.red, Color.blue, Color.green }; int[] sizes = new int[] { 10, 14, 12, 9, 16 }; // Add the test repeatedly int offs = 0; int count = 0; SimpleAttributeSet attrs = new SimpleAttributeSet(); for (int i = 0; i < iterations; i++) { for (int j = 0; j < str.length; j++) { // Make some random style changes StyleConstants.setFontSize( attrs, sizes[count % sizes.length]); StyleConstants.setForeground( attrs, colors[count % colors.length]); if (useBatch) bDoc.appendBatchString(str[j], attrs); else doc.insertString(offs, str[j], attrs); // Update out counters count++; offs += str[j].length(); } // Add a linefeed after each instance of the string if (useBatch) bDoc.appendBatchLineFeed(attrs); else doc.insertString(offs, "\n", attrs); offs++; } // If we're testing the batch document, process all // of the updates now. if (useBatch) bDoc.processBatchUpdates(0); System.out.println("Time to update = " + (System.currentTimeMillis() - start)); System.out.println("Text size = " + offs); if (! updateWhileVisible) { if (useBatch) { jtp.setDocument(bDoc); } else { jtp.setDocument(doc); } } } }
The Test
class in Figure 2 should be run with three parameters:
- “
true
” if theBatchDocument
class should be used or “false
” if aDefaultStyledDocument
should be used.
- “
true
” if the updates should be made while the document is attached to a visibleJTextPane
, or “false
” if the updates should be made while the document is unattached.
- The number of times that the test string should be repeated to build the document.
Figure 3 shows some sample results from running the Test
class.
Test | Command line | Time (milliseconds) |
---|---|---|
Default document, updated while visible | java Test false true 10000 | 460827 |
Default document, updated while detached | java Test false false 10000 | 151591 |
Batch document, updated while visible | java Test true true 10000 | 30185 |
Batch document, updated while detached | java Test true false 10000 | 29444 |
Figure 3. Sample results from running the
Test
class.
As we noted in Part I , simply detaching a
DefaultStyledDocument
before inserting the text is roughly three times faster for this particular test. Switching to
BatchDocument
boosts the speed by another
five
times, producing an initialization time that is roughly 15 times faster than initializing a visible
DefaultStyledDocument
. With
BatchDocument
, visibility is less of a factor since only a single UI refresh will be triggered. The difference between the visible vs. detached times for
BatchDocument
shown in the results above is simply “noise”.
Results will, of course, vary considerably based on machine speed and memory, JDK version, document size, and (perhaps most importantly) the complexity of the document content.