In the official API, Android developers are supplied with some simple text widgets for the purpose of displaying to and interacting with users such as EditText. Styling text is achieved by preparing the display text with instances of SpannableStringBuilder and setting the styled region (i.e. spanning) with objects in package android.tex.style.
Unfortunately, naive uses of the API will not solve the syntax highlighting
problem satisfactorily. The reason is the lack of application responsiveness when the source is too long.
After reading the related source code
(mainly packages android.text.*
and android.widgets.*
) and using traceview
profiling tool, I can pinpoint the source of such unresponsiveness. Let me
abridge the detail in the following paragraphs.
Traceview shows that most of the CPU time is for rendering (i.e. drawing) the text which eventually is handled by Layout object (for editable text, the instance used is actually of subclass DynamicLayout).
The app always lags right after user edits texts with long content with too
many styles. Logically, when edit occurs, Android must have a mechanism to redraw
the display. In GUI-term, it is usually done by notifying the view. My plausible
guess is that too much redraw
requests is performed. Digging deeper, one
realizes that when user edits the content,
the underlying content is updated via replace
method, for example, of
SpannableStringBuilder (let me skip
the full mechanism here which involves complicated way to handle soft vs. hard
keyboard).
A close investigation of SpannableStringBuilder source code shows
that instances of TextWatcher
and SpanWatcher
that is previously bound will be
notified. There are a number of implementation for these interfaces and the most
important one is implemented as an internal class of DynamicLayout which
basically invokes its reflow(CharSequence s, int where, int before, int after)
method to redraw the text. SpannableStringBuilder's method which performs
notification is sendToSpanWatchers(int replaceStart, int replaceEnd, int nbNewChars)
whose implementation iterates through every single span! BINGO! This explains why
refreshing is slow when there are lots of styling elements applied.
In fact, looking at other methods of SpannableStringBuilder, one easily picked up lots of loop-through-all-spans blocks.
(At first, I think it is stupid; but then I realize that that is the only thing they can do. Think of the situation when there are overlapping styles in which case the lastly added style must have the priority.)
Beside this responsiveness issue, the OS imposes certain restrictions such as 16/24/32MB app heap memory limit which I discovered earlier. This whole discussion is only meant to prove that:
Writing syntax highlighter in Android is HARD.
We have seen that unresponsiveness come from blindly iterate through all styling objects applied to the text after each edit. So there are at least two ways to deal with this inherent inefficiency:
- Limit the number of possible iterations. In particular, since the screen is not big enough to contain all the text, we can dynamically add and remove the styles based on the currently displayed content. When user navigate (scroll, arrow key, etc.), shift the focusing area appropriately.
- Overriding the behavior so that it does not loop through all styling elements whenever the content gets changed. In other words, don't use the API the naive way.
The first solution is used by many editors such as DroidEdit. The evidence is that when we scroll a big chunk of text, the unshown content will be highlighted after a noticeable delay. Certainly, this only works if the displayed region does not already contain a large amount of highlights.
As far as I know, I only believe that the second solution is only used in Jota+. To test it, just load a big text file and scroll through it, there is no noticeable highlighting delay: the styles are already there.
I devise a solution using the second approach. But this is a long story and I shall describe in another document. (After I implemented this, I realize that Jota+ claim that it supports 1 million characters should be understood as an upper limit: it cannot support very much bigger content than that stated amount.)