字体介绍

TextView

安卓通过 TextView 控件承载字体的显示,text 有3个可以设置字体样式的属性:textStyle、typeface、fontFamily。

textStyle 设置文字的样式,有3种样式,分别为 normal、bold、italic,粗体与斜体可以叠加。

typeface 设置 TextView 的字体

  • normal: 普通字体,默认使用的字体
  • sans: 非衬线字体
  • serif: 衬线字体
  • monospace: 等宽字体

FontFamily 表示 android 系统支持的一系列字体,每个字体都有一个别名

textStyle、typeface、fontFamily 三个属性的关系:

  1. 当我们设置 typeface 属性时,会将对应的属性值赋给 mTypefaceIndex,并把 mFontFamily 置为 null
  2. 当我们设置 fontFamily 属性时,首先会通过 appearance.getFont() 方法去获取字体文件,如果能获取到,则赋值给 mFontTypeface,如果获取不到,则通过 appearance.getString() 方法取获取当前字体别名并赋值给 mFontFamily
  3. 当我们设置 textStyle 属性时,会将获取的属性值赋给 mTextStyle

上述方法走完了,会调 setTypefaceFromAttrs() 方法,这个方法就是最终 TextView 设置字体的方法,我们来解析下这个方法:

private void setTypefaceFromAttrs(@Nullable Typeface typeface, @Nullable String familyName, 
@XMLTypefaceAttr int typefaceIndex, @Typeface.Style int style, @IntRange(from = -1, to = FontStyle.FONT_WEIGHT_MAX) int weight) {
    if (typeface == null && familyName != null) {
        // Lookup normal Typeface from system font map.
        inal Typeface normalTypeface = Typeface.create(familyName, Typeface.NORMAL);
        resolveStyleAndSetTypeface(normalTypeface, style, weight);
    } else if (typeface != null) {
        resolveStyleAndSetTypeface(typeface, style, weight);
    } else {  // both typeface and familyName is null.
        switch (typefaceIndex) {
        case SANS:
            resolveStyleAndSetTypeface(Typeface.SANS_SERIF, style, weight);
            break;
        case SERIF:
            resolveStyleAndSetTypeface(Typeface.SERIF, style, weight);
            break;
        case MONOSPACE:
            resolveStyleAndSetTypeface(Typeface.MONOSPACE, style, weight);
            break;
        case DEFAULT_TYPEFACE:
        default:
            resolveStyleAndSetTypeface(null, style, weight);
            break;
        }
    }
}
  1. 当 typeface 为空并且 familyName 不为空时,取 familyName 的字体
  2. 当 typeface 不为空并且 familyName 为空时,取 typeface 的字体
  3. 当 typeface 和 familyName 都为空,则根据 typefaceIndex 的值取相应的字体
  4. typeface ,familyName 和 typefaceIndex 在我们分析的 readTextAppearance 方法会被赋值
  5. resolveStyleAndSetTypefce 方法会进行字体和字体样式的设置
  6. style 是在 readTextAppearance 方法中赋值的,他和设置字体并不冲突

结论:fontFamily、typeface 属性用于字体设置,如果都设置了,优先使用 fontFamily 属性,typeface 属性不会生效。textStyle 用于字体样式设置,与字体设置不会产生冲突。

setTypeface

系统字体的设置最终会走向 TextView 的 setTyepface 重载方法:

//重载方法一
public void setTypeface(@Nullable Typeface tf) {
    if (mTextPaint.getTypeface() != tf) {
        //通过 mTextPaint 设置字体
        mTextPaint.setTypeface(tf);
        //刷新重绘
        if (mLayout != null) {
            nullLayouts();
            requestLayout();
            invalidate();
        }
    }
}

