Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6f11c6e7 authored by Seigo Nonaka's avatar Seigo Nonaka
Browse files

Extract native methods into isolated classes

StaticLayout/MeasuredParagraph uses native methods which is a pain point
of porting TextView to JetPack.
To make minimize the dependency to the native methods, extract native
methods and put them into a thin wrapper class.

The performance impact is limited:

android.text.StaticLayoutPerfTest:
    PrecomputedText Balanced Hyphenation  :    602 ->    644: ( +42, +7.0%)
    PrecomputedText Balanced NoHyphenation:    457 ->    476: ( +19, +4.2%)
    PrecomputedText Greedy Hyphenation    :    397 ->    412: ( +15, +3.8%)
    PrecomputedText Greedy NoHyphenation  :    397 ->    411: ( +14, +3.5%)
    RandomText Balanced Hyphenation       : 17,594 -> 17,715: (+121, +0.7%)
    RandomText Balanced NoHyphenation     :  7,146 ->  7,236: ( +90, +1.3%)
    RandomText Greedy Hyphenation         :  7,125 ->  7,196: ( +71, +1.0%)
    RandomText Greedy NoHyphenation       :  7,099 ->  7,187: ( +88, +1.2%)
  draw
    PrecomputedText NoStyle               :    614 ->    628: ( +14, +2.3%)
    PrecomputedText Style                 :    778 ->    826: ( +48, +6.2%)
    RandomText NoStyle                    :    537 ->    540: (  +3, +0.6%)
    RandomText Style                      :    786 ->    759: ( -27, -3.4%)

Bug: N/A
Test: atest CtsWidgetTestCases:EditTextTest
    CtsWidgetTestCases:TextViewFadingEdgeTest
    FrameworksCoreTests:TextViewFallbackLineSpacingTest
    FrameworksCoreTests:TextViewTest FrameworksCoreTests:TypefaceTest
    CtsGraphicsTestCases:TypefaceTest CtsWidgetTestCases:TextViewTest
    CtsTextTestCases FrameworksCoreTests:android.text
    CtsWidgetTestCases:TextViewPrecomputedTextTest

Change-Id: I976df4db63be241af395dd30dd94182f76bdae53
parent a71bee87
Loading
Loading
Loading
Loading
+41 −125
Original line number Diff line number Diff line
@@ -30,10 +30,6 @@ import android.text.style.MetricAffectingSpan;
import android.text.style.ReplacementSpan;
import android.util.Pools.SynchronizedPool;

import dalvik.annotation.optimization.CriticalNative;

import libcore.util.NativeAllocationRegistry;

import java.util.Arrays;

