Associate native MeasuredText with Java one.
To measure text beforehand, remove static layout dependency from
MeasuredText. Now MeasuredText can compute native measured text
by itself and StaticLayout use it for line breaking.
This CL introduce one additional JNI method call per paragraph during
line breaking but looks negligible cost.
Here is a raw performance test result on walleye-userdebug.
StaticLayoutPerfTest (median, N=100):
createRandom: 7,879,440 -> 7,964,789 (+1.08%)
createRandom Balanced: 7,835,192 -> 7,848,151 (+0.17%)
TextViewOnMeasurePerfTest (median, N=100):
measure_AtMost: 92,599,175 -> 93,027,121 (+0.47%)
measure_Exactly: 89,949,922 -> 90,439,886 (+0.54%)
measure_Unspecified: 148,645,916 -> 150,047,694 (+0.94%)
Bug: 67504091
Test: bit CtsTextTestCases:*
Test: bit CtsWidgetTestCases:*
Test: bit CtsGraphicsTestCases:*
Test: bit FrameworksCoreTests:android.text.StaticLayoutTest
Change-Id: Ie932903845645e50cfa0cb428babb31a44babc47
diff --git a/core/java/android/text/MeasuredText.java b/core/java/android/text/MeasuredText.java
index 71aaf8f..14d6f9e 100644
--- a/core/java/android/text/MeasuredText.java
+++ b/core/java/android/text/MeasuredText.java
@@ -29,6 +29,10 @@
import android.text.style.ReplacementSpan;
import android.util.Pools.SynchronizedPool;
+import dalvik.annotation.optimization.CriticalNative;
+
+import libcore.util.NativeAllocationRegistry;
+
import java.util.Arrays;
/**
@@ -57,6 +61,9 @@
public class MeasuredText {
private static final char OBJECT_REPLACEMENT_CHARACTER = '\uFFFC';
+ private static final NativeAllocationRegistry sRegistry = new NativeAllocationRegistry(
+ MeasuredText.class.getClassLoader(), nGetReleaseFunc(), 1024);
+
private MeasuredText() {} // Use build static functions instead.
private static final SynchronizedPool<MeasuredText> sPool = new SynchronizedPool<>(1);
@@ -119,6 +126,26 @@
// See getFontMetrics comments.
private @Nullable IntArray mFontMetrics = new IntArray(4 * 4);
+ // The native MeasuredText.
+ // See getNativePtr comments.
+ // Do not modify these members directly. Use bindNativeObject/unbindNativeObject instead.
+ private /* Maybe Zero */ long mNativePtr = 0;
+ private @Nullable Runnable mNativeObjectCleaner;
+
+ // Associate the native object to this Java object.
+ private void bindNativeObject(/* Non Zero*/ long nativePtr) {
+ mNativePtr = nativePtr;
+ mNativeObjectCleaner = sRegistry.registerNativeAllocation(this, nativePtr);
+ }
+
+ // Decouple the native object from this Java object and release the native object.
+ private void unbindNativeObject() {
+ if (mNativePtr != 0) {
+ mNativeObjectCleaner.run();
+ mNativePtr = 0;
+ }
+ }
+
// Following two objects are for avoiding object allocation.
private @NonNull TextPaint mCachedPaint = new TextPaint();
private @Nullable Paint.FontMetricsInt mCachedFm;
@@ -145,6 +172,7 @@
mWidths.clear();
mFontMetrics.clear();
mSpanEndCache.clear();
+ unbindNativeObject();
}
/**
@@ -226,6 +254,16 @@
}
/**
+ * Returns the native ptr of the MeasuredText.
+ *
+ * This is available only if the MeasureText is computed with computeForStaticLayout.
+ * Returns 0 in other cases.
+ */
+ public /* Maybe Zero */ long getNativePtr() {
+ return mNativePtr;
+ }
+
+ /**
* Generates new MeasuredText for Bidi computation.
*
* If recycle is null, this returns new instance. If recycle is not null, this fills computed
@@ -308,7 +346,6 @@
* @param start the inclusive start offset of the target region in the text
* @param end the exclusive end offset of the target region in the text
* @param textDir the text direction
- * @param nativeStaticLayoutPtr the pointer to the native static layout object
* @param recycle pass existing MeasuredText if you want to recycle it.
*
* @return measured text
@@ -319,31 +356,46 @@
@IntRange(from = 0) int start,
@IntRange(from = 0) int end,
@NonNull TextDirectionHeuristic textDir,
- /* Non-Zero */ long nativeStaticLayoutPtr,
@Nullable MeasuredText recycle) {
final MeasuredText mt = recycle == null ? obtain() : recycle;
mt.resetAndAnalyzeBidi(text, start, end, textDir);
if (mt.mTextLength == 0) {
+ // Need to build empty native measured text for StaticLayout.
+ // TODO: Stop creating empty measured text for empty lines.
+ long nativeBuilderPtr = nInitBuilder();
+ try {
+ mt.bindNativeObject(nBuildNativeMeasuredText(nativeBuilderPtr, mt.mCopiedBuffer));
+ } finally {
+ nFreeBuilder(nativeBuilderPtr);
+ }
return mt;
}
- if (mt.mSpanned == null) {
- // No style change by MetricsAffectingSpan. Just measure all text.
- mt.applyMetricsAffectingSpan(paint, null /* spans */, start, end,
- nativeStaticLayoutPtr);
- mt.mSpanEndCache.append(end);
- } else {
- // There may be a MetricsAffectingSpan. Split into span transitions and apply styles.
- int spanEnd;
- for (int spanStart = start; spanStart < end; spanStart = spanEnd) {
- spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end, MetricAffectingSpan.class);
- MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
- MetricAffectingSpan.class);
- spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, MetricAffectingSpan.class);
- mt.applyMetricsAffectingSpan(paint, spans, spanStart, spanEnd,
- nativeStaticLayoutPtr);
- mt.mSpanEndCache.append(spanEnd);
+ long nativeBuilderPtr = nInitBuilder();
+ try {
+ if (mt.mSpanned == null) {
+ // No style change by MetricsAffectingSpan. Just measure all text.
+ mt.applyMetricsAffectingSpan(paint, null /* spans */, start, end, nativeBuilderPtr);
+ mt.mSpanEndCache.append(end);
+ } else {
+ // There may be a MetricsAffectingSpan. Split into span transitions and apply
+ // styles.
+ int spanEnd;
+ for (int spanStart = start; spanStart < end; spanStart = spanEnd) {
+ spanEnd = mt.mSpanned.nextSpanTransition(spanStart, end,
+ MetricAffectingSpan.class);
+ MetricAffectingSpan[] spans = mt.mSpanned.getSpans(spanStart, spanEnd,
+ MetricAffectingSpan.class);
+ spans = TextUtils.removeEmptySpans(spans, mt.mSpanned,
+ MetricAffectingSpan.class);
+ mt.applyMetricsAffectingSpan(paint, spans, spanStart, spanEnd,
+ nativeBuilderPtr);
+ mt.mSpanEndCache.append(spanEnd);
+ }
}
+ mt.bindNativeObject(nBuildNativeMeasuredText(nativeBuilderPtr, mt.mCopiedBuffer));
+ } finally {
+ nFreeBuilder(nativeBuilderPtr);
}
return mt;
@@ -415,13 +467,13 @@
private void applyReplacementRun(@NonNull ReplacementSpan replacement,
@IntRange(from = 0) int start, // inclusive, in copied buffer
@IntRange(from = 0) int end, // exclusive, in copied buffer
- /* Maybe Zero */ long nativeStaticLayoutPtr) {
+ /* Maybe Zero */ long nativeBuilderPtr) {
// Use original text. Shouldn't matter.
// TODO: passing uninitizlied FontMetrics to developers. Do we need to keep this for
// backward compatibility? or Should we initialize them for getFontMetricsInt?
final float width = replacement.getSize(
mCachedPaint, mSpanned, start + mTextStart, end + mTextStart, mCachedFm);
- if (nativeStaticLayoutPtr == 0) {
+ if (nativeBuilderPtr == 0) {
// Assigns all width to the first character. This is the same behavior as minikin.
mWidths.set(start, width);
if (end > start + 1) {
@@ -429,25 +481,26 @@
}
mWholeWidth += width;
} else {
- StaticLayout.addReplacementRun(nativeStaticLayoutPtr, mCachedPaint, start, end, width);
+ nAddReplacementRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), start, end,
+ width);
}
}
private void applyStyleRun(@IntRange(from = 0) int start, // inclusive, in copied buffer
@IntRange(from = 0) int end, // exclusive, in copied buffer
- /* Maybe Zero */ long nativeStaticLayoutPtr) {
- if (nativeStaticLayoutPtr != 0) {
+ /* Maybe Zero */ long nativeBuilderPtr) {
+ if (nativeBuilderPtr != 0) {
mCachedPaint.getFontMetricsInt(mCachedFm);
}
if (mLtrWithoutBidi) {
// If the whole text is LTR direction, just apply whole region.
- if (nativeStaticLayoutPtr == 0) {
+ if (nativeBuilderPtr == 0) {
mWholeWidth += mCachedPaint.getTextRunAdvances(
mCopiedBuffer, start, end - start, start, end - start, false /* isRtl */,
mWidths.getRawArray(), start);
} else {
- StaticLayout.addStyleRun(nativeStaticLayoutPtr, mCachedPaint, start, end,
+ nAddStyleRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), start, end,
false /* isRtl */);
}
} else {
@@ -458,14 +511,14 @@
for (int levelStart = start, levelEnd = start + 1;; ++levelEnd) {
if (levelEnd == end || mLevels.get(levelEnd) != level) { // transition point
final boolean isRtl = (level & 0x1) != 0;
- if (nativeStaticLayoutPtr == 0) {
+ if (nativeBuilderPtr == 0) {
final int levelLength = levelEnd - levelStart;
mWholeWidth += mCachedPaint.getTextRunAdvances(
mCopiedBuffer, levelStart, levelLength, levelStart, levelLength,
isRtl, mWidths.getRawArray(), levelStart);
} else {
- StaticLayout.addStyleRun(
- nativeStaticLayoutPtr, mCachedPaint, levelStart, levelEnd, isRtl);
+ nAddStyleRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), levelStart,
+ levelEnd, isRtl);
}
if (levelEnd == end) {
break;
@@ -482,12 +535,12 @@
@Nullable MetricAffectingSpan[] spans,
@IntRange(from = 0) int start, // inclusive, in original text buffer
@IntRange(from = 0) int end, // exclusive, in original text buffer
- /* Maybe Zero */ long nativeStaticLayoutPtr) {
+ /* Maybe Zero */ long nativeBuilderPtr) {
mCachedPaint.set(paint);
// XXX paint should not have a baseline shift, but...
mCachedPaint.baselineShift = 0;
- final boolean needFontMetrics = nativeStaticLayoutPtr != 0;
+ final boolean needFontMetrics = nativeBuilderPtr != 0;
if (needFontMetrics && mCachedFm == null) {
mCachedFm = new Paint.FontMetricsInt();
@@ -512,9 +565,9 @@
if (replacement != null) {
applyReplacementRun(replacement, startInCopiedBuffer, endInCopiedBuffer,
- nativeStaticLayoutPtr);
+ nativeBuilderPtr);
} else {
- applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, nativeStaticLayoutPtr);
+ applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, nativeBuilderPtr);
}
if (needFontMetrics) {
@@ -580,4 +633,44 @@
}
return width;
}
+
+ private static native /* Non Zero */ long nInitBuilder();
+
+ /**
+ * Apply style to make native measured text.
+ *
+ * @param nativeBuilderPtr The native MeasuredText builder pointer.
+ * @param paintPtr The native paint pointer to be applied.
+ * @param start The start offset in the copied buffer.
+ * @param end The end offset in the copied buffer.
+ * @param isRtl True if the text is RTL.
+ */
+ private static native void nAddStyleRun(/* Non Zero */ long nativeBuilderPtr,
+ /* Non Zero */ long paintPtr,
+ @IntRange(from = 0) int start,
+ @IntRange(from = 0) int end,
+ boolean isRtl);
+
+ /**
+ * Apply ReplacementRun to make native measured text.
+ *
+ * @param nativeBuilderPtr The native MeasuredText builder pointer.
+ * @param paintPtr The native paint pointer to be applied.
+ * @param start The start offset in the copied buffer.
+ * @param end The end offset in the copied buffer.
+ * @param width The width of the replacement.
+ */
+ private static native void nAddReplacementRun(/* Non Zero */ long nativeBuilderPtr,
+ /* Non Zero */ long paintPtr,
+ @IntRange(from = 0) int start,
+ @IntRange(from = 0) int end,
+ @FloatRange(from = 0) float width);
+
+ private static native long nBuildNativeMeasuredText(/* Non Zero */ long nativeBuilderPtr,
+ @NonNull char[] text);
+
+ private static native void nFreeBuilder(/* Non Zero */ long nativeBuilderPtr);
+
+ @CriticalNative
+ private static native /* Non Zero */ long nGetReleaseFunc();
}
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
index 81c82c9..400b075 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -48,6 +48,18 @@
* Canvas.drawText()} directly.</p>
*/
public class StaticLayout extends Layout {
+ /*
+ * The break iteration is done in native code. The protocol for using the native code is as
+ * follows.
+ *
+ * First, call nInit to setup native line breaker object. Then, for each paragraph, do the
+ * following:
+ *
+ * - Create MeasuredText by MeasuredText.buildForStaticLayout which measures in native.
+ * - Run nComputeLineBreaks() to obtain line breaks for the paragraph.
+ *
+ * After all paragraphs, call finish() to release expensive buffers.
+ */
static final String TAG = "StaticLayout";
@@ -724,10 +736,11 @@
}
measured = MeasuredText.buildForStaticLayout(
- paint, source, paraStart, paraEnd, textDir, nativePtr, measured);
+ paint, source, paraStart, paraEnd, textDir, measured);
final char[] chs = measured.getChars();
final int[] spanEndCache = measured.getSpanEndCache().getRawArray();
final int[] fmCache = measured.getFontMetrics().getRawArray();
+ // TODO: Stop keeping duplicated width copy in native and Java.
widths.resize(chs.length);
// measurement has to be done before performing line breaking
@@ -739,6 +752,7 @@
// Inputs
chs,
+ measured.getNativePtr(),
paraEnd - paraStart,
firstWidth,
firstWidthLineCount,
@@ -873,18 +887,14 @@
if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE)
&& mLineCount < mMaximumVisibleLineCount) {
- measured = MeasuredText.buildForStaticLayout(
- paint, source, bufEnd, bufEnd, textDir, nativePtr, measured);
-
paint.getFontMetricsInt(fm);
-
v = out(source,
bufEnd, bufEnd, fm.ascent, fm.descent,
fm.top, fm.bottom,
v,
spacingmult, spacingadd, null,
null, fm, 0,
- needMultiply, measured, bufEnd,
+ needMultiply, null, bufEnd,
includepad, trackpad, addLastLineSpacing, null,
null, bufStart, ellipsize,
ellipsizedWidth, 0, paint, false);
@@ -902,7 +912,7 @@
private int out(final CharSequence text, final int start, final int end, int above, int below,
int top, int bottom, int v, final float spacingmult, final float spacingadd,
final LineHeightSpan[] chooseHt, final int[] chooseHtv, final Paint.FontMetricsInt fm,
- final int flags, final boolean needMultiply, final MeasuredText measured,
+ final int flags, final boolean needMultiply, @Nullable final MeasuredText measured,
final int bufEnd, final boolean includePad, final boolean trackPad,
final boolean addLastLineLineSpacing, final char[] chs, final float[] widths,
final int widthStart, final TextUtils.TruncateAt ellipsize, final float ellipsisWidth,
@@ -911,7 +921,7 @@
final int off = j * mColumns;
final int want = off + mColumns + TOP;
int[] lines = mLines;
- final int dir = measured.getParagraphDir();
+ final int dir = (start == end) ? Layout.DIR_LEFT_TO_RIGHT : measured.getParagraphDir();
if (want >= lines.length) {
final int[] grow = ArrayUtils.newUnpaddedIntArray(GrowingArrayUtils.growSize(want));
@@ -938,7 +948,11 @@
lines[off + TAB] |= flags & TAB_MASK;
lines[off + HYPHEN] = flags;
lines[off + DIR] |= dir << DIR_SHIFT;
- mLineDirections[j] = measured.getDirections(start - widthStart, end - widthStart);
+ if (start == end) {
+ mLineDirections[j] = Layout.DIRS_ALL_LEFT_TO_RIGHT;
+ } else {
+ mLineDirections[j] = measured.getDirections(start - widthStart, end - widthStart);
+ }
final boolean firstLine = (j == 0);
final boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
@@ -1415,33 +1429,6 @@
mMaxLineHeight : super.getHeight();
}
- /**
- * Measurement and break iteration is done in native code. The protocol for using
- * the native code is as follows.
- *
- * First, call nInit to setup native line breaker object. Then, for each paragraph, do the
- * following:
- *
- * - Call one of the following methods for each run within the paragraph depending on the type
- * of run:
- * + addStyleRun (a text run, to be measured in native code)
- * + addReplacementRun (a replacement run, width is given)
- *
- * - Run nComputeLineBreaks() to obtain line breaks for the paragraph.
- *
- * After all paragraphs, call finish() to release expensive buffers.
- */
-
- /* package */ static void addStyleRun(long nativePtr, TextPaint paint, int start, int end,
- boolean isRtl) {
- nAddStyleRun(nativePtr, paint.getNativeInstance(), start, end, isRtl);
- }
-
- /* package */ static void addReplacementRun(long nativePtr, TextPaint paint, int start, int end,
- float width) {
- nAddReplacementRun(nativePtr, paint.getNativeInstance(), start, end, width);
- }
-
@FastNative
private static native long nInit(
@BreakStrategy int breakStrategy,
@@ -1454,17 +1441,6 @@
@CriticalNative
private static native void nFinish(long nativePtr);
- @CriticalNative
- private static native void nAddStyleRun(
- /* non-zero */ long nativePtr, /* non-zero */ long nativePaint,
- @IntRange(from = 0) int start, @IntRange(from = 0) int end, boolean isRtl);
-
- @CriticalNative
- private static native void nAddReplacementRun(
- /* non-zero */ long nativePtr, /* non-zero */ long nativePaint,
- @IntRange(from = 0) int start, @IntRange(from = 0) int end,
- @FloatRange(from = 0.0f) float width);
-
// populates LineBreaks and returns the number of breaks found
//
// the arrays inside the LineBreaks objects are passed in as well
@@ -1477,6 +1453,7 @@
// Inputs
@NonNull char[] text,
+ /* Non Zero */ long measuredTextPtr,
@IntRange(from = 0) int length,
@FloatRange(from = 0.0f) float firstWidth,
@IntRange(from = 0) int firstWidthLineCount,