//重载方法二
public void setTypeface(@Nullable Typeface tf, @Typeface.Style int style) {
    if (style > 0) {
        if (tf == null) {
            tf = Typeface.defaultFromStyle(style);
        } else {
            tf = Typeface.create(tf, style);
        }
        //调用重载方法一,设置字体
        setTypeface(tf);
        int typefaceStyle = tf != null ? tf.getStyle() : 0;
        int need = style & ~typefaceStyle;
        //打开画笔的粗体和斜体
        mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
        mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
    } else {
        mTextPaint.setFakeBoldText(false);
        mTextPaint.setTextSkewX(0);
        setTypeface(tf);
    }
}

重载方法一:TextView 设置字体实际上就是操作 mTextPaint,mTextPaint 是 TextPaint 的类对象,继承自 Paint 即画笔,因此我们设置的字体实际上会通过调用画笔的方法来进行绘制。
重载方法二:相对于重载方法一,法二多传递了一个 textStyle 参数,主要用来标记粗体和斜体的:

  1. 如果设置了 textStyle ,进入第一个条件体,分情况:1、如果传进来的 tf 为 null ,则会根据传入的 style 去获取 Typeface 字体,2、如果不为 null ,则会根据传入的 tf 和 style 去获取 Typeface 字体。设置好字体后,接下来还会打开画笔的粗体和斜体设置。
  2. 如果没有设置 textStyle,则只会设置字体,并把画笔的粗斜体设置置为 false 和 0。

TextView 设置字体和字体样式最终都是通过画笔来完成的。

Typeface

Typeface 负责 Android 字体的加载以及对上层提供相关字体 API 的调用

Typeface 对上层开放调用的一些方法:

  1. create(family: Typeface!, style: Int):通过 Typeface 和 Style 获取新的 Typeface
  2. create(familyName: String!, style: Int):通过字体名称和 Style 获取字体
  3. create(family: Typeface?, weight: Int, italic: Boolean):通过 Typeface 、weight(粗体) 和 italic(斜体) 获取新的 Typeface。
  4. createFromAsset(mgr: AssetManager!, path: String!):通过 AssetManager 和对应字体路径获取字体。
  5. createFromFile(file: File?):通过字体文件获取字体
  6. createFromFile(path: String?):通过字体路径获取字体

安卓字体加载原理

安卓5.0以上系统的字体加载原理

Java 层

安卓字体起作用主要的是 android.graphics.Typeface 类,其主要负责字体加载以及提供创建字体功能的调用。

在 Android 启动的过程中,ZygoteInit 类中的 main() 方法会调用加载方法 preload(),对各种类、链接库、资源等进行初始化。

//主要用于加载并初始化各种类、链接库、资源等。
static void preload() {
    Log.d(TAG, "begin preload");
    //Systrace开始tag
    Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "BeginIcuCachePinning");
    //开始Icu缓存开销
    beginIcuCachePinning();
    //Systrace结束tag
    Trace.traceEnd(Trace.TRACE_TAG_DALVIK);
    Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadClasses");
    //预加载Classes
    preloadClasses();
    Trace.traceEnd(Trace.TRACE_TAG_DALVIK);
    Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadResources");
    //预加载resources
    preloadResources();
    Trace.traceEnd(Trace.TRACE_TAG_DALVIK);
    Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadOpenGL");
    //预加载openGL
    preloadOpenGL();
    Trace.traceEnd(Trace.TRACE_TAG_DALVIK);
    //加载分享库
    preloadSharedLibraries();
    //加载文本资源
    preloadTextResources();
    // Ask the WebViewFactory to do any initialization that must run in the zygote process,
    // for memory sharing purposes.、
    WebViewFactory.prepareWebViewInZygote();
    endIcuCachePinning();
    warmUpJcaProviders();
    Log.d(TAG, "end preload");
}

其中 preloadClasses() 方法会读取 preloaded-classes 文件中的内容,加载并初始化一些系统常用的 API 类,包括 Typeface 类。

/**
 * Performs Zygote process initialization. Loads and initializes
 * commonly used classes.
 *
 * Most classes only cause a few hundred bytes to be allocated, but
 * a few will allocate a dozen Kbytes (in one case, 500+K).
 */
