Reorganize JNI in StaticLayout

This CL reorganize the JNI strategy as follows:
- Remove nNewBuilder/nFreeBuilder.
- Make addStyleRun/addReplacementRun CriticalNative.
- Remove nSetupParagraph and pass necessary arguments to
  nComputeLineBreaks instead.

Here is a performance scores: (w/o patch -> w/ patch)

StaticLayoutPerfTest (median):
createRandom:           3,755,090 -> 3,781,394 (+0.70%)
createRandom Balanced:  3,702,837 -> 3,730,435 (+0.74%)

TextViewOnMeasurePerfTest (median):
measure_AtMost:        39,172,360 -> 35,883,014 (-8.4%)
measure_Exactly:       38,005,066 -> 34,585,052 (-9.0%)
measure_Unspecified:   67,191,780 -> 63,100,545 (-6.1%)

Bug: 65024629
Test: bit CtsTextTestCases:*
Test: bit CtsWidgetTestCases:*
Change-Id: If9db1fdd2b03a1cf240f87322c1e852cf8085fff
diff --git a/core/java/android/text/Layout.java b/core/java/android/text/Layout.java
index ac5c2e9..4d2a962 100644
--- a/core/java/android/text/Layout.java
+++ b/core/java/android/text/Layout.java
@@ -1910,7 +1910,7 @@
         MeasuredText mt = MeasuredText.obtain();
         TextLine tl = TextLine.obtain();
         try {
-            mt.setPara(text, start, end, textDir, null);
+            mt.setPara(text, start, end, textDir);
             Directions directions;
             int dir;
             if (mt.mEasy) {
diff --git a/core/java/android/text/MeasuredText.java b/core/java/android/text/MeasuredText.java
index ffc44a7..3d9fba7 100644
--- a/core/java/android/text/MeasuredText.java
+++ b/core/java/android/text/MeasuredText.java
@@ -39,7 +39,6 @@
 
     private int mPos;
     private TextPaint mWorkPaint;
-    private StaticLayout.Builder mBuilder;
 
     private MeasuredText() {
         mWorkPaint = new TextPaint();
@@ -82,7 +81,6 @@
 
     void finish() {
         mText = null;
-        mBuilder = null;
         if (mLen > 1000) {
             mWidths = null;
             mChars = null;
@@ -93,9 +91,7 @@
     /**
      * Analyzes text for bidirectional runs.  Allocates working buffers.
      */
-    void setPara(CharSequence text, int start, int end, TextDirectionHeuristic textDir,
-            StaticLayout.Builder builder) {
-        mBuilder = builder;
+    void setPara(CharSequence text, int start, int end, TextDirectionHeuristic textDir) {
         mText = text;
         mTextStart = start;
 
@@ -159,12 +155,12 @@
     /**
      * Apply the style.
      *
-     * If StaticLyaout.Builder is not provided in setPara() method, this method measures the styled
-     * text width.
-     * If StaticLayout.Builder is provided in setPara() method, this method just passes the style
-     * information to native code by calling StaticLayout.Builder.addstyleRun() and returns 0.
+     * If nativeStaticLayoutPtr is 0, this method measures the styled text width.
+     * If nativeStaticLayoutPtr is not 0, this method just passes the style information to native
+     * code by calling StaticLayout.addstyleRun() and returns 0.
      */
-    float addStyleRun(TextPaint paint, int len, Paint.FontMetricsInt fm) {
+    float addStyleRun(TextPaint paint, int len, Paint.FontMetricsInt fm,
+            long nativeStaticLayoutPtr) {
         if (fm != null) {
             paint.getFontMetricsInt(fm);
         }
@@ -174,10 +170,10 @@
 
         if (mEasy) {
             final boolean isRtl = mDir != Layout.DIR_LEFT_TO_RIGHT;
-            if (mBuilder == null) {
+            if (nativeStaticLayoutPtr == 0) {
                 return paint.getTextRunAdvances(mChars, p, len, p, len, isRtl, mWidths, p);
             } else {
-                mBuilder.addStyleRun(paint, p, p + len, isRtl);
+                StaticLayout.addStyleRun(nativeStaticLayoutPtr, paint, p, p + len, isRtl);
                 return 0.0f;  // Builder.addStyleRun doesn't return the width.
             }
         }
@@ -187,12 +183,12 @@
         for (int q = p, i = p + 1, e = p + len;; ++i) {
             if (i == e || mLevels[i] != level) {
                 final boolean isRtl = (level & 0x1) != 0;
-                if (mBuilder == null) {
+                if (nativeStaticLayoutPtr == 0) {
                     totalAdvance +=
                             paint.getTextRunAdvances(mChars, q, i - q, q, i - q, isRtl, mWidths, q);
                 } else {
                     // Builder.addStyleRun doesn't return the width.
-                    mBuilder.addStyleRun(paint, q, i, isRtl);
+                    StaticLayout.addStyleRun(nativeStaticLayoutPtr, paint, q, i, isRtl);
                 }
                 if (i == e) {
                     break;
@@ -201,11 +197,15 @@
                 level = mLevels[i];
             }
         }
-        return totalAdvance;  // If mBuilder is null, the result is zero.
+        return totalAdvance;  // If nativeStaticLayoutPtr is 0, the result is zero.
+    }
+
+    float addStyleRun(TextPaint paint, int len, Paint.FontMetricsInt fm) {
+        return addStyleRun(paint, len, fm, 0 /* native ptr */);
     }
 
     float addStyleRun(TextPaint paint, MetricAffectingSpan[] spans, int len,
-            Paint.FontMetricsInt fm) {
+            Paint.FontMetricsInt fm, long nativeStaticLayoutPtr) {
 
         TextPaint workPaint = mWorkPaint;
         workPaint.set(paint);
@@ -224,18 +224,18 @@
 
         float wid;
         if (replacement == null) {
-            wid = addStyleRun(workPaint, len, fm);
+            wid = addStyleRun(workPaint, len, fm, nativeStaticLayoutPtr);
         } else {
             // Use original text.  Shouldn't matter.
             wid = replacement.getSize(workPaint, mText, mTextStart + mPos,
                     mTextStart + mPos + len, fm);
-            if (mBuilder == null) {
+            if (nativeStaticLayoutPtr == 0) {
                 float[] w = mWidths;
                 w[mPos] = wid;
                 for (int i = mPos + 1, e = mPos + len; i < e; i++)
                     w[i] = 0;
             } else {
-                mBuilder.addReplacementRun(paint, mPos, mPos + len, wid);
+                StaticLayout.addReplacementRun(nativeStaticLayoutPtr, paint, mPos, mPos + len, wid);
             }
             mPos += len;
         }
@@ -253,6 +253,11 @@
         return wid;
     }
 
+    float addStyleRun(TextPaint paint, MetricAffectingSpan[] spans, int len,
+            Paint.FontMetricsInt fm) {
+        return addStyleRun(paint, spans, len, fm, 0 /* native ptr */);
+    }
+
     int breakText(int limit, boolean forwards, float width) {
         float[] w = mWidths;
         if (forwards) {
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
index 5c60188..c0fc44f 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -32,6 +32,9 @@
 import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.GrowingArrayUtils;
 
+import dalvik.annotation.optimization.CriticalNative;
+import dalvik.annotation.optimization.FastNative;
+
 import java.util.Arrays;
 
 /**
@@ -57,9 +60,7 @@
      * default values.
      */
     public final static class Builder {
-        private Builder() {
-            mNativePtr = nNewBuilder();
-        }
+        private Builder() {}
 
         /**
          * Obtain a builder for constructing StaticLayout objects.
@@ -116,13 +117,11 @@
             b.mRightIndents = null;
             b.mLeftPaddings = null;
             b.mRightPaddings = null;
-            nFinishBuilder(b.mNativePtr);
             sPool.release(b);
         }
 
         // release any expensive state
         /* package */ void finish() {
-            nFinishBuilder(mNativePtr);
             mText = null;
             mPaint = null;
             mLeftIndents = null;
@@ -405,32 +404,6 @@
         }
 
         /**
-         * Measurement and break iteration is done in native code. The protocol for using
-         * the native code is as follows.
-         *
-         * For each paragraph, do a nSetupParagraph, which sets paragraph text, line width, tab
-         * stops, break strategy, and hyphenation frequency (and possibly other parameters in the
-         * future).
-         *
-         * Then, for each run within the paragraph:
-         *  - one of the following, 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 */ void addStyleRun(TextPaint paint, int start, int end, boolean isRtl) {
-            nAddStyleRun(mNativePtr, paint.getNativeInstance(), start, end, isRtl);
-        }
-
-        /* package */ void addReplacementRun(TextPaint paint, int start, int end, float width) {
-            nAddReplacementRun(mNativePtr, paint.getNativeInstance(), start, end, width);
-        }
-
-        /**
          * Build the {@link StaticLayout} after options have been set.
          *
          * <p>Note: the builder object must not be reused in any way after calling this
@@ -446,17 +419,6 @@
             return result;
         }
 
-        @Override
-        protected void finalize() throws Throwable {
-            try {
-                nFreeBuilder(mNativePtr);
-            } finally {
-                super.finalize();
-            }
-        }
-
-        /* package */ long mNativePtr;
-
         private CharSequence mText;
         private int mStart;
         private int mEnd;
@@ -694,270 +656,294 @@
             indents = null;
         }
 
-        int paraEnd;
-        for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) {
-            paraEnd = TextUtils.indexOf(source, CHAR_NEW_LINE, paraStart, bufEnd);
-            if (paraEnd < 0)
-                paraEnd = bufEnd;
-            else
-                paraEnd++;
+        final long nativePtr = nInit(
+                b.mBreakStrategy, b.mHyphenationFrequency,
+                // TODO: Support more justification mode, e.g. letter spacing, stretching.
+                b.mJustificationMode != Layout.JUSTIFICATION_MODE_NONE,
+                indents, mLeftPaddings, mRightPaddings);
 
-            int firstWidthLineCount = 1;
-            int firstWidth = outerWidth;
-            int restWidth = outerWidth;
+        try {
+            int paraEnd;
+            for (int paraStart = bufStart; paraStart <= bufEnd; paraStart = paraEnd) {
+                paraEnd = TextUtils.indexOf(source, CHAR_NEW_LINE, paraStart, bufEnd);
+                if (paraEnd < 0) {
+                    paraEnd = bufEnd;
+                } else {
+                    paraEnd++;
+                }
 
-            LineHeightSpan[] chooseHt = null;
+                int firstWidthLineCount = 1;
+                int firstWidth = outerWidth;
+                int restWidth = outerWidth;
 
-            if (spanned != null) {
-                LeadingMarginSpan[] sp = getParagraphSpans(spanned, paraStart, paraEnd,
-                        LeadingMarginSpan.class);
-                for (int i = 0; i < sp.length; i++) {
-                    LeadingMarginSpan lms = sp[i];
-                    firstWidth -= sp[i].getLeadingMargin(true);
-                    restWidth -= sp[i].getLeadingMargin(false);
+                LineHeightSpan[] chooseHt = null;
 
-                    // LeadingMarginSpan2 is odd.  The count affects all
-                    // leading margin spans, not just this particular one
-                    if (lms instanceof LeadingMarginSpan2) {
-                        LeadingMarginSpan2 lms2 = (LeadingMarginSpan2) lms;
-                        firstWidthLineCount = Math.max(firstWidthLineCount,
-                                lms2.getLeadingMarginLineCount());
+                if (spanned != null) {
+                    LeadingMarginSpan[] sp = getParagraphSpans(spanned, paraStart, paraEnd,
+                            LeadingMarginSpan.class);
+                    for (int i = 0; i < sp.length; i++) {
+                        LeadingMarginSpan lms = sp[i];
+                        firstWidth -= sp[i].getLeadingMargin(true);
+                        restWidth -= sp[i].getLeadingMargin(false);
+
+                        // LeadingMarginSpan2 is odd.  The count affects all
+                        // leading margin spans, not just this particular one
+                        if (lms instanceof LeadingMarginSpan2) {
+                            LeadingMarginSpan2 lms2 = (LeadingMarginSpan2) lms;
+                            firstWidthLineCount = Math.max(firstWidthLineCount,
+                                    lms2.getLeadingMarginLineCount());
+                        }
+                    }
+
+                    chooseHt = getParagraphSpans(spanned, paraStart, paraEnd, LineHeightSpan.class);
+
+                    if (chooseHt.length == 0) {
+                        chooseHt = null; // So that out() would not assume it has any contents
+                    } else {
+                        if (chooseHtv == null || chooseHtv.length < chooseHt.length) {
+                            chooseHtv = ArrayUtils.newUnpaddedIntArray(chooseHt.length);
+                        }
+
+                        for (int i = 0; i < chooseHt.length; i++) {
+                            int o = spanned.getSpanStart(chooseHt[i]);
+
+                            if (o < paraStart) {
+                                // starts in this layout, before the
+                                // current paragraph
+
+                                chooseHtv[i] = getLineTop(getLineForOffset(o));
+                            } else {
+                                // starts in this paragraph
+
+                                chooseHtv[i] = v;
+                            }
+                        }
                     }
                 }
 
-                chooseHt = getParagraphSpans(spanned, paraStart, paraEnd, LineHeightSpan.class);
+                measured.setPara(source, paraStart, paraEnd, textDir);
+                char[] chs = measured.mChars;
+                float[] widths = measured.mWidths;
+                byte[] chdirs = measured.mLevels;
+                int dir = measured.mDir;
+                boolean easy = measured.mEasy;
 
-                if (chooseHt.length == 0) {
-                    chooseHt = null; // So that out() would not assume it has any contents
-                } else {
-                    if (chooseHtv == null ||
-                        chooseHtv.length < chooseHt.length) {
-                        chooseHtv = ArrayUtils.newUnpaddedIntArray(chooseHt.length);
+                // tab stop locations
+                int[] variableTabStops = null;
+                if (spanned != null) {
+                    TabStopSpan[] spans = getParagraphSpans(spanned, paraStart,
+                            paraEnd, TabStopSpan.class);
+                    if (spans.length > 0) {
+                        int[] stops = new int[spans.length];
+                        for (int i = 0; i < spans.length; i++) {
+                            stops[i] = spans[i].getTabStop();
+                        }
+                        Arrays.sort(stops, 0, stops.length);
+                        variableTabStops = stops;
+                    }
+                }
+
+                // measurement has to be done before performing line breaking
+                // but we don't want to recompute fontmetrics or span ranges the
+                // second time, so we cache those and then use those stored values
+                int fmCacheCount = 0;
+                int spanEndCacheCount = 0;
+                for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
+                    if (fmCacheCount * 4 >= fmCache.length) {
+                        int[] grow = new int[fmCacheCount * 4 * 2];
+                        System.arraycopy(fmCache, 0, grow, 0, fmCacheCount * 4);
+                        fmCache = grow;
                     }
 
-                    for (int i = 0; i < chooseHt.length; i++) {
-                        int o = spanned.getSpanStart(chooseHt[i]);
+                    if (spanEndCacheCount >= spanEndCache.length) {
+                        int[] grow = new int[spanEndCacheCount * 2];
+                        System.arraycopy(spanEndCache, 0, grow, 0, spanEndCacheCount);
+                        spanEndCache = grow;
+                    }
 
-                        if (o < paraStart) {
-                            // starts in this layout, before the
-                            // current paragraph
+                    if (spanned == null) {
+                        spanEnd = paraEnd;
+                        int spanLen = spanEnd - spanStart;
+                        measured.addStyleRun(paint, spanLen, fm, nativePtr);
+                    } else {
+                        spanEnd = spanned.nextSpanTransition(spanStart, paraEnd,
+                                MetricAffectingSpan.class);
+                        int spanLen = spanEnd - spanStart;
+                        MetricAffectingSpan[] spans =
+                                spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class);
+                        spans = TextUtils.removeEmptySpans(spans, spanned,
+                                MetricAffectingSpan.class);
+                        measured.addStyleRun(paint, spans, spanLen, fm, nativePtr);
+                    }
 
-                            chooseHtv[i] = getLineTop(getLineForOffset(o));
+                    // the order of storage here (top, bottom, ascent, descent) has to match the
+                    // code below where these values are retrieved
+                    fmCache[fmCacheCount * 4 + 0] = fm.top;
+                    fmCache[fmCacheCount * 4 + 1] = fm.bottom;
+                    fmCache[fmCacheCount * 4 + 2] = fm.ascent;
+                    fmCache[fmCacheCount * 4 + 3] = fm.descent;
+                    fmCacheCount++;
+
+                    spanEndCache[spanEndCacheCount] = spanEnd;
+                    spanEndCacheCount++;
+                }
+
+                int breakCount = nComputeLineBreaks(
+                        nativePtr,
+
+                        // Inputs
+                        chs,
+                        paraEnd - paraStart,
+                        firstWidth,
+                        firstWidthLineCount,
+                        restWidth,
+                        variableTabStops,
+                        TAB_INCREMENT,
+                        mLineCount,
+
+                        // Outputs
+                        lineBreaks,
+                        lineBreaks.breaks.length,
+                        lineBreaks.breaks,
+                        lineBreaks.widths,
+                        lineBreaks.ascents,
+                        lineBreaks.descents,
+                        lineBreaks.flags,
+                        widths);
+
+                final int[] breaks = lineBreaks.breaks;
+                final float[] lineWidths = lineBreaks.widths;
+                final float[] ascents = lineBreaks.ascents;
+                final float[] descents = lineBreaks.descents;
+                final int[] flags = lineBreaks.flags;
+
+                final int remainingLineCount = mMaximumVisibleLineCount - mLineCount;
+                final boolean ellipsisMayBeApplied = ellipsize != null
+                        && (ellipsize == TextUtils.TruncateAt.END
+                            || (mMaximumVisibleLineCount == 1
+                                    && ellipsize != TextUtils.TruncateAt.MARQUEE));
+                if (0 < remainingLineCount && remainingLineCount < breakCount
+                        && ellipsisMayBeApplied) {
+                    // Calculate width and flag.
+                    float width = 0;
+                    int flag = 0; // XXX May need to also have starting hyphen edit
+                    for (int i = remainingLineCount - 1; i < breakCount; i++) {
+                        if (i == breakCount - 1) {
+                            width += lineWidths[i];
                         } else {
-                            // starts in this paragraph
-
-                            chooseHtv[i] = v;
+                            for (int j = (i == 0 ? 0 : breaks[i - 1]); j < breaks[i]; j++) {
+                                width += widths[j];
+                            }
                         }
+                        flag |= flags[i] & TAB_MASK;
                     }
-                }
-            }
+                    // Treat the last line and overflowed lines as a single line.
+                    breaks[remainingLineCount - 1] = breaks[breakCount - 1];
+                    lineWidths[remainingLineCount - 1] = width;
+                    flags[remainingLineCount - 1] = flag;
 
-            measured.setPara(source, paraStart, paraEnd, textDir, b);
-            char[] chs = measured.mChars;
-            float[] widths = measured.mWidths;
-            byte[] chdirs = measured.mLevels;
-            int dir = measured.mDir;
-            boolean easy = measured.mEasy;
-
-            // tab stop locations
-            int[] variableTabStops = null;
-            if (spanned != null) {
-                TabStopSpan[] spans = getParagraphSpans(spanned, paraStart,
-                        paraEnd, TabStopSpan.class);
-                if (spans.length > 0) {
-                    int[] stops = new int[spans.length];
-                    for (int i = 0; i < spans.length; i++) {
-                        stops[i] = spans[i].getTabStop();
-                    }
-                    Arrays.sort(stops, 0, stops.length);
-                    variableTabStops = stops;
-                }
-            }
-
-            nSetupParagraph(b.mNativePtr, chs, paraEnd - paraStart,
-                    firstWidth, firstWidthLineCount, restWidth,
-                    variableTabStops, TAB_INCREMENT, b.mBreakStrategy, b.mHyphenationFrequency,
-                    // TODO: Support more justification mode, e.g. letter spacing, stretching.
-                    b.mJustificationMode != Layout.JUSTIFICATION_MODE_NONE,
-                    // TODO: indents and paddings don't need to get passed to native code for every
-                    // paragraph. Pass them to native code just once.
-                    indents, mLeftPaddings, mRightPaddings, mLineCount);
-
-            // measurement has to be done before performing line breaking
-            // but we don't want to recompute fontmetrics or span ranges the
-            // second time, so we cache those and then use those stored values
-            int fmCacheCount = 0;
-            int spanEndCacheCount = 0;
-            for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
-                if (fmCacheCount * 4 >= fmCache.length) {
-                    int[] grow = new int[fmCacheCount * 4 * 2];
-                    System.arraycopy(fmCache, 0, grow, 0, fmCacheCount * 4);
-                    fmCache = grow;
+                    breakCount = remainingLineCount;
                 }
 
-                if (spanEndCacheCount >= spanEndCache.length) {
-                    int[] grow = new int[spanEndCacheCount * 2];
-                    System.arraycopy(spanEndCache, 0, grow, 0, spanEndCacheCount);
-                    spanEndCache = grow;
-                }
+                // here is the offset of the starting character of the line we are currently
+                // measuring
+                int here = paraStart;
 
-                if (spanned == null) {
-                    spanEnd = paraEnd;
-                    int spanLen = spanEnd - spanStart;
-                    measured.addStyleRun(paint, spanLen, fm);
-                } else {
-                    spanEnd = spanned.nextSpanTransition(spanStart, paraEnd,
-                            MetricAffectingSpan.class);
-                    int spanLen = spanEnd - spanStart;
-                    MetricAffectingSpan[] spans =
-                            spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class);
-                    spans = TextUtils.removeEmptySpans(spans, spanned, MetricAffectingSpan.class);
-                    measured.addStyleRun(paint, spans, spanLen, fm);
-                }
+                int fmTop = 0, fmBottom = 0, fmAscent = 0, fmDescent = 0;
+                int fmCacheIndex = 0;
+                int spanEndCacheIndex = 0;
+                int breakIndex = 0;
+                for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
+                    // retrieve end of span
+                    spanEnd = spanEndCache[spanEndCacheIndex++];
 
-                // the order of storage here (top, bottom, ascent, descent) has to match the code below
-                // where these values are retrieved
-                fmCache[fmCacheCount * 4 + 0] = fm.top;
-                fmCache[fmCacheCount * 4 + 1] = fm.bottom;
-                fmCache[fmCacheCount * 4 + 2] = fm.ascent;
-                fmCache[fmCacheCount * 4 + 3] = fm.descent;
-                fmCacheCount++;
+                    // retrieve cached metrics, order matches above
+                    fm.top = fmCache[fmCacheIndex * 4 + 0];
+                    fm.bottom = fmCache[fmCacheIndex * 4 + 1];
+                    fm.ascent = fmCache[fmCacheIndex * 4 + 2];
+                    fm.descent = fmCache[fmCacheIndex * 4 + 3];
+                    fmCacheIndex++;
 
-                spanEndCache[spanEndCacheCount] = spanEnd;
-                spanEndCacheCount++;
-            }
-
-            int breakCount = nComputeLineBreaks(b.mNativePtr, lineBreaks, lineBreaks.breaks,
-                    lineBreaks.widths, lineBreaks.ascents, lineBreaks.descents, lineBreaks.flags,
-                    lineBreaks.breaks.length, widths);
-
-            final int[] breaks = lineBreaks.breaks;
-            final float[] lineWidths = lineBreaks.widths;
-            final float[] ascents = lineBreaks.ascents;
-            final float[] descents = lineBreaks.descents;
-            final int[] flags = lineBreaks.flags;
-
-            final int remainingLineCount = mMaximumVisibleLineCount - mLineCount;
-            final boolean ellipsisMayBeApplied = ellipsize != null
-                    && (ellipsize == TextUtils.TruncateAt.END
-                        || (mMaximumVisibleLineCount == 1
-                                && ellipsize != TextUtils.TruncateAt.MARQUEE));
-            if (0 < remainingLineCount && remainingLineCount < breakCount
-                    && ellipsisMayBeApplied) {
-                // Calculate width and flag.
-                float width = 0;
-                int flag = 0; // XXX May need to also have starting hyphen edit
-                for (int i = remainingLineCount - 1; i < breakCount; i++) {
-                    if (i == breakCount - 1) {
-                        width += lineWidths[i];
-                    } else {
-                        for (int j = (i == 0 ? 0 : breaks[i - 1]); j < breaks[i]; j++) {
-                            width += widths[j];
-                        }
-                    }
-                    flag |= flags[i] & TAB_MASK;
-                }
-                // Treat the last line and overflowed lines as a single line.
-                breaks[remainingLineCount - 1] = breaks[breakCount - 1];
-                lineWidths[remainingLineCount - 1] = width;
-                flags[remainingLineCount - 1] = flag;
-
-                breakCount = remainingLineCount;
-            }
-
-            // here is the offset of the starting character of the line we are currently measuring
-            int here = paraStart;
-
-            int fmTop = 0, fmBottom = 0, fmAscent = 0, fmDescent = 0;
-            int fmCacheIndex = 0;
-            int spanEndCacheIndex = 0;
-            int breakIndex = 0;
-            for (int spanStart = paraStart, spanEnd; spanStart < paraEnd; spanStart = spanEnd) {
-                // retrieve end of span
-                spanEnd = spanEndCache[spanEndCacheIndex++];
-
-                // retrieve cached metrics, order matches above
-                fm.top = fmCache[fmCacheIndex * 4 + 0];
-                fm.bottom = fmCache[fmCacheIndex * 4 + 1];
-                fm.ascent = fmCache[fmCacheIndex * 4 + 2];
-                fm.descent = fmCache[fmCacheIndex * 4 + 3];
-                fmCacheIndex++;
-
-                if (fm.top < fmTop) {
-                    fmTop = fm.top;
-                }
-                if (fm.ascent < fmAscent) {
-                    fmAscent = fm.ascent;
-                }
-                if (fm.descent > fmDescent) {
-                    fmDescent = fm.descent;
-                }
-                if (fm.bottom > fmBottom) {
-                    fmBottom = fm.bottom;
-                }
-
-                // skip breaks ending before current span range
-                while (breakIndex < breakCount && paraStart + breaks[breakIndex] < spanStart) {
-                    breakIndex++;
-                }
-
-                while (breakIndex < breakCount && paraStart + breaks[breakIndex] <= spanEnd) {
-                    int endPos = paraStart + breaks[breakIndex];
-
-                    boolean moreChars = (endPos < bufEnd);
-
-                    final int ascent = fallbackLineSpacing
-                            ? Math.min(fmAscent, Math.round(ascents[breakIndex]))
-                            : fmAscent;
-                    final int descent = fallbackLineSpacing
-                            ? Math.max(fmDescent, Math.round(descents[breakIndex]))
-                            : fmDescent;
-                    v = out(source, here, endPos,
-                            ascent, descent, fmTop, fmBottom,
-                            v, spacingmult, spacingadd, chooseHt, chooseHtv, fm, flags[breakIndex],
-                            needMultiply, chdirs, dir, easy, bufEnd, includepad, trackpad,
-                            addLastLineSpacing, chs, widths, paraStart, ellipsize,
-                            ellipsizedWidth, lineWidths[breakIndex], paint, moreChars);
-
-                    if (endPos < spanEnd) {
-                        // preserve metrics for current span
+                    if (fm.top < fmTop) {
                         fmTop = fm.top;
-                        fmBottom = fm.bottom;
+                    }
+                    if (fm.ascent < fmAscent) {
                         fmAscent = fm.ascent;
+                    }
+                    if (fm.descent > fmDescent) {
                         fmDescent = fm.descent;
-                    } else {
-                        fmTop = fmBottom = fmAscent = fmDescent = 0;
+                    }
+                    if (fm.bottom > fmBottom) {
+                        fmBottom = fm.bottom;
                     }
 
-                    here = endPos;
-                    breakIndex++;
-
-                    if (mLineCount >= mMaximumVisibleLineCount && mEllipsized) {
-                        return;
+                    // skip breaks ending before current span range
+                    while (breakIndex < breakCount && paraStart + breaks[breakIndex] < spanStart) {
+                        breakIndex++;
                     }
+
+                    while (breakIndex < breakCount && paraStart + breaks[breakIndex] <= spanEnd) {
+                        int endPos = paraStart + breaks[breakIndex];
+
+                        boolean moreChars = (endPos < bufEnd);
+
+                        final int ascent = fallbackLineSpacing
+                                ? Math.min(fmAscent, Math.round(ascents[breakIndex]))
+                                : fmAscent;
+                        final int descent = fallbackLineSpacing
+                                ? Math.max(fmDescent, Math.round(descents[breakIndex]))
+                                : fmDescent;
+                        v = out(source, here, endPos,
+                                ascent, descent, fmTop, fmBottom,
+                                v, spacingmult, spacingadd, chooseHt, chooseHtv, fm,
+                                flags[breakIndex], needMultiply, chdirs, dir, easy, bufEnd,
+                                includepad, trackpad, addLastLineSpacing, chs, widths, paraStart,
+                                ellipsize, ellipsizedWidth, lineWidths[breakIndex], paint,
+                                moreChars);
+
+                        if (endPos < spanEnd) {
+                            // preserve metrics for current span
+                            fmTop = fm.top;
+                            fmBottom = fm.bottom;
+                            fmAscent = fm.ascent;
+                            fmDescent = fm.descent;
+                        } else {
+                            fmTop = fmBottom = fmAscent = fmDescent = 0;
+                        }
+
+                        here = endPos;
+                        breakIndex++;
+
+                        if (mLineCount >= mMaximumVisibleLineCount && mEllipsized) {
+                            return;
+                        }
+                    }
+                }
+
+                if (paraEnd == bufEnd) {
+                    break;
                 }
             }
 
-            if (paraEnd == bufEnd)
-                break;
-        }
+            if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE)
+                    && mLineCount < mMaximumVisibleLineCount) {
+                measured.setPara(source, bufEnd, bufEnd, textDir);
 
-        if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE) &&
-                mLineCount < mMaximumVisibleLineCount) {
-            measured.setPara(source, bufEnd, bufEnd, textDir, b);
+                paint.getFontMetricsInt(fm);
 
-            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.mLevels, measured.mDir, measured.mEasy, bufEnd,
-                    includepad, trackpad, addLastLineSpacing, null,
-                    null, bufStart, ellipsize,
-                    ellipsizedWidth, 0, paint, false);
+                v = out(source,
+                        bufEnd, bufEnd, fm.ascent, fm.descent,
+                        fm.top, fm.bottom,
+                        v,
+                        spacingmult, spacingadd, null,
+                        null, fm, 0,
+                        needMultiply, measured.mLevels, measured.mDir, measured.mEasy, bufEnd,
+                        includepad, trackpad, addLastLineSpacing, null,
+                        null, bufStart, ellipsize,
+                        ellipsizedWidth, 0, paint, false);
+            }
+        } finally {
+            nFinish(nativePtr);
         }
     }
 
@@ -1487,26 +1473,51 @@
                 mMaxLineHeight : super.getHeight();
     }
 
-    private static native long nNewBuilder();
-    private static native void nFreeBuilder(long nativePtr);
-    private static native void nFinishBuilder(long nativePtr);
+    /**
+     * 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.
+     */
 
-    // Set up paragraph text and settings; done as one big method to minimize jni crossings
-    private static native void nSetupParagraph(
-            /* non zero */ long nativePtr, @NonNull char[] text, @IntRange(from = 0) int length,
-            @FloatRange(from = 0.0f) float firstWidth, @IntRange(from = 0) int firstWidthLineCount,
-            @FloatRange(from = 0.0f) float restWidth, @Nullable int[] variableTabStops,
-            int defaultTabStop, @BreakStrategy int breakStrategy,
-            @HyphenationFrequency int hyphenationFrequency, boolean isJustified,
-            @Nullable int[] indents, @Nullable int[] leftPaddings, @Nullable int[] rightPaddings,
-            @IntRange(from = 0) int indentsOffset);
+    /* package */ static void addStyleRun(long nativePtr, TextPaint paint, int start, int end,
+            boolean isRtl) {
+        nAddStyleRun(nativePtr, paint.getNativeInstance(), start, end, isRtl);
+    }
 
-    // TODO: Make this method CriticalNative once native code defers doing layouts.
+    /* 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,
+            @HyphenationFrequency int hyphenationFrequency,
+            boolean isJustified,
+            @Nullable int[] indents,
+            @Nullable int[] leftPaddings,
+            @Nullable int[] rightPaddings);
+
+    @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);
 
-    // TODO: Make this method CriticalNative once native code defers doing layouts.
+    @CriticalNative
     private static native void nAddReplacementRun(
             /* non-zero */ long nativePtr, /* non-zero */ long nativePaint,
             @IntRange(from = 0) int start, @IntRange(from = 0) int end,
@@ -1519,10 +1530,28 @@
     // arrays do not have to be resized
     // The individual character widths will be returned in charWidths. The length of charWidths must
     // be at least the length of the text.
-    private static native int nComputeLineBreaks(long nativePtr, LineBreaks recycle,
-            int[] recycleBreaks, float[] recycleWidths, float[] recycleAscents,
-            float[] recycleDescents, int[] recycleFlags, int recycleLength,
-            float[] charWidths);
+    private static native int nComputeLineBreaks(
+            /* non zero */ long nativePtr,
+
+            // Inputs
+            @NonNull char[] text,
+            @IntRange(from = 0) int length,
+            @FloatRange(from = 0.0f) float firstWidth,
+            @IntRange(from = 0) int firstWidthLineCount,
+            @FloatRange(from = 0.0f) float restWidth,
+            @Nullable int[] variableTabStops,
+            int defaultTabStop,
+            @IntRange(from = 0) int indentsOffset,
+
+            // Outputs
+            @NonNull LineBreaks recycle,
+            @IntRange(from  = 0) int recycleLength,
+            @NonNull int[] recycleBreaks,
+            @NonNull float[] recycleWidths,
+            @NonNull float[] recycleAscents,
+            @NonNull float[] recycleDescents,
+            @NonNull int[] recycleFlags,
+            @NonNull float[] charWidths);
 
     private int mLineCount;
     private int mTopPadding, mBottomPadding;
diff --git a/core/java/android/text/TextUtils.java b/core/java/android/text/TextUtils.java
index 68afeec..cbdaa69 100644
--- a/core/java/android/text/TextUtils.java
+++ b/core/java/android/text/TextUtils.java
@@ -1519,7 +1519,7 @@
                     }
 
                     // XXX this is probably ok, but need to look at it more
-                    tempMt.setPara(format, 0, format.length(), textDir, null);
+                    tempMt.setPara(format, 0, format.length(), textDir);
                     float moreWid = tempMt.addStyleRun(p, tempMt.mLen, null);
 
                     if (w + moreWid <= avail) {
@@ -1541,7 +1541,7 @@
     private static float setPara(MeasuredText mt, TextPaint paint,
             CharSequence text, int start, int end, TextDirectionHeuristic textDir) {
 
-        mt.setPara(text, start, end, textDir, null);
+        mt.setPara(text, start, end, textDir);
 
         float width;
         Spanned sp = text instanceof Spanned ? (Spanned) text : null;
diff --git a/core/jni/android_text_StaticLayout.cpp b/core/jni/android_text_StaticLayout.cpp
index 04e9dfd..c1419ba 100644
--- a/core/jni/android_text_StaticLayout.cpp
+++ b/core/jni/android_text_StaticLayout.cpp
@@ -55,11 +55,11 @@
 class JNILineBreakerLineWidth : public minikin::LineBreaker::LineWidthDelegate {
     public:
         JNILineBreakerLineWidth(float firstWidth, int32_t firstLineCount, float restWidth,
-                std::vector<float>&& indents, std::vector<float>&& leftPaddings,
-                std::vector<float>&& rightPaddings, int32_t indentsAndPaddingsOffset)
+                const std::vector<float>& indents, const std::vector<float>& leftPaddings,
+                const std::vector<float>& rightPaddings, int32_t indentsAndPaddingsOffset)
             : mFirstWidth(firstWidth), mFirstLineCount(firstLineCount), mRestWidth(restWidth),
-              mIndents(std::move(indents)), mLeftPaddings(std::move(leftPaddings)),
-              mRightPaddings(std::move(rightPaddings)), mOffset(indentsAndPaddingsOffset) {}
+              mIndents(indents), mLeftPaddings(leftPaddings),
+              mRightPaddings(rightPaddings), mOffset(indentsAndPaddingsOffset) {}
 
         float getLineWidth(size_t lineNo) override {
             const float width = ((ssize_t)lineNo < (ssize_t)mFirstLineCount)
@@ -91,9 +91,9 @@
         const float mFirstWidth;
         const int32_t mFirstLineCount;
         const float mRestWidth;
-        const std::vector<float> mIndents;
-        const std::vector<float> mLeftPaddings;
-        const std::vector<float> mRightPaddings;
+        const std::vector<float>& mIndents;
+        const std::vector<float>& mLeftPaddings;
+        const std::vector<float>& mRightPaddings;
         const int32_t mOffset;
 };
 
@@ -106,32 +106,132 @@
     }
 }
 
+class Run {
+    public:
+        Run(int32_t start, int32_t end) : mStart(start), mEnd(end) {}
+        virtual ~Run() {}
+
+        virtual void addTo(minikin::LineBreaker* lineBreaker) = 0;
+
+    protected:
+        const int32_t mStart;
+        const int32_t mEnd;
+
+    private:
+        // Forbid copy and assign.
+        Run(const Run&) = delete;
+        void operator=(const Run&) = delete;
+};
+
+class StyleRun : public Run {
+    public:
+        StyleRun(int32_t start, int32_t end, minikin::MinikinPaint&& paint,
+                std::shared_ptr<minikin::FontCollection>&& collection,
+                minikin::FontStyle&& style, bool isRtl)
+            : Run(start, end), mPaint(std::move(paint)), mCollection(std::move(collection)),
+              mStyle(std::move(style)), mIsRtl(isRtl) {}
+
+        void addTo(minikin::LineBreaker* lineBreaker) override {
+            lineBreaker->addStyleRun(&mPaint, mCollection, mStyle, mStart, mEnd, mIsRtl);
+        }
+
+    private:
+        minikin::MinikinPaint mPaint;
+        std::shared_ptr<minikin::FontCollection> mCollection;
+        minikin::FontStyle mStyle;
+        const bool mIsRtl;
+};
+
+class Replacement : public Run {
+    public:
+        Replacement(int32_t start, int32_t end, float width, uint32_t localeListId)
+            : Run(start, end), mWidth(width), mLocaleListId(localeListId) {}
+
+        void addTo(minikin::LineBreaker* lineBreaker) override {
+            lineBreaker->addReplacement(mStart, mEnd, mWidth, mLocaleListId);
+        }
+
+    private:
+        const float mWidth;
+        const uint32_t mLocaleListId;
+};
+
+class StaticLayoutNative {
+    public:
+        StaticLayoutNative(
+                minikin::BreakStrategy strategy, minikin::HyphenationFrequency frequency,
+                bool isJustified, std::vector<float>&& indents, std::vector<float>&& leftPaddings,
+                std::vector<float>&& rightPaddings)
+            : mStrategy(strategy), mFrequency(frequency), mIsJustified(isJustified),
+              mIndents(std::move(indents)), mLeftPaddings(std::move(leftPaddings)),
+              mRightPaddings(std::move(rightPaddings)) {}
+
+        void addStyleRun(int32_t start, int32_t end, minikin::MinikinPaint&& paint,
+                         std::shared_ptr<minikin::FontCollection> collection,
+                         minikin::FontStyle&& style, bool isRtl) {
+            mRuns.emplace_back(std::make_unique<StyleRun>(
+                    start, end, std::move(paint), std::move(collection), std::move(style), isRtl));
+        }
+
+        void addReplacementRun(int32_t start, int32_t end, float width, uint32_t localeListId) {
+            mRuns.emplace_back(std::make_unique<Replacement>(start, end, width, localeListId));
+        }
+
+        // Only valid while this instance is alive.
+        inline std::unique_ptr<minikin::LineBreaker::LineWidthDelegate> buildLineWidthDelegate(
+                float firstWidth, int32_t firstLineCount, float restWidth,
+                int32_t indentsAndPaddingsOffset) {
+            return std::make_unique<JNILineBreakerLineWidth>(
+                firstWidth, firstLineCount, restWidth, mIndents, mLeftPaddings, mRightPaddings,
+                indentsAndPaddingsOffset);
+        }
+
+        void addRuns(minikin::LineBreaker* lineBreaker) {
+            for (const auto& run : mRuns) {
+                run->addTo(lineBreaker);
+            }
+        }
+
+        void clearRuns() {
+            mRuns.clear();
+        }
+
+        inline minikin::BreakStrategy getStrategy() const { return mStrategy; }
+        inline minikin::HyphenationFrequency getFrequency() const { return mFrequency; }
+        inline bool isJustified() const { return mIsJustified; }
+
+    private:
+        const minikin::BreakStrategy mStrategy;
+        const minikin::HyphenationFrequency mFrequency;
+        const bool mIsJustified;
+        const std::vector<float> mIndents;
+        const std::vector<float> mLeftPaddings;
+        const std::vector<float> mRightPaddings;
+
+        std::vector<std::unique_ptr<Run>> mRuns;
+};
+
+static inline StaticLayoutNative* toNative(jlong ptr) {
+    return reinterpret_cast<StaticLayoutNative*>(ptr);
+}
+
 // set text and set a number of parameters for creating a layout (width, tabstops, strategy,
 // hyphenFrequency)
-static void nSetupParagraph(JNIEnv* env, jclass, jlong nativePtr, jcharArray text, jint length,
-        jfloat firstWidth, jint firstWidthLineLimit, jfloat restWidth,
-        jintArray variableTabStops, jint defaultTabStop, jint strategy, jint hyphenFrequency,
-        jboolean isJustified, jintArray indents, jintArray leftPaddings, jintArray rightPaddings,
-        jint indentsAndPaddingsOffset) {
-    minikin::LineBreaker* b = reinterpret_cast<minikin::LineBreaker*>(nativePtr);
-    b->resize(length);
-    env->GetCharArrayRegion(text, 0, length, b->buffer());
-    b->setText();
-    if (variableTabStops == nullptr) {
-        b->setTabStops(nullptr, 0, defaultTabStop);
-    } else {
-        ScopedIntArrayRO stops(env, variableTabStops);
-        b->setTabStops(stops.get(), stops.size(), defaultTabStop);
-    }
-    b->setStrategy(static_cast<minikin::BreakStrategy>(strategy));
-    b->setHyphenationFrequency(static_cast<minikin::HyphenationFrequency>(hyphenFrequency));
-    b->setJustified(isJustified);
+static jlong nInit(JNIEnv* env, jclass /* unused */,
+        jint breakStrategy, jint hyphenationFrequency, jboolean isJustified,
+        jintArray indents, jintArray leftPaddings, jintArray rightPaddings) {
+    return reinterpret_cast<jlong>(new StaticLayoutNative(
+            static_cast<minikin::BreakStrategy>(breakStrategy),
+            static_cast<minikin::HyphenationFrequency>(hyphenationFrequency),
+            isJustified,
+            jintArrayToFloatVector(env, indents),
+            jintArrayToFloatVector(env, leftPaddings),
+            jintArrayToFloatVector(env, rightPaddings)));
+}
 
-    // TODO: copy indents and paddings only once when LineBreaker is started to be used.
-    b->setLineWidthDelegate(std::make_unique<JNILineBreakerLineWidth>(
-            firstWidth, firstWidthLineLimit, restWidth, jintArrayToFloatVector(env, indents),
-            jintArrayToFloatVector(env, leftPaddings), jintArrayToFloatVector(env, rightPaddings),
-            indentsAndPaddingsOffset));
+// CriticalNative
+static void nFinish(jlong nativePtr) {
+    delete toNative(nativePtr);
 }
 
 static void recycleCopy(JNIEnv* env, jobject recycle, jintArray recycleBreaks,
@@ -163,42 +263,65 @@
 }
 
 static jint nComputeLineBreaks(JNIEnv* env, jclass, jlong nativePtr,
-                               jobject recycle, jintArray recycleBreaks,
-                               jfloatArray recycleWidths, jfloatArray recycleAscents,
-                               jfloatArray recycleDescents, jintArray recycleFlags,
-                               jint recycleLength, jfloatArray charWidths) {
-    minikin::LineBreaker* b = reinterpret_cast<minikin::LineBreaker*>(nativePtr);
+        // Inputs
+        jcharArray text,
+        jint length,
+        jfloat firstWidth,
+        jint firstWidthLineCount,
+        jfloat restWidth,
+        jintArray variableTabStops,
+        jint defaultTabStop,
+        jint indentsOffset,
 
-    size_t nBreaks = b->computeBreaks();
+        // Outputs
+        jobject recycle,
+        jint recycleLength,
+        jintArray recycleBreaks,
+        jfloatArray recycleWidths,
+        jfloatArray recycleAscents,
+        jfloatArray recycleDescents,
+        jintArray recycleFlags,
+        jfloatArray charWidths) {
+
+    StaticLayoutNative* builder = toNative(nativePtr);
+
+    // TODO: Reorganize minikin APIs.
+    minikin::LineBreaker b;
+    b.resize(length);
+    env->GetCharArrayRegion(text, 0, length, b.buffer());
+    b.setText();
+    if (variableTabStops == nullptr) {
+        b.setTabStops(nullptr, 0, defaultTabStop);
+    } else {
+        ScopedIntArrayRO stops(env, variableTabStops);
+        b.setTabStops(stops.get(), stops.size(), defaultTabStop);
+    }
+    b.setStrategy(builder->getStrategy());
+    b.setHyphenationFrequency(builder->getFrequency());
+    b.setJustified(builder->isJustified());
+    b.setLineWidthDelegate(builder->buildLineWidthDelegate(
+            firstWidth, firstWidthLineCount, restWidth, indentsOffset));
+
+    builder->addRuns(&b);
+
+    size_t nBreaks = b.computeBreaks();
 
     recycleCopy(env, recycle, recycleBreaks, recycleWidths, recycleAscents, recycleDescents,
-            recycleFlags, recycleLength, nBreaks, b->getBreaks(), b->getWidths(), b->getAscents(),
-            b->getDescents(), b->getFlags());
+            recycleFlags, recycleLength, nBreaks, b.getBreaks(), b.getWidths(), b.getAscents(),
+            b.getDescents(), b.getFlags());
 
-    env->SetFloatArrayRegion(charWidths, 0, b->size(), b->charWidths());
+    env->SetFloatArrayRegion(charWidths, 0, b.size(), b.charWidths());
 
-    b->finish();
+    b.finish();
+    builder->clearRuns();
 
     return static_cast<jint>(nBreaks);
 }
 
-static jlong nNewBuilder(JNIEnv*, jclass) {
-    return reinterpret_cast<jlong>(new minikin::LineBreaker);
-}
-
-static void nFreeBuilder(JNIEnv*, jclass, jlong nativePtr) {
-    delete reinterpret_cast<minikin::LineBreaker*>(nativePtr);
-}
-
-static void nFinishBuilder(JNIEnv*, jclass, jlong nativePtr) {
-    minikin::LineBreaker* b = reinterpret_cast<minikin::LineBreaker*>(nativePtr);
-    b->finish();
-}
-
 // Basically similar to Paint.getTextRunAdvances but with C++ interface
-static void nAddStyleRun(JNIEnv* env, jclass, jlong nativePtr, jlong nativePaint, jint start,
-        jint end, jboolean isRtl) {
-    minikin::LineBreaker* b = reinterpret_cast<minikin::LineBreaker*>(nativePtr);
+// CriticalNative
+static void nAddStyleRun(jlong nativePtr, jlong nativePaint, jint start, jint end, jboolean isRtl) {
+    StaticLayoutNative* builder = toNative(nativePtr);
     Paint* paint = reinterpret_cast<Paint*>(nativePaint);
     const Typeface* typeface = paint->getAndroidTypeface();
     minikin::MinikinPaint minikinPaint;
@@ -206,26 +329,59 @@
     minikin::FontStyle style = MinikinUtils::prepareMinikinPaint(&minikinPaint, paint,
             typeface);
 
-    b->addStyleRun(&minikinPaint, resolvedTypeface->fFontCollection, style, start, end, isRtl);
+    builder->addStyleRun(
+        start, end, std::move(minikinPaint), resolvedTypeface->fFontCollection, std::move(style),
+        isRtl);
 }
 
-static void nAddReplacementRun(JNIEnv* env, jclass, jlong nativePtr, jlong nativePaint,
-        jint start, jint end, jfloat width) {
-    minikin::LineBreaker* b = reinterpret_cast<minikin::LineBreaker*>(nativePtr);
+// CriticalNative
+static void nAddReplacementRun(jlong nativePtr, jlong nativePaint, jint start, jint end,
+        jfloat width) {
+    StaticLayoutNative* builder = toNative(nativePtr);
     Paint* paint = reinterpret_cast<Paint*>(nativePaint);
-    b->addReplacement(start, end, width, paint->getMinikinLangListId());
+    builder->addReplacementRun(start, end, width, paint->getMinikinLangListId());
 }
 
 static const JNINativeMethod gMethods[] = {
-    // TODO performance: many of these are candidates for fast jni, awaiting guidance
-    {"nNewBuilder", "()J", (void*) nNewBuilder},
-    {"nFreeBuilder", "(J)V", (void*) nFreeBuilder},
-    {"nFinishBuilder", "(J)V", (void*) nFinishBuilder},
-    {"nSetupParagraph", "(J[CIFIF[IIIIZ[I[I[II)V", (void*) nSetupParagraph},
+    // Fast Natives
+    {"nInit", "("
+        "I"  // breakStrategy
+        "I"  // hyphenationFrequency
+        "Z"  // isJustified
+        "[I"  // indents
+        "[I"  // left paddings
+        "[I"  // right paddings
+        ")J", (void*) nInit},
+
+    // Critical Natives
+    {"nFinish", "(J)V", (void*) nFinish},
     {"nAddStyleRun", "(JJIIZ)V", (void*) nAddStyleRun},
     {"nAddReplacementRun", "(JJIIF)V", (void*) nAddReplacementRun},
-    {"nComputeLineBreaks", "(JLandroid/text/StaticLayout$LineBreaks;[I[F[F[F[II[F)I",
-        (void*) nComputeLineBreaks}
+
+    // Regular JNI
+    {"nComputeLineBreaks", "("
+        "J"  // nativePtr
+
+        // Inputs
+        "[C"  // text
+        "I"  // length
+        "F"  // firstWidth
+        "I"  // firstWidthLineCount
+        "F"  // restWidth
+        "[I"  // variableTabStops
+        "I"  // defaultTabStop
+        "I"  // indentsOffset
+
+        // Outputs
+        "Landroid/text/StaticLayout$LineBreaks;"  // recycle
+        "I"  // recycleLength
+        "[I"  // recycleBreaks
+        "[F"  // recycleWidths
+        "[F"  // recycleAscents
+        "[F"  // recycleDescents
+        "[I"  // recycleFlags
+        "[F"  // charWidths
+        ")I", (void*) nComputeLineBreaks}
 };
 
 int register_android_text_StaticLayout(JNIEnv* env)