From d56d2e9a673330c74f6a1f68dde8e8b14f1b7a7e Mon Sep 17 00:00:00 2001 From: Kohsuke Yatoh Date: Thu, 12 Nov 2020 15:42:04 -0800 Subject: [PATCH] Retry "Support loading system font map from SharedMemory." This is the second try of commit c7a5b3a90db2f97c1d95332e9bb5b5c0f34084d7, which was reverted in commit 735d447d4807bb9440b2037bc67e35da95edb209 Changes: - Use reflection to update static final fields, because some apps and tests rely on Typeface.create() to return references to static final fields (which is a behavior resulted from the use of internal cache). - Make setSystemFontMap() thread safe. - Hold a reference to ByteBuffer so that the memory won't be unmapped. Bug: 172891184 Test: atest FrameworksCoreTests:TypefaceTest Test: atest CtsWidgetTestCases:android.widget.cts.TextViewPrecomputedTextTest Change-Id: I1dea26c08a8585ea10e866c063bef6d682d8b15e --- .../src/android/graphics/TypefaceTest.java | 76 +++++- graphics/java/android/graphics/Typeface.java | 247 +++++++++++++----- libs/hwui/jni/Typeface.cpp | 16 ++ 3 files changed, 255 insertions(+), 84 deletions(-) diff --git a/core/tests/coretests/src/android/graphics/TypefaceTest.java b/core/tests/coretests/src/android/graphics/TypefaceTest.java index cfed2cef9ce6f..5fa8c4fbaa565 100644 --- a/core/tests/coretests/src/android/graphics/TypefaceTest.java +++ b/core/tests/coretests/src/android/graphics/TypefaceTest.java @@ -23,6 +23,8 @@ import static org.junit.Assert.assertTrue; import android.content.Context; import android.content.res.AssetManager; import android.content.res.Resources; +import android.graphics.fonts.SystemFonts; +import android.os.SharedMemory; import androidx.test.InstrumentationRegistry; import androidx.test.filters.LargeTest; @@ -35,9 +37,9 @@ import com.android.frameworks.coretests.R; import org.junit.Test; import org.junit.runner.RunWith; -import java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.List; +import java.nio.ByteOrder; +import java.util.HashMap; +import java.util.Map; import java.util.Random; @RunWith(AndroidJUnit4.class) @@ -54,6 +56,10 @@ public class TypefaceTest { Typeface.create(Typeface.MONOSPACE, 0) }; + private static final int[] STYLES = { + Typeface.NORMAL, Typeface.BOLD, Typeface.ITALIC, Typeface.BOLD_ITALIC, + }; + @SmallTest @Test public void testBasic() throws Exception { @@ -64,6 +70,16 @@ public class TypefaceTest { assertTrue("basic", Typeface.MONOSPACE != null); } + @SmallTest + @Test + public void testDefaults() { + for (int style : STYLES) { + String msg = "style = " + style; + assertNotNull(msg, Typeface.defaultFromStyle(style)); + assertEquals(msg, style, Typeface.defaultFromStyle(style).getStyle()); + } + } + @SmallTest @Test public void testUnique() throws Exception { @@ -178,21 +194,57 @@ public class TypefaceTest { @SmallTest @Test public void testSerialize() throws Exception { - int size = Typeface.writeTypefaces(null, Arrays.asList(mFaces)); - ByteBuffer buffer = ByteBuffer.allocateDirect(size); - Typeface.writeTypefaces(buffer, Arrays.asList(mFaces)); - List copiedTypefaces = Typeface.readTypefaces(buffer); - assertNotNull(copiedTypefaces); - assertEquals(mFaces.length, copiedTypefaces.size()); - for (int i = 0; i < mFaces.length; i++) { - Typeface original = mFaces[i]; - Typeface copied = copiedTypefaces.get(i); + HashMap systemFontMap = new HashMap<>(); + Typeface.initSystemDefaultTypefaces(systemFontMap, SystemFonts.getRawSystemFallbackMap(), + SystemFonts.getAliases()); + SharedMemory sharedMemory = Typeface.serializeFontMap(systemFontMap); + Map copiedFontMap = + Typeface.deserializeFontMap(sharedMemory.mapReadOnly().order(ByteOrder.BIG_ENDIAN)); + assertEquals(systemFontMap.size(), copiedFontMap.size()); + for (String key : systemFontMap.keySet()) { + assertTrue(copiedFontMap.containsKey(key)); + Typeface original = systemFontMap.get(key); + Typeface copied = copiedFontMap.get(key); assertEquals(original.getStyle(), copied.getStyle()); assertEquals(original.getWeight(), copied.getWeight()); assertEquals(measureText(original, "hello"), measureText(copied, "hello"), 1e-6); } } + @SmallTest + @Test + public void testSetSystemFontMap() throws Exception { + Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + Resources res = context.getResources(); + Map fontMap = Map.of( + "sans-serif", Typeface.create(res.getFont(R.font.samplefont), Typeface.NORMAL), + "serif", Typeface.create(res.getFont(R.font.samplefont2), Typeface.NORMAL), + "monospace", Typeface.create(res.getFont(R.font.samplefont3), Typeface.NORMAL), + "sample", Typeface.create(res.getFont(R.font.samplefont4), Typeface.NORMAL), + "sample-italic", Typeface.create(res.getFont(R.font.samplefont4), Typeface.ITALIC)); + Typeface.setSystemFontMap(fontMap); + + // Test public static final fields + assertEquals(fontMap.get("sans-serif"), Typeface.DEFAULT); + assertEquals(Typeface.BOLD, Typeface.DEFAULT_BOLD.getStyle()); + assertEquals(fontMap.get("sans-serif"), Typeface.SANS_SERIF); + assertEquals(fontMap.get("serif"), Typeface.SERIF); + assertEquals(fontMap.get("monospace"), Typeface.MONOSPACE); + + // Test defaults + assertEquals(fontMap.get("sans-serif"), Typeface.defaultFromStyle(Typeface.NORMAL)); + for (int style : STYLES) { + String msg = "style = " + style; + assertNotNull(msg, Typeface.defaultFromStyle(style)); + assertEquals(msg, style, Typeface.defaultFromStyle(style).getStyle()); + } + + // Test create() + assertEquals(fontMap.get("sample"), Typeface.create("sample", Typeface.NORMAL)); + assertEquals( + fontMap.get("sample-italic"), Typeface.create("sample-italic", Typeface.ITALIC)); + } + private static float measureText(Typeface typeface, String text) { Paint paint = new Paint(); paint.setTypeface(typeface); diff --git a/graphics/java/android/graphics/Typeface.java b/graphics/java/android/graphics/Typeface.java index c58e5f36cde39..5743df5216bee 100644 --- a/graphics/java/android/graphics/Typeface.java +++ b/graphics/java/android/graphics/Typeface.java @@ -34,8 +34,12 @@ import android.graphics.fonts.FontVariationAxis; import android.graphics.fonts.SystemFonts; import android.os.Build; import android.os.ParcelFileDescriptor; +import android.os.SharedMemory; +import android.os.Trace; import android.provider.FontRequest; import android.provider.FontsContract; +import android.system.ErrnoException; +import android.system.OsConstants; import android.text.FontConfig; import android.util.Base64; import android.util.LongSparseArray; @@ -50,6 +54,7 @@ import dalvik.annotation.optimization.CriticalNative; import libcore.util.NativeAllocationRegistry; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileDescriptor; import java.io.IOException; @@ -57,6 +62,7 @@ import java.io.InputStream; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -79,19 +85,19 @@ public class Typeface { Typeface.class.getClassLoader(), nativeGetReleaseFunc()); /** The default NORMAL typeface object */ - public static final Typeface DEFAULT; + public static final Typeface DEFAULT = null; /** * The default BOLD typeface object. Note: this may be not actually be * bold, depending on what fonts are installed. Call getStyle() to know * for sure. */ - public static final Typeface DEFAULT_BOLD; + public static final Typeface DEFAULT_BOLD = null; /** The NORMAL style of the default sans serif typeface. */ - public static final Typeface SANS_SERIF; + public static final Typeface SANS_SERIF = null; /** The NORMAL style of the default serif typeface. */ - public static final Typeface SERIF; + public static final Typeface SERIF = null; /** The NORMAL style of the default monospace typeface. */ - public static final Typeface MONOSPACE; + public static final Typeface MONOSPACE = null; /** * The default {@link Typeface}s for different text styles. @@ -99,6 +105,7 @@ public class Typeface { * It shouldn't be changed for app wide typeface settings. Please use theme and font XML for * the same purpose. */ + @GuardedBy("SYSTEM_FONT_MAP_LOCK") @UnsupportedAppUsage(trackingBug = 123769446) static Typeface[] sDefaults; @@ -125,16 +132,28 @@ public class Typeface { private static final LruCache sDynamicTypefaceCache = new LruCache<>(16); private static final Object sDynamicCacheLock = new Object(); + + @GuardedBy("SYSTEM_FONT_MAP_LOCK") static Typeface sDefaultTypeface; - // Following two fields are not used but left for hiddenapi private list /** * sSystemFontMap is read only and unmodifiable. * Use public API {@link #create(String, int)} to get the typeface for given familyName. */ + @GuardedBy("SYSTEM_FONT_MAP_LOCK") @UnsupportedAppUsage(trackingBug = 123769347) - static final Map sSystemFontMap; + static final Map sSystemFontMap = new HashMap<>(); + // DirectByteBuffer object to hold sSystemFontMap's backing memory mapping. + @GuardedBy("SYSTEM_FONT_MAP_LOCK") + static ByteBuffer sSystemFontMapBuffer = null; + + // Lock to guard sSystemFontMap and derived default or public typefaces. + // sStyledCacheLock may be held while this lock is held. Holding them in the reverse order may + // introduce deadlock. + private static final Object SYSTEM_FONT_MAP_LOCK = new Object(); + + // This field is used but left for hiddenapi private list // We cannot support sSystemFallbackMap since we will migrate to public FontFamily API. /** * @deprecated Use {@link android.graphics.fonts.FontFamily} instead. @@ -187,8 +206,16 @@ public class Typeface { */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) private static void setDefault(Typeface t) { - sDefaultTypeface = t; - nativeSetDefault(t.native_instance); + synchronized (SYSTEM_FONT_MAP_LOCK) { + sDefaultTypeface = t; + nativeSetDefault(t.native_instance); + } + } + + private static Typeface getDefault() { + synchronized (SYSTEM_FONT_MAP_LOCK) { + return sDefaultTypeface; + } } /** Returns the typeface's weight value */ @@ -833,7 +860,7 @@ public class Typeface { style = NORMAL; } if (family == null) { - family = sDefaultTypeface; + family = getDefault(); } // Return early if we're asked for the same face/style @@ -902,7 +929,7 @@ public class Typeface { @IntRange(from = 1, to = 1000) int weight, boolean italic) { Preconditions.checkArgumentInRange(weight, 0, 1000, "weight"); if (family == null) { - family = sDefaultTypeface; + family = getDefault(); } return createWeightStyle(family, weight, italic); } @@ -944,7 +971,9 @@ public class Typeface { * @return the default typeface that corresponds to the style */ public static Typeface defaultFromStyle(@Style int style) { - return sDefaults[style]; + synchronized (SYSTEM_FONT_MAP_LOCK) { + return sDefaults[style]; + } } /** @@ -1137,40 +1166,142 @@ public class Typeface { } } + /** @hide */ + public static SharedMemory serializeFontMap(Map fontMap) + throws IOException, ErrnoException { + long[] nativePtrs = new long[fontMap.size()]; + // The name table will not be large, so let's create a byte array in memory. + ByteArrayOutputStream namesBytes = new ByteArrayOutputStream(); + int i = 0; + for (Map.Entry entry : fontMap.entrySet()) { + nativePtrs[i++] = entry.getValue().native_instance; + writeString(namesBytes, entry.getKey()); + } + int typefacesBytesCount = nativeWriteTypefaces(null, nativePtrs); + // int (typefacesBytesCount), typefaces, namesBytes + SharedMemory sharedMemory = SharedMemory.create( + "fontMap", Integer.BYTES + typefacesBytesCount + namesBytes.size()); + ByteBuffer writableBuffer = sharedMemory.mapReadWrite().order(ByteOrder.BIG_ENDIAN); + try { + writableBuffer.putInt(typefacesBytesCount); + int writtenBytesCount = nativeWriteTypefaces(writableBuffer.slice(), nativePtrs); + if (writtenBytesCount != typefacesBytesCount) { + throw new IOException(String.format("Unexpected bytes written: %d, expected: %d", + writtenBytesCount, typefacesBytesCount)); + } + writableBuffer.position(writableBuffer.position() + writtenBytesCount); + writableBuffer.put(namesBytes.toByteArray()); + } finally { + SharedMemory.unmap(writableBuffer); + } + sharedMemory.setProtect(OsConstants.PROT_READ); + return sharedMemory; + } + + // buffer's byte order should be BIG_ENDIAN. + /** @hide */ + @VisibleForTesting + public static Map deserializeFontMap(ByteBuffer buffer) throws IOException { + Map fontMap = new HashMap<>(); + int typefacesBytesCount = buffer.getInt(); + long[] nativePtrs = nativeReadTypefaces(buffer.slice()); + if (nativePtrs == null) { + throw new IOException("Could not read typefaces"); + } + buffer.position(buffer.position() + typefacesBytesCount); + for (long nativePtr : nativePtrs) { + String name = readString(buffer); + fontMap.put(name, new Typeface(nativePtr)); + } + return fontMap; + } + + private static String readString(ByteBuffer buffer) { + int length = buffer.getInt(); + byte[] bytes = new byte[length]; + buffer.get(bytes); + return new String(bytes); + } + + private static void writeString(ByteArrayOutputStream bos, String value) throws IOException { + byte[] bytes = value.getBytes(); + writeInt(bos, bytes.length); + bos.write(bytes); + } + + private static void writeInt(ByteArrayOutputStream bos, int value) { + // Write in the big endian order. + bos.write((value >> 24) & 0xFF); + bos.write((value >> 16) & 0xFF); + bos.write((value >> 8) & 0xFF); + bos.write(value & 0xFF); + } + + /** + * Deserialize font map and set it as system font map. This method should be called at most once + * per process. + */ + /** @hide */ + public static void setSystemFontMap(SharedMemory sharedMemory) + throws IOException, ErrnoException { + if (sSystemFontMapBuffer != null) { + throw new UnsupportedOperationException( + "Once set, buffer-based system font map cannot be updated"); + } + Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "setSystemFontMap"); + try { + sSystemFontMapBuffer = sharedMemory.mapReadOnly().order(ByteOrder.BIG_ENDIAN); + Map systemFontMap = deserializeFontMap(sSystemFontMapBuffer); + setSystemFontMap(systemFontMap); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS); + } + } + + /** @hide */ + @VisibleForTesting + public static void setSystemFontMap(Map systemFontMap) { + synchronized (SYSTEM_FONT_MAP_LOCK) { + sSystemFontMap.clear(); + sSystemFontMap.putAll(systemFontMap); + + // We can't assume DEFAULT_FAMILY available on Roboletric. + if (sSystemFontMap.containsKey(DEFAULT_FAMILY)) { + setDefault(sSystemFontMap.get(DEFAULT_FAMILY)); + } + + // Set up defaults and typefaces exposed in public API + // Use sDefaultTypeface here, because create(String, int) uses DEFAULT as fallback. + nativeForceSetStaticFinalField("DEFAULT", create(sDefaultTypeface, 0)); + nativeForceSetStaticFinalField("DEFAULT_BOLD", create(sDefaultTypeface, Typeface.BOLD)); + nativeForceSetStaticFinalField("SANS_SERIF", create("sans-serif", 0)); + nativeForceSetStaticFinalField("SERIF", create("serif", 0)); + nativeForceSetStaticFinalField("MONOSPACE", create("monospace", 0)); + + sDefaults = new Typeface[]{ + DEFAULT, + DEFAULT_BOLD, + create((String) null, Typeface.ITALIC), + create((String) null, Typeface.BOLD_ITALIC), + }; + + // A list of generic families to be registered in native. + // https://www.w3.org/TR/css-fonts-4/#generic-font-families + String[] genericFamilies = { + "serif", "sans-serif", "cursive", "fantasy", "monospace", "system-ui" + }; + + for (String genericFamily : genericFamilies) { + registerGenericFamilyNative(genericFamily, systemFontMap.get(genericFamily)); + } + } + } + static { final HashMap systemFontMap = new HashMap<>(); initSystemDefaultTypefaces(systemFontMap, SystemFonts.getRawSystemFallbackMap(), SystemFonts.getAliases()); - sSystemFontMap = Collections.unmodifiableMap(systemFontMap); - - // We can't assume DEFAULT_FAMILY available on Roboletric. - if (sSystemFontMap.containsKey(DEFAULT_FAMILY)) { - setDefault(sSystemFontMap.get(DEFAULT_FAMILY)); - } - - // Set up defaults and typefaces exposed in public API - DEFAULT = create((String) null, 0); - DEFAULT_BOLD = create((String) null, Typeface.BOLD); - SANS_SERIF = create("sans-serif", 0); - SERIF = create("serif", 0); - MONOSPACE = create("monospace", 0); - - sDefaults = new Typeface[] { - DEFAULT, - DEFAULT_BOLD, - create((String) null, Typeface.ITALIC), - create((String) null, Typeface.BOLD_ITALIC), - }; - - // A list of generic families to be registered in native. - // https://www.w3.org/TR/css-fonts-4/#generic-font-families - String[] genericFamilies = { - "serif", "sans-serif", "cursive", "fantasy", "monospace", "system-ui" - }; - - for (String genericFamily : genericFamilies) { - registerGenericFamilyNative(genericFamily, systemFontMap.get(genericFamily)); - } + setSystemFontMap(systemFontMap); } @Override @@ -1210,36 +1341,6 @@ public class Typeface { return Arrays.binarySearch(mSupportedAxes, axis) >= 0; } - /** - * Writes Typeface instances to the ByteBuffer and returns the number of bytes written. - * - *

If {@code buffer} is null, this method returns the number of bytes required to serialize - * the typefaces, without writing anything. - * @hide - */ - public static int writeTypefaces( - @Nullable ByteBuffer buffer, @NonNull List typefaces) { - long[] nativePtrs = new long[typefaces.size()]; - for (int i = 0; i < nativePtrs.length; i++) { - nativePtrs[i] = typefaces.get(i).native_instance; - } - return nativeWriteTypefaces(buffer, nativePtrs); - } - - /** - * Reads serialized Typeface instances from the ByteBuffer. Returns null on errors. - * @hide - */ - public static @Nullable List readTypefaces(@NonNull ByteBuffer buffer) { - long[] nativePtrs = nativeReadTypefaces(buffer); - if (nativePtrs == null) return null; - List typefaces = new ArrayList<>(nativePtrs.length); - for (long nativePtr : nativePtrs) { - typefaces.add(new Typeface(nativePtr)); - } - return typefaces; - } - private static native long nativeCreateFromTypeface(long native_instance, int style); private static native long nativeCreateFromTypefaceWithExactStyle( long native_instance, int weight, boolean italic); @@ -1270,4 +1371,6 @@ public class Typeface { @Nullable ByteBuffer buffer, @NonNull long[] nativePtrs); private static native @Nullable long[] nativeReadTypefaces(@NonNull ByteBuffer buffer); + + private static native void nativeForceSetStaticFinalField(String fieldName, Typeface typeface); } diff --git a/libs/hwui/jni/Typeface.cpp b/libs/hwui/jni/Typeface.cpp index dc066da36cbea..b88ffa6e79b86 100644 --- a/libs/hwui/jni/Typeface.cpp +++ b/libs/hwui/jni/Typeface.cpp @@ -217,6 +217,20 @@ static jlongArray Typeface_readTypefaces(JNIEnv *env, jobject, jobject buffer) { return result; } + +static void Typeface_forceSetStaticFinalField(JNIEnv *env, jclass cls, jstring fieldName, + jobject typeface) { + ScopedUtfChars fieldNameChars(env, fieldName); + jfieldID fid = + env->GetStaticFieldID(cls, fieldNameChars.c_str(), "Landroid/graphics/Typeface;"); + if (fid == 0) { + jniThrowRuntimeException(env, "Unable to find field"); + return; + } + env->SetStaticObjectField(cls, fid, typeface); +} + + /////////////////////////////////////////////////////////////////////////////// static const JNINativeMethod gTypefaceMethods[] = { @@ -237,6 +251,8 @@ static const JNINativeMethod gTypefaceMethods[] = { (void*)Typeface_registerGenericFamily }, { "nativeWriteTypefaces", "(Ljava/nio/ByteBuffer;[J)I", (void*)Typeface_writeTypefaces}, { "nativeReadTypefaces", "(Ljava/nio/ByteBuffer;)[J", (void*)Typeface_readTypefaces}, + { "nativeForceSetStaticFinalField", "(Ljava/lang/String;Landroid/graphics/Typeface;)V", + (void*)Typeface_forceSetStaticFinalField }, }; int register_android_graphics_Typeface(JNIEnv* env)