private static void preloadClasses() {

    ...    

    InputStream is;
    try {
        is = new FileInputStream(PRELOADED_CLASSES);
    } catch (FileNotFoundException e) {
        Log.e(TAG, "Couldn't find " + PRELOADED_CLASSES + ".");
        return;
    }

    ...

    try {
    BufferedReader br
        = new BufferedReader(new InputStreamReader(is), 256);

    int count = 0;
    String line;
    while ((line = br.readLine()) != null) {
        // Skip comments and blank lines.
        line = line.trim();
        if (line.startsWith("#") || line.equals("")) {
            continue;
        }

        Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadClass " + line);
        try {
            if (false) {
                Log.v(TAG, "Preloading " + line + "...");
            }
            // Load and explicitly initialize the given class. Use
            // Class.forName(String, boolean, ClassLoader) to avoid repeated stack lookups
            // (to derive the caller's class-loader). Use true to force initialization, and
            // null for the boot classpath class-loader (could as well cache the
            // class-loader of this class in a variable).
            Class.forName(line, true, null);
            count++;

    ...

}

Android 通过反射机制加载 Typeface 类,加载的同时会调用类中 static 方法块。在 static 方法块中,最终通过调用 Native 层方法 nativeCreateFromTypeface() 来初始化系统字体并且设置默认的系统字体和字体样式。

static {
    //初始化系统字体
    init();
    // 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),
    };
}

public static Typeface create(String familyName, int style) {
    if (sSystemFontMap != null) {
        return create(sSystemFontMap.get(familyName), style);
    }
    return null;
}

public static Typeface create(Typeface family, int style) {
    ...
    typeface = new Typeface(nativeCreateFromTypeface(ni, style));
    ...
    return typeface;
}

其中初始化了 sDefaults 中的默认字体,包含4种style:normal,bold,italic,bolditalic。代码块,下面是init() 的代码。


/*
 * (non-Javadoc)
 *
 * This should only be called once, from the static class initializer block.
 */
private static void init() {
    // Load font config and initialize Minikin state
    //获取系统字体配置文件位置放置于system/etc目录下
    File systemFontConfigLocation = getSystemFontConfigLocation();
    //获取配置文件fonts.xml
    File configFilename = new File(systemFontConfigLocation, FONTS_CONFIG);
    //以下代码是对fonts.xml的解析,即是对系统字体的解析
    try {
        FileInputStream fontsIn = new FileInputStream(configFilename);
        FontListParser.Config fontConfig = FontListParser.parse(fontsIn);

        Map<String, ByteBuffer> bufferForPath = new HashMap<String, ByteBuffer>();
        //用来承载fonts.xml中的每个family节点
        List<FontFamily> familyList = new ArrayList<FontFamily>();
        // Note that the default typeface is always present in the fallback list;
        // this is an enhancement from pre-Minikin behavior.
        //从每个family节点中解析字体样式,这里解析系统默认字体
        for (int i = 0; i < fontConfig.families.size(); i++) {
            FontListParser.Family f = fontConfig.families.get(i);
            if (i == 0 || f.name == null) {
                familyList.add(makeFamilyFromParsed(f, bufferForPath));
            }
        }
        //系统默认字体集合
        sFallbackFonts = familyList.toArray(new FontFamily[familyList.size()]);
        //设置默认系统字体
        setDefault(Typeface.createFromFamilies(sFallbackFonts));
        //这里加载系统字体,包括默认字体
        Map<String, Typeface> systemFonts = new HashMap<String, Typeface>();
        for (int i = 0; i < fontConfig.families.size(); i++) {
            Typeface typeface;
            FontListParser.Family f = fontConfig.families.get(i);
            if (f.name != null) {
                if (i == 0) {
                    // The first entry is the default typeface; no sense in
                    // duplicating the corresponding FontFamily.
                    typeface = sDefaultTypeface;
                } else {
                    //从每个family节点中解析字体
                    FontFamily fontFamily = makeFamilyFromParsed(f, bufferForPath);
                    FontFamily[] families = { fontFamily };
                    typeface = Typeface.createFromFamiliesWithDefault(families);
                }
                //解析的字体添加到系统字体中
                systemFonts.put(f.name, typeface);
            }
        }
        //通过权重别号解析字体,别名必须与字体对应
        for (FontListParser.Alias alias : fontConfig.aliases) {
            Typeface base = systemFonts.get(alias.toName);
            Typeface newFace = base;
            int weight = alias.weight;
            if (weight != 400) {
                newFace = new Typeface(nativeCreateWeightAlias(base.native_instance, weight));
            }
            systemFonts.put(alias.name, newFace);
        }
        //系统字体集合
        sSystemFontMap = systemFonts;

    } catch (RuntimeException e) {
        Log.w(TAG, "Didn't create default family (most likely, non-Minikin build)", e);
        // TODO: normal in non-Minikin case, remove or make error when Minikin-only
    } catch (FileNotFoundException e) {
        Log.e(TAG, "Error opening " + configFilename, e);
    } catch (IOException e) {
        Log.e(TAG, "Error reading " + configFilename, e);
    } catch (XmlPullParserException e) {
        Log.e(TAG, "XML parse exception for " + configFilename, e);
    }
}

可以看到,init() 中加载了三种字体,系统默认字体、系统中所有字体、设置别名的字体。它们加载的主要代码涉及以下方法。

//通过family节点解析FontFamily
private static FontFamily makeFamilyFromParsed(FontListParser.Family family,
        Map<String, ByteBuffer> bufferForPath) {
    //这里的lang表示国家缩写,variant表示字体的排列格式一般有compact与elegant两种
    FontFamily fontFamily = new FontFamily(family.lang, family.variant);
    for (FontListParser.Font font : family.fonts) {
        ByteBuffer fontBuffer = bufferForPath.get(font.fontName);
        if (fontBuffer == null) {
            try (FileInputStream file = new FileInputStream(font.fontName)) {
                FileChannel fileChannel = file.getChannel();
                long fontSize = fileChannel.size();
                fontBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fontSize);
                bufferForPath.put(font.fontName, fontBuffer);
            } catch (IOException e) {
                Log.e(TAG, "Error mapping font file " + font.fontName);
                continue;
            }
        }
        if (!fontFamily.addFontWeightStyle(fontBuffer, font.ttcIndex, font.axes,
                font.weight, font.isItalic)) {
            Log.e(TAG, "Error creating font " + font.fontName + "#" + font.ttcIndex);
        }
    }
    return fontFamily;
}
/*
以下是通过不同的格式解析出不同的family
*/
public FontFamily() {
    mNativePtr = nCreateFamily(null, 0);
    if (mNativePtr == 0) {
        throw new IllegalStateException("error creating native FontFamily");
    }
}

public FontFamily(String lang, String variant) {
    int varEnum = 0;
    if ("compact".equals(variant)) {
        varEnum = 1;
    } else if ("elegant".equals(variant)) {
        varEnum = 2;
    }
    mNativePtr = nCreateFamily(lang, varEnum);
    if (mNativePtr == 0) {
        throw new IllegalStateException("error creating native FontFamily");
    }
}

@Override
protected void finalize() throws Throwable {
    try {
        nUnrefFamily(mNativePtr);
    } finally {
        super.finalize();
    }
}

public boolean addFont(String path, int ttcIndex) {
    try (FileInputStream file = new FileInputStream(path)) {
        FileChannel fileChannel = file.getChannel();
        long fontSize = fileChannel.size();
        ByteBuffer fontBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fontSize);
        return nAddFont(mNativePtr, fontBuffer, ttcIndex);
    } catch (IOException e) {
        Log.e(TAG, "Error mapping font file " + path);
        return false;
    }
}

