1.引言

最近老大安排一个任务,让我看看android 字体这块,将我们产品中的字体替换下。花了1.2天看懂,还得写篇文章,教程在组内进行分享。这次算是我进军Android系统的第一步。这篇文章基于Android 8.0

2.正题

2.1 font.xml字段描述

系统字体相关的文件、文件夹:fonts文件 和font.xml配置文件

系统字体存放在: System/fonts/文件夹下

系统字体配置文件 font.xml 存放在:System/etc目录下。font.xml如下所示:

Roboto-Thin.ttf
Roboto-ThinItalic.ttf
Roboto-Light.ttf
Roboto-LightItalic.ttf
Roboto-Regular.ttf
Roboto-Italic.ttf
Roboto-Medium.ttf
Roboto-MediumItalic.ttf
Roboto-Black.ttf
Roboto-BlackItalic.ttf
Roboto-Bold.ttf
Roboto-BoldItalic.ttf
NotoNaskhArabic-Regular.ttf
NotoNaskhArabic-Bold.ttf
NotoNaskhArabicUI-Regular.ttf
NotoNaskhArabicUI-Bold.ttf
NotoSansEthiopic-Regular.ttf
NotoSansEthiopic-Bold.ttf
NotoSansSC-Regular.otf
NotoSansTC-Regular.otf
family
name:源码中通过name去找到font族
lang: 国家语言
font
weight:字体的粗细
style: 字体类型 普通、加粗,斜体

index:从0开始,表示备用字体的序号,0优先加载

字体匹配规则:

当是中文的时候会寻找中文也就是zh的配置,其中zh-hans是简体,zh-hant是繁体。原生系统指定的备用字体是NotoSerifCJK-Regular.ttc。ttc是多种字体的组合字体,上述的字体是中日韩三国的综合字体。

Roboto是不支持加载中文字体的。我把下图标注的字体换成Roboto-Italic.ttf之后,发现字体还真的是能显示出斜体来。这是不是说明“ Roboto”支持中文斜体呢?

image.png

其实不是,我们把文件fonts中的字体都删除只保留Roboto的字体。发现中文字体集体乱码显示不出来。由此我们可以推断。当加载中文的时候,Roboto会主动去用NotoSerifCJK-Regular.ttc来加载中文。验证方法将NotoSerifCJK-Regular.ttc 添加到fonts文件中之后。中文就显示出来了。

删除font.xml的 zh-hans/zh-hant配置之后,发现依旧能正常显示中文。说明加载中文时,系统会自动加载NotoSerifCJK-Regular.ttc字体。

知道了这些规则之后。如何更改系统中文字体呢?

我们只需要替换如下的配置:

自己的ttf

//字体加粗的中文字体,不加这句,会以NotoSerifCJK来显示加粗字体
自己的ttf
NotoSansTC-Regular.otf

自己的ttf

到此为止,替换系统字体就会生效。

上文说到:weight==400是android系统中正常字体的粗细,weight==700表示是加粗的字体。系统是在哪里加载的字体呢??是不是我们更改下代码,就能达到weight==500的时候显示正常字体,weight==100的时候字体加粗呢?

TypeFace.java
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));//创建默认“sans-serif”的TypeFace
}
// Set up defaults and typefaces exposed in public API
DEFAULT = create((String) null, 0);//正常粗细的“sans-serif”字体
DEFAULT_BOLD = create((String) null, Typeface.BOLD);//粗体 “sans-serif”类型的字体
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));
}
}
点进Create方法,nativeCreateFromTypeface native 方法来初始化系统字体并且设置默认的系统字体以及字体样式。
public static Typeface create(Typeface family, @Style int style) {
if ((style & ~STYLE_MASK) != 0) {
style = NORMAL;
}
if (family == null) {
family = sDefaultTypeface;
}
// Return early if we're asked for the same face/style
if (family.mStyle == style) {
return family;
}
final long ni = family.native_instance;
Typeface typeface;
synchronized (sStyledCacheLock) {
SparseArray styles = sStyledTypefaceCache.get(ni);
if (styles == null) {
styles = new SparseArray(4);
sStyledTypefaceCache.put(ni, styles);
} else {
typeface = styles.get(style);
if (typeface != null) {
return typeface;
}
}
typeface = new Typeface(nativeCreateFromTypeface(ni, style));
styles.put(style, typeface);
}
return typeface;
}
/framwork/base/core/jni/android/graphics/Typeface.cpp#nativeCreateFromTypeface
static jlong Typeface_createFromTypeface(JNIEnv* env, jobject, jlong familyHandle, jint style) {
Typeface* family = toTypeface(familyHandle);
Typeface* face = Typeface::createRelative(family, (Typeface::Style)style);
// TODO: the following logic shouldn't be necessary, the above should always succeed.
// Try to find the closest matching font, using the standard heuristic
if (NULL == face) {
face = Typeface::createRelative(family, (Typeface::Style)(style ^ Typeface::kItalic));
}
for (int i = 0; NULL == face && i < 4; i++) {
face = Typeface::createRelative(family, (Typeface::Style)i);
}
return toJLong(face);
}
进一步调用真正的创建类:
/framwork/base/libs/hwui/hwui/Typeface.cpp# createRelative
Typeface* Typeface::createFromFamilies(std::vector<:shared_ptr>>&& families,
int weight, int italic) {
Typeface* result = new Typeface;
result->fFontCollection.reset(new minikin::FontCollection(families));
if (weight == RESOLVE_BY_FONT_TABLE || italic == RESOLVE_BY_FONT_TABLE) {
int weightFromFont;
bool italicFromFont;
const minikin::FontStyle defaultStyle;
const minikin::MinikinFont* mf =
families.empty()
? nullptr
: families[0]->getClosestMatch(defaultStyle).font->typeface().get();
if (mf != nullptr) {
SkTypeface* skTypeface = reinterpret_cast(mf)->GetSkTypeface();
const SkFontStyle& style = skTypeface->fontStyle();
weightFromFont = style.weight();
italicFromFont = style.slant() != SkFontStyle::kUpright_Slant;
} else {
// We can't obtain any information from fonts. Just use default values.
weightFromFont = SkFontStyle::kNormal_Weight;//默认值400
italicFromFont = false;
}
if (weight == RESOLVE_BY_FONT_TABLE) {
weight = weightFromFont;
}
if (italic == RESOLVE_BY_FONT_TABLE) {
italic = italicFromFont ? 1 : 0;
}
}

由上面的流程可以看到,系统默认的确还是400,加粗是在base-weight上加300.

上面我们知道了,TypeFace是系统加载默认字体的地方,通过jni调用告诉底层字体样式。也知道了代码中正常字体粗细是400.加粗是700。又有一个问题:TypeFace是在哪里加载的呢?

android系统启动是通过解析init.rc文件。进一步得到Zygote进程,ZygoteInit进程会产生System_Server进程。且Zygote进程会通过反射进入ZygoteInit.java main()方法。这是底层进入到java层的第一个方法。

main方法中调用了preload()方法:
image.png
preload又会调用preloadClass() 预加载基础类。
/**
* The path of a file that contains classes to preload.
*/
private static final String PRELOADED_CLASSES = "/system/etc/preloaded-classes";

preloaded-classes 是一个xml,里面写入了成千上万个全路径类名。存在于System/etc 下

好了,今天的问题都弄明白了。下一期分享开机动画的知识。