/**
@@ -62,9 +58,6 @@ import java.util.Arrays;
public class MeasuredParagraph {
    private static final char OBJECT_REPLACEMENT_CHARACTER = '\uFFFC';

    private static final NativeAllocationRegistry sRegistry = new NativeAllocationRegistry(
            MeasuredParagraph.class.getClassLoader(), nGetReleaseFunc(), 1024);

    private MeasuredParagraph() {}  // Use build static functions instead.

    private static final SynchronizedPool<MeasuredParagraph> sPool = new SynchronizedPool<>(1);
@@ -128,24 +121,7 @@ public class MeasuredParagraph {
    private @Nullable IntArray mFontMetrics = new IntArray(4 * 4);

    // The native MeasuredParagraph.
    // 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;
        }
    }
    private @Nullable NativeMeasuredParagraph mNativeMeasuredParagraph;

    // Following two objects are for avoiding object allocation.
    private @NonNull TextPaint mCachedPaint = new TextPaint();
@@ -173,7 +149,7 @@ public class MeasuredParagraph {
        mWidths.clear();
        mFontMetrics.clear();
        mSpanEndCache.clear();
        unbindNativeObject();
        mNativeMeasuredParagraph = null;
    }

    /**
@@ -267,10 +243,10 @@ public class MeasuredParagraph {
     * Returns the native ptr of the MeasuredParagraph.
     *
     * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
     * Returns 0 in other cases.
     * Returns null in other cases.
     */
    public /* Maybe Zero */ long getNativePtr() {
        return mNativePtr;
    public NativeMeasuredParagraph getNativeMeasuredParagraph() {
        return mNativeMeasuredParagraph;
    }

    /**
@@ -283,7 +259,7 @@ public class MeasuredParagraph {
     * @param end the exclusive end offset of the target region in the text
     */
    public float getWidth(int start, int end) {
        if (mNativePtr == 0) {
        if (mNativeMeasuredParagraph == null) {
            // We have result in Java.
            final float[] widths = mWidths.getRawArray();
            float r = 0.0f;
@@ -293,7 +269,7 @@ public class MeasuredParagraph {
            return r;
        } else {
            // We have result in native.
            return nGetWidth(mNativePtr, start, end);
            return mNativeMeasuredParagraph.getWidth(start, end);
        }
    }

@@ -305,7 +281,16 @@ public class MeasuredParagraph {
     */
    public void getBounds(@IntRange(from = 0) int start, @IntRange(from = 0) int end,
            @NonNull Rect bounds) {
        nGetBounds(mNativePtr, mCopiedBuffer, start, end, bounds);
        mNativeMeasuredParagraph.getBounds(mCopiedBuffer, start, end, bounds);
    }

    /**
     * Returns a width of the character at the offset.
     *
     * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
     */
    public float getCharWidthAt(@IntRange(from = 0) int offset) {
        return mNativeMeasuredParagraph.getCharWidthAt(offset);
    }

    /**
@@ -364,7 +349,7 @@ public class MeasuredParagraph {
        if (mt.mSpanned == null) {
            // No style change by MetricsAffectingSpan. Just measure all text.
            mt.applyMetricsAffectingSpan(
                    paint, null /* spans */, start, end, 0 /* native static layout ptr */);
                    paint, null /* spans */, start, end, null /* native builder ptr */);
        } else {
            // There may be a MetricsAffectingSpan. Split into span transitions and apply styles.
            int spanEnd;
@@ -374,7 +359,7 @@ public class MeasuredParagraph {
                        MetricAffectingSpan.class);
                spans = TextUtils.removeEmptySpans(spans, mt.mSpanned, MetricAffectingSpan.class);
                mt.applyMetricsAffectingSpan(
                        paint, spans, spanStart, spanEnd, 0 /* native static layout ptr */);
                        paint, spans, spanStart, spanEnd, null /* native builder ptr */);
            }
        }
        return mt;
@@ -406,25 +391,16 @@ public class MeasuredParagraph {
            @Nullable MeasuredParagraph recycle) {
        final MeasuredParagraph mt = recycle == null ? obtain() : recycle;
        mt.resetAndAnalyzeBidi(text, start, end, textDir);
        final NativeMeasuredParagraph.Builder builder = new NativeMeasuredParagraph.Builder();
        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(
                        nBuildNativeMeasuredParagraph(nativeBuilderPtr, mt.mCopiedBuffer,
                              computeHyphenation, computeLayout));
            } finally {
                nFreeBuilder(nativeBuilderPtr);
            }
            return mt;
        }

        long nativeBuilderPtr = nInitBuilder();
        try {
            mt.mNativeMeasuredParagraph = builder.build(mt.mCopiedBuffer, computeHyphenation,
                        computeLayout);
        } else {
            if (mt.mSpanned == null) {
                // No style change by MetricsAffectingSpan. Just measure all text.
                mt.applyMetricsAffectingSpan(paint, null /* spans */, start, end, nativeBuilderPtr);
                mt.applyMetricsAffectingSpan(paint, null /* spans */, start, end, builder);
                mt.mSpanEndCache.append(end);
            } else {
                // There may be a MetricsAffectingSpan. Split into span transitions and apply
@@ -437,15 +413,12 @@ public class MeasuredParagraph {
                            MetricAffectingSpan.class);
                    spans = TextUtils.removeEmptySpans(spans, mt.mSpanned,
                                                       MetricAffectingSpan.class);
                    mt.applyMetricsAffectingSpan(paint, spans, spanStart, spanEnd,
                                                 nativeBuilderPtr);
                    mt.applyMetricsAffectingSpan(paint, spans, spanStart, spanEnd, builder);
                    mt.mSpanEndCache.append(spanEnd);
                }
            }
            mt.bindNativeObject(nBuildNativeMeasuredParagraph(nativeBuilderPtr, mt.mCopiedBuffer,
                      computeHyphenation, computeLayout));
        } finally {
            nFreeBuilder(nativeBuilderPtr);
            mt.mNativeMeasuredParagraph = builder.build(mt.mCopiedBuffer, computeHyphenation,
                    computeLayout);
        }

        return mt;