public boolean addFontWeightStyle(ByteBuffer font, int ttcIndex, List<FontListParser.Axis> axes,
        int weight, boolean style) {
    return nAddFontWeightStyle(mNativePtr, font, ttcIndex, axes, weight, style);
}

public boolean addFontFromAsset(AssetManager mgr, String path) {
    return nAddFontFromAsset(mNativePtr, mgr, path);
}

private static native long nCreateFamily(String lang, int variant);
private static native void nUnrefFamily(long nativePtr);
private static native boolean nAddFont(long nativeFamily, ByteBuffer font, int ttcIndex);
private static native boolean nAddFontWeightStyle(long nativeFamily, ByteBuffer font,
        int ttcIndex, List<FontListParser.Axis> listOfAxis,
        int weight, boolean isItalic);
private static native boolean nAddFontFromAsset(long nativeFamily, AssetManager mgr,
        String path);
/**
 * Create a new typeface from an array of font families.
 *
 * @param families array of font families
 * @hide
 */
//通过FontFamily解析创建字体
public static Typeface createFromFamilies(FontFamily[] families) {
    long[] ptrArray = new long[families.length];
    for (int i = 0; i < families.length; i++) {
        ptrArray[i] = families[i].mNativePtr;
    }
    return new Typeface(nativeCreateFromArray(ptrArray));
}

