安卓字体解析
字体介绍
TextView
安卓通过 TextView 控件承载字体的显示,text 有3个可以设置字体样式的属性:textStyle、typeface、fontFamily。
textStyle 设置文字的样式,有3种样式,分别为 normal、bold、italic,粗体与斜体可以叠加。
typeface 设置 TextView 的字体
- normal: 普通字体,默认使用的字体
- sans: 非衬线字体
- serif: 衬线字体
- monospace: 等宽字体
FontFamily 表示 android 系统支持的一系列字体,每个字体都有一个别名
textStyle、typeface、fontFamily 三个属性的关系:
- 当我们设置 typeface 属性时,会将对应的属性值赋给 mTypefaceIndex,并把 mFontFamily 置为 null
- 当我们设置 fontFamily 属性时,首先会通过 appearance.getFont() 方法去获取字体文件,如果能获取到,则赋值给 mFontTypeface,如果获取不到,则通过 appearance.getString() 方法取获取当前字体别名并赋值给 mFontFamily
- 当我们设置 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;
}
}
}
- 当 typeface 为空并且 familyName 不为空时,取 familyName 的字体
- 当 typeface 不为空并且 familyName 为空时,取 typeface 的字体
- 当 typeface 和 familyName 都为空,则根据 typefaceIndex 的值取相应的字体
- typeface ,familyName 和 typefaceIndex 在我们分析的 readTextAppearance 方法会被赋值
- resolveStyleAndSetTypefce 方法会进行字体和字体样式的设置
- 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 参数,主要用来标记粗体和斜体的:
- 如果设置了 textStyle ,进入第一个条件体,分情况:1、如果传进来的 tf 为 null ,则会根据传入的 style 去获取 Typeface 字体,2、如果不为 null ,则会根据传入的 tf 和 style 去获取 Typeface 字体。设置好字体后,接下来还会打开画笔的粗体和斜体设置。
- 如果没有设置 textStyle,则只会设置字体,并把画笔的粗斜体设置置为 false 和 0。
TextView 设置字体和字体样式最终都是通过画笔来完成的。
Typeface
Typeface 负责 Android 字体的加载以及对上层提供相关字体 API 的调用
Typeface 对上层开放调用的一些方法:
create(family: Typeface!, style: Int)
:通过 Typeface 和 Style 获取新的 Typefacecreate(familyName: String!, style: Int)
:通过字体名称和 Style 获取字体create(family: Typeface?, weight: Int, italic: Boolean)
:通过 Typeface 、weight(粗体) 和 italic(斜体) 获取新的 Typeface。createFromAsset(mgr: AssetManager!, path: String!)
:通过 AssetManager 和对应字体路径获取字体。createFromFile(file: File?)
:通过字体文件获取字体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(紧凑型)以及(简洁型)。
添加新字体的流程:
- 在frameworks/base/data/fonts/fonts.xml中添加字体节点
<family lang="my">
<font weight="400" style="normal">newFontFile.ttf</font>
</family>
- 在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
- 在frameworks/base/data/fonts/Android.mk的font_src_files最后加入新加的字体文件
font_src_files := \
AndroidClock.ttf \
Flyme-Light.ttf \
newFontFile.ttf
- 将下载的字体放入frameworks/base/data/fonts下
参考博客
- 感谢你赐予我前进的力量