Skip to content

Commit 945ee9b

Browse files
author
Gilles Debunne
committed
Bug 5250788: TextView gets slower as the text length grows
getSpans was called too many times in handleRun. Pre-compute the subset of intersected spans and iterate over a subset of it instead. Moving the instanceof test in getSpans after the other tests also speeds things up a lot. On a text with ~300 words, all with a span attached, getSpans went down from 78% to 14% of the CPU usage. Change-Id: I59bc44f610e9a548e0dcec68b180934da9e5c559
1 parent 5840639 commit 945ee9b

File tree

2 files changed

+120
-47
lines changed

2 files changed

+120
-47
lines changed

core/java/android/text/SpannableStringBuilder.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -709,8 +709,6 @@ public <T> T[] getSpans(int queryStart, int queryEnd, Class<T> kind) {
709709
T ret1 = null;
710710

711711
for (int i = 0; i < spanCount; i++) {
712-
if (!kind.isInstance(spans[i])) continue;
713-
714712
int spanStart = starts[i];
715713
int spanEnd = ends[i];
716714

@@ -735,6 +733,9 @@ public <T> T[] getSpans(int queryStart, int queryEnd, Class<T> kind) {
735733
continue;
736734
}
737735

736+
// Expensive test, should be performed after the previous tests
737+
if (!kind.isInstance(spans[i])) continue;
738+
738739
if (count == 0) {
739740
// Safe conversion thanks to the isInstance test above
740741
ret1 = (T) spans[i];

core/java/android/text/TextLine.java

Lines changed: 117 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030

3131
import com.android.internal.util.ArrayUtils;
3232

33+
import java.lang.reflect.Array;
34+
3335
/**
3436
* Represents a line of styled text, for measuring in visual order and
3537
* for rendering.
@@ -823,6 +825,73 @@ private float handleReplacement(ReplacementSpan replacement, TextPaint wp,
823825
return runIsRtl ? -ret : ret;
824826
}
825827

828+
private static class SpanSet<E> {
829+
final int numberOfSpans;
830+
final E[] spans;
831+
final int[] spanStarts;
832+
final int[] spanEnds;
833+
final int[] spanFlags;
834+
835+
@SuppressWarnings("unchecked")
836+
SpanSet(Spanned spanned, int start, int limit, Class<? extends E> type) {
837+
final E[] allSpans = spanned.getSpans(start, limit, type);
838+
final int length = allSpans.length;
839+
// These arrays may end up being too large because of empty spans
840+
spans = (E[]) Array.newInstance(type, length);
841+
spanStarts = new int[length];
842+
spanEnds = new int[length];
843+
spanFlags = new int[length];
844+
845+
int count = 0;
846+
for (int i = 0; i < length; i++) {
847+
final E span = allSpans[i];
848+
849+
final int spanStart = spanned.getSpanStart(span);
850+
final int spanEnd = spanned.getSpanEnd(span);
851+
if (spanStart == spanEnd) continue;
852+
853+
final int spanFlag = spanned.getSpanFlags(span);
854+
final int priority = spanFlag & Spanned.SPAN_PRIORITY;
855+
if (priority != 0 && count != 0) {
856+
int j;
857+
858+
for (j = 0; j < count; j++) {
859+
final int otherPriority = spanFlags[j] & Spanned.SPAN_PRIORITY;
860+
if (priority > otherPriority) break;
861+
}
862+
863+
System.arraycopy(spans, j, spans, j + 1, count - j);
864+
System.arraycopy(spanStarts, j, spanStarts, j + 1, count - j);
865+
System.arraycopy(spanEnds, j, spanEnds, j + 1, count - j);
866+
System.arraycopy(spanFlags, j, spanFlags, j + 1, count - j);
867+
868+
spans[j] = span;
869+
spanStarts[j] = spanStart;
870+
spanEnds[j] = spanEnd;
871+
spanFlags[j] = spanFlag;
872+
} else {
873+
spans[i] = span;
874+
spanStarts[i] = spanStart;
875+
spanEnds[i] = spanEnd;
876+
spanFlags[i] = spanFlag;
877+
}
878+
879+
count++;
880+
}
881+
numberOfSpans = count;
882+
}
883+
884+
int getNextTransition(int start, int limit) {
885+
for (int i = 0; i < numberOfSpans; i++) {
886+
final int spanStart = spanStarts[i];
887+
final int spanEnd = spanEnds[i];
888+
if (spanStart > start && spanStart < limit) limit = spanStart;
889+
if (spanEnd > start && spanEnd < limit) limit = spanEnd;
890+
}
891+
return limit;
892+
}
893+
}
894+
826895
/**
827896
* Utility function for handling a unidirectional run. The run must not
828897
* contain tabs or emoji but can contain styles.
@@ -856,66 +925,70 @@ private float handleRun(int start, int measureLimit,
856925
return 0f;
857926
}
858927

928+
if (mSpanned == null) {
929+
TextPaint wp = mWorkPaint;
930+
wp.set(mPaint);
931+
final int mlimit = measureLimit;
932+
return handleText(wp, start, mlimit, start, limit, runIsRtl, c, x, top,
933+
y, bottom, fmi, needWidth || mlimit < measureLimit);
934+
}
935+
936+
final SpanSet<MetricAffectingSpan> metricAffectingSpans = new SpanSet<MetricAffectingSpan>(
937+
mSpanned, mStart + start, mStart + limit, MetricAffectingSpan.class);
938+
final SpanSet<CharacterStyle> characterStyleSpans = new SpanSet<CharacterStyle>(
939+
mSpanned, mStart + start, mStart + limit, CharacterStyle.class);
940+
859941
// Shaping needs to take into account context up to metric boundaries,
860942
// but rendering needs to take into account character style boundaries.
861943
// So we iterate through metric runs to get metric bounds,
862944
// then within each metric run iterate through character style runs
863945
// for the run bounds.
864-
float ox = x;
946+
final float originalX = x;
865947
for (int i = start, inext; i < measureLimit; i = inext) {
866948
TextPaint wp = mWorkPaint;
867949
wp.set(mPaint);
868950

869-
int mlimit;
870-
if (mSpanned == null) {
871-
inext = limit;
872-
mlimit = measureLimit;
873-
} else {
874-
inext = mSpanned.nextSpanTransition(mStart + i, mStart + limit,
875-
MetricAffectingSpan.class) - mStart;
876-
877-
mlimit = inext < measureLimit ? inext : measureLimit;
878-
MetricAffectingSpan[] spans = mSpanned.getSpans(mStart + i,
879-
mStart + mlimit, MetricAffectingSpan.class);
880-
spans = TextUtils.removeEmptySpans(spans, mSpanned, MetricAffectingSpan.class);
881-
882-
if (spans.length > 0) {
883-
ReplacementSpan replacement = null;
884-
for (int j = 0; j < spans.length; j++) {
885-
MetricAffectingSpan span = spans[j];
886-
if (span instanceof ReplacementSpan) {
887-
replacement = (ReplacementSpan)span;
888-
} else {
889-
// We might have a replacement that uses the draw
890-
// state, otherwise measure state would suffice.
891-
span.updateDrawState(wp);
892-
}
893-
}
894-
895-
if (replacement != null) {
896-
x += handleReplacement(replacement, wp, i,
897-
mlimit, runIsRtl, c, x, top, y, bottom, fmi,
898-
needWidth || mlimit < measureLimit);
899-
continue;
900-
}
951+
inext = metricAffectingSpans.getNextTransition(mStart + i, mStart + limit) - mStart;
952+
int mlimit = Math.min(inext, measureLimit);
953+
954+
ReplacementSpan replacement = null;
955+
956+
for (int j = 0; j < metricAffectingSpans.numberOfSpans; j++) {
957+
// Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT
958+
// empty by construction. This special case in getSpans() explains the >= & <= tests
959+
if ((metricAffectingSpans.spanStarts[j] >= mStart + mlimit) ||
960+
(metricAffectingSpans.spanEnds[j] <= mStart + i)) continue;
961+
MetricAffectingSpan span = metricAffectingSpans.spans[j];
962+
if (span instanceof ReplacementSpan) {
963+
replacement = (ReplacementSpan)span;
964+
} else {
965+
// We might have a replacement that uses the draw
966+
// state, otherwise measure state would suffice.
967+
span.updateDrawState(wp);
901968
}
902969
}
903970

904-
if (mSpanned == null || c == null) {
971+
if (replacement != null) {
972+
x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,
973+
bottom, fmi, needWidth || mlimit < measureLimit);
974+
continue;
975+
}
976+
977+
if (c == null) {
905978
x += handleText(wp, i, mlimit, i, inext, runIsRtl, c, x, top,
906979
y, bottom, fmi, needWidth || mlimit < measureLimit);
907980
} else {
908981
for (int j = i, jnext; j < mlimit; j = jnext) {
909-
jnext = mSpanned.nextSpanTransition(mStart + j,
910-
mStart + mlimit, CharacterStyle.class) - mStart;
911-
912-
CharacterStyle[] spans = mSpanned.getSpans(mStart + j,
913-
mStart + jnext, CharacterStyle.class);
914-
spans = TextUtils.removeEmptySpans(spans, mSpanned, CharacterStyle.class);
982+
jnext = characterStyleSpans.getNextTransition(mStart + j, mStart + mlimit) -
983+
mStart;
915984

916985
wp.set(mPaint);
917-
for (int k = 0; k < spans.length; k++) {
918-
CharacterStyle span = spans[k];
986+
for (int k = 0; k < characterStyleSpans.numberOfSpans; k++) {
987+
// Intentionally using >= and <= as explained above
988+
if ((characterStyleSpans.spanStarts[k] >= mStart + jnext) ||
989+
(characterStyleSpans.spanEnds[k] <= mStart + j)) continue;
990+
991+
CharacterStyle span = characterStyleSpans.spans[k];
919992
span.updateDrawState(wp);
920993
}
921994

@@ -925,7 +998,7 @@ private float handleRun(int start, int measureLimit,
925998
}
926999
}
9271000

928-
return x - ox;
1001+
return x - originalX;
9291002
}
9301003

9311004
/**
@@ -970,8 +1043,7 @@ float ascent(int pos) {
9701043
}
9711044

9721045
pos += mStart;
973-
MetricAffectingSpan[] spans = mSpanned.getSpans(pos, pos + 1,
974-
MetricAffectingSpan.class);
1046+
MetricAffectingSpan[] spans = mSpanned.getSpans(pos, pos + 1, MetricAffectingSpan.class);
9751047
if (spans.length == 0) {
9761048
return mPaint.ascent();
9771049
}

0 commit comments

Comments
 (0)