/**
 * Create a new typeface from an array of font families, including
 * also the font families in the fallback list.
 *
 * @param families array of font families
 * @hide
 */
//通过FontFamily解析创建字体
public static Typeface createFromFamiliesWithDefault(FontFamily[] families) {
    long[] ptrArray = new long[families.length + sFallbackFonts.length];
    for (int i = 0; i < families.length; i++) {
        ptrArray[i] = families[i].mNativePtr;
    }
    for (int i = 0; i < sFallbackFonts.length; i++) {
        ptrArray[i + families.length] = sFallbackFonts[i].mNativePtr;
    }
    return new Typeface(nativeCreateFromArray(ptrArray));
}

系统通过解析 fonts.xml 字体配置文件,然后接受 Native 层方法回调上来的值。来创建指定的字体,保存在 sSystemFontMap 中。相关 native 方法列表以及注册如下。

private static native long nativeCreateFromTypeface(long native_instance, int style);
private static native long nativeCreateWeightAlias(long native_instance, int weight);
private static native void nativeUnref(long native_instance);
private static native int  nativeGetStyle(long native_instance);
private static native long nativeCreateFromArray(long[] familyArray);
private static native void nativeSetDefault(long native_instance);

///

static const JNINativeMethod gTypefaceMethods[] = {
    { "nativeCreateFromTypeface", "(JI)J", (void*)Typeface_createFromTypeface },
    { "nativeCreateWeightAlias",  "(JI)J", (void*)Typeface_createWeightAlias },
    { "nativeUnref",              "(J)V",  (void*)Typeface_unref },
    { "nativeGetStyle",           "(J)I",  (void*)Typeface_getStyle },
    { "nativeCreateFromArray",    "([J)J", (void*)Typeface_createFromArray },
    { "nativeSetDefault",         "(J)V",  (void*)Typeface_setDefault },
};

int register_android_graphics_Typeface(JNIEnv* env) {
    return RegisterMethodsOrDie(env, "android/graphics/Typeface", gTypefaceMethods,NELEM(gTypefaceMethods));
}

Native 层

在 Typeface 中,所有最终操作到加载字体的部分,全部都是 native 的方法。而 native 方法就是以效率著称的,这里只需要保证不频繁的调用(Typeface 已经做好了缓存,不会频繁的调用),基本上也不会存在效率的问题。

字体配置文件