@@ -517,13 +490,13 @@ public class MeasuredParagraph {
    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 nativeBuilderPtr) {
                                     @Nullable NativeMeasuredParagraph.Builder builder) {
        // 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 (nativeBuilderPtr == 0) {
        if (builder == null) {
            // Assigns all width to the first character. This is the same behavior as minikin.
            mWidths.set(start, width);
            if (end > start + 1) {
@@ -531,24 +504,22 @@ public class MeasuredParagraph {
            }
            mWholeWidth += width;
        } else {
            nAddReplacementRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), start, end,
                               width);
            builder.addReplacementRun(mCachedPaint, 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 nativeBuilderPtr) {
                               @Nullable NativeMeasuredParagraph.Builder builder) {

        if (mLtrWithoutBidi) {
            // If the whole text is LTR direction, just apply whole region.
            if (nativeBuilderPtr == 0) {
            if (builder == null) {
                mWholeWidth += mCachedPaint.getTextRunAdvances(
                        mCopiedBuffer, start, end - start, start, end - start, false /* isRtl */,
                        mWidths.getRawArray(), start);
            } else {
                nAddStyleRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), start, end,
                        false /* isRtl */);
                builder.addStyleRun(mCachedPaint, start, end, false /* isRtl */);
            }
        } else {
            // If there is multiple bidi levels, split into individual bidi level and apply style.
@@ -558,14 +529,13 @@ public class MeasuredParagraph {
            for (int levelStart = start, levelEnd = start + 1;; ++levelEnd) {
                if (levelEnd == end || mLevels.get(levelEnd) != level) {  // transition point
                    final boolean isRtl = (level & 0x1) != 0;
                    if (nativeBuilderPtr == 0) {
                    if (builder == null) {
                        final int levelLength = levelEnd - levelStart;
                        mWholeWidth += mCachedPaint.getTextRunAdvances(
                                mCopiedBuffer, levelStart, levelLength, levelStart, levelLength,
                                isRtl, mWidths.getRawArray(), levelStart);
                    } else {
                        nAddStyleRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), levelStart,
                                levelEnd, isRtl);
                        builder.addStyleRun(mCachedPaint, levelStart, levelEnd, isRtl);
                    }
                    if (levelEnd == end) {
                        break;
@@ -582,12 +552,12 @@ public class MeasuredParagraph {
            @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 nativeBuilderPtr) {
            @Nullable NativeMeasuredParagraph.Builder builder) {
        mCachedPaint.set(paint);
        // XXX paint should not have a baseline shift, but...
        mCachedPaint.baselineShift = 0;

        final boolean needFontMetrics = nativeBuilderPtr != 0;
        final boolean needFontMetrics = builder != null;

        if (needFontMetrics && mCachedFm == null) {
            mCachedFm = new Paint.FontMetricsInt();
@@ -610,15 +580,14 @@ public class MeasuredParagraph {
        final int startInCopiedBuffer = start - mTextStart;
        final int endInCopiedBuffer = end - mTextStart;

        if (nativeBuilderPtr != 0) {
        if (builder != null) {
            mCachedPaint.getFontMetricsInt(mCachedFm);
        }

        if (replacement != null) {
            applyReplacementRun(replacement, startInCopiedBuffer, endInCopiedBuffer,
                                nativeBuilderPtr);
            applyReplacementRun(replacement, startInCopiedBuffer, endInCopiedBuffer, builder);
        } else {
            applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, nativeBuilderPtr);
            applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, builder);
        }

        if (needFontMetrics) {
@@ -689,59 +658,6 @@ public class MeasuredParagraph {
     * This only works if the MeasuredParagraph is computed with buildForStaticLayout.
     */
    public @IntRange(from = 0) int getMemoryUsage() {
        return nGetMemoryUsage(mNativePtr);
        return mNativeMeasuredParagraph.getMemoryUsage();
    }

    private static native /* Non Zero */ long nInitBuilder();

    /**
     * Apply style to make native measured text.
     *
     * @param nativeBuilderPtr The native MeasuredParagraph 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 MeasuredParagraph 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 nBuildNativeMeasuredParagraph(/* Non Zero */ long nativeBuilderPtr,
                                                 @NonNull char[] text,
                                                 boolean computeHyphenation,
                                                 boolean computeLayout);

    private static native void nFreeBuilder(/* Non Zero */ long nativeBuilderPtr);

    @CriticalNative
    private static native float nGetWidth(/* Non Zero */ long nativePtr,
                                         @IntRange(from = 0) int start,
                                         @IntRange(from = 0) int end);

    @CriticalNative
    private static native /* Non Zero */ long nGetReleaseFunc();

    @CriticalNative
    private static native int nGetMemoryUsage(/* Non Zero */ long nativePtr);

    private static native void nGetBounds(long nativePtr, char[] buf, int start, int end,
            Rect rect);
}
+155 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.text;

import android.annotation.FloatRange;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;

import dalvik.annotation.optimization.CriticalNative;
import dalvik.annotation.optimization.FastNative;

import libcore.util.NativeAllocationRegistry;

/**
 * A native implementation of the line breaker.
 * TODO: Consider to make this class public.
 * @hide
 */
public class NativeLineBreaker {

    /**
     * A result object of a line breaking
     */
    public static class LineBreaks {
        public int breakCount;
        private static final int INITIAL_SIZE = 16;
        public int[] breaks = new int[INITIAL_SIZE];
        public float[] widths = new float[INITIAL_SIZE];
        public float[] ascents = new float[INITIAL_SIZE];
        public float[] descents = new float[INITIAL_SIZE];
        public int[] flags = new int[INITIAL_SIZE];
        // breaks, widths, and flags should all have the same length
    }

    private static final NativeAllocationRegistry sRegistry = new NativeAllocationRegistry(
            NativeLineBreaker.class.getClassLoader(), nGetReleaseFunc(), 64);

    private final long mNativePtr;

    /**
     * A constructor of NativeLineBreaker
     */
    public NativeLineBreaker(@Layout.BreakStrategy int breakStrategy,
            @Layout.HyphenationFrequency int hyphenationFrequency,
            boolean justify, @Nullable int[] indents) {
        mNativePtr = nInit(breakStrategy, hyphenationFrequency, justify, indents);
        sRegistry.registerNativeAllocation(this, mNativePtr);
    }

    /**
     * Break text into lines
     *
     * @param chars an array of characters
     * @param measuredPara a result of the text measurement
     * @param length a length of the target text from the begining
     * @param firstWidth a width of the first width of the line in this paragraph
     * @param firstWidthLineCount a number of lines that has the length of the firstWidth
     * @param restWidth a width of the rest of the lines.
     * @param variableTabStops an array of tab stop widths
     * @param defaultTabStop a width of the tab stop
     * @param indentsOffset an offset of the indents to be used.
     * @param out output buffer
     * @return a number of the lines
     */
    @NonNull public int computeLineBreaks(
            @NonNull char[] chars,
            @NonNull NativeMeasuredParagraph measuredPara,
            @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,
            @NonNull LineBreaks out) {
        return nComputeLineBreaks(
                mNativePtr,

                // Inputs
                chars,
                measuredPara.getNativePtr(),
                length,
                firstWidth,
                firstWidthLineCount,
                restWidth,
                variableTabStops,
                defaultTabStop,
                indentsOffset,

                // Outputs
                out,
                out.breaks.length,
                out.breaks,
                out.widths,
                out.ascents,
                out.descents,
                out.flags);

    }

    @FastNative
    private static native long nInit(
            @Layout.BreakStrategy int breakStrategy,
            @Layout.HyphenationFrequency int hyphenationFrequency,
            boolean isJustified,
            @Nullable int[] indents);

    @CriticalNative
    private static native long nGetReleaseFunc();

    // populates LineBreaks and returns the number of breaks found
    //
    // the arrays inside the LineBreaks objects are passed in as well
    // to reduce the number of JNI calls in the common case where the
    // 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(
            /* non zero */ long nativePtr,

            // Inputs
            @NonNull char[] text,
            /* Non Zero */ long measuredTextPtr,
            @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);
}
+176 −0

File added.

Preview size limit exceeded, changes collapsed.

+15 −0
Original line number Diff line number Diff line
@@ -517,6 +517,21 @@ public class PrecomputedText implements Spannable {
        getMeasuredParagraph(paraIndex).getBounds(start - paraStart, end - paraStart, bounds);
    }

    /**
     * Returns a width of a character at offset
     *
     * @param offset an offset of the text.
     * @return a width of the character.
     * @hide
     */
    public float getCharWidthAt(@IntRange(from = 0) int offset) {
        Preconditions.checkArgument(0 <= offset && offset < mText.length(), "invalid offset");
        final int paraIndex = findParaIndex(offset);
        final int paraStart = getParagraphStart(paraIndex);
        final int paraEnd = getParagraphEnd(paraIndex);
        return getMeasuredParagraph(paraIndex).getCharWidthAt(offset - paraStart);
    }

    /**
     * Returns the size of native PrecomputedText memory usage.
     *
+200 −268

File changed.

Preview size limit exceeded, changes collapsed.

Loading