<familyset version="22">
    <!-- first font is default -->
    <family name="sans-serif">
        <font weight="100" style="normal">Roboto-Thin.ttf</font>
        <font weight="100" style="italic">Roboto-ThinItalic.ttf</font>
        <font weight="300" style="normal">Roboto-Light.ttf</font>
        <font weight="300" style="italic">Roboto-LightItalic.ttf</font>
        <font weight="400" style="normal">Roboto-Regular.ttf</font>
        <font weight="400" style="italic">Roboto-Italic.ttf</font>
        <font weight="500" style="normal">Roboto-Medium.ttf</font>
        <font weight="500" style="italic">Roboto-MediumItalic.ttf</font>
        <font weight="900" style="normal">Roboto-Black.ttf</font>
        <font weight="900" style="italic">Roboto-BlackItalic.ttf</font>
        <font weight="700" style="normal">Roboto-Bold.ttf</font>
        <font weight="700" style="italic">Roboto-BoldItalic.ttf</font>
    </family>

    <!-- Note that aliases must come after the fonts they reference. -->
    <alias name="sans-serif-thin" to="sans-serif" weight="100" />
    <alias name="sans-serif-light" to="sans-serif" weight="300" />
    <alias name="sans-serif-medium" to="sans-serif" weight="500" />
    <alias name="sans-serif-black" to="sans-serif" weight="900" />
    <alias name="arial" to="sans-serif" />
    <alias name="helvetica" to="sans-serif" />
    <alias name="tahoma" to="sans-serif" />
    <alias name="verdana" to="sans-serif" />

...

    <!-- fallback fonts -->
    <family lang="und-Arab" variant="elegant">
        <font weight="400" style="normal">NotoNaskhArabic-Regular.ttf</font>
        <font weight="700" style="normal">NotoNaskhArabic-Bold.ttf</font>
    </family>
    <family lang="und-Arab" variant="compact">
        <font weight="400" style="normal">NotoNaskhArabicUI-Regular.ttf</font>
        <font weight="700" style="normal">NotoNaskhArabicUI-Bold.ttf</font>
    </family>
    <family lang="und-Ethi">
        <font weight="400" style="normal">NotoSansEthiopic-Regular.ttf</font>
        <font weight="700" style="normal">NotoSansEthiopic-Bold.ttf</font>
    </family>
    <!-- 简体中文字体 -->
    <family lang="zh-Hans">
        <font weight="400" style="normal">NotoSansSC-Regular.otf</font>
    </family>
    <!-- 繁体中文字体 -->
    <family lang="zh-Hant">
        <font weight="400" style="normal">NotoSansTC-Regular.otf</font>
    </family>

第一个 family 节点为系统默认字体,nameset 节点的各个 name 子节点定义可用的字体名称,fileset 节点的 file 子节点分别对应 normal、bold、italic、bold-italic 四种字体样式,如果 file 节点个数少于4个,相应字体会对应已有兄弟 file 节点的字体文件。family 属性中 lang 代表国家的缩写,系统在切换语言的时候会从加载的字体中匹配国家的缩写,从而调出对于的系统字体、variant 属性指的是字体的排列格式通常有compact(紧凑型)以及(简洁型)。

添加新字体的流程:

  1. 在frameworks/base/data/fonts/fonts.xml中添加字体节点
<family lang="my">
    <font weight="400" style="normal">newFontFile.ttf</font>
</family>
  1. 在frameworks/base/data/fonts/fonts.mk的最后加入新加的字体文件
PRODUCT_COPY_FILES := \
    frameworks/base/data/fonts/fonts.xml:$(TARGET_COPY_OUT_SYSTEM)/etc/fonts.xml

PRODUCT_PACKAGES := \
    DroidSansFallback.ttf \
    DroidSansMono.ttf \
    AndroidClock.ttf \
    DINPro-Black.otf \
    DINPro-Bold.otf \
    DINPro-Light.otf \
    DINPro-Medium.otf \
    DINPro-Regular.otf \
    Flyme-Light.ttf \
    newFontFile.ttf
  1. 在frameworks/base/data/fonts/Android.mk的font_src_files最后加入新加的字体文件
font_src_files := \
    AndroidClock.ttf \
    Flyme-Light.ttf \
    newFontFile.ttf
  1. 将下载的字体放入frameworks/base/data/fonts下

参考博客

Android字体系列
浅析Android字体加载原理