Android 系统默认字体替换方案
目标
将 Android 系统默认字体替换为阿里巴巴普惠体(Alibaba PuHuiTi)。
最终方案
通过fonts_customization.xml配合 product 分区预装字体文件,使用new-named-family覆盖系统sans-serif字体族。
文件结构
vendor/[product]/fonts/ ├── Android.mk # 编译规则,将字体文件和配置安装到 product 分区 ├── fonts.mk # 产品级 Makefile,声明需要编译的模块 ├── fonts_customization.xml # 字体定制配置 ├── Alibaba-PuHuiTi-Regular.otf # 常规字重字体文件 └── Alibaba-PuHuiTi-Medium.otf # 中等字重字体文件安装路径
| 文件 | 安装到设备的路径 |
|---|---|
Alibaba-PuHuiTi-Regular.otf | /product/fonts/ |
Alibaba-PuHuiTi-Medium.otf | /product/fonts/ |
fonts_customization.xml | /product/etc/ |
关键配置
1. fonts_customization.xml
<fonts-modificationversion="1"><familycustomizationType="new-named-family"name="sans-serif"><fontweight="400"style="normal">Alibaba-PuHuiTi-Regular.otf</font><fontweight="500"style="normal">Alibaba-PuHuiTi-Medium.otf</font><fontweight="700"style="normal">Alibaba-PuHuiTi-Medium.otf</font><!-- ... 其他字重映射 --></family></fonts-modification>核心要点:customizationType只能为new-named-family,name必须设为"sans-serif",直接覆盖系统默认字体族。系统的默认字体是在frameworks/base/core/res/res/values/config.xml 中的config_bodyFontFamily中定义的。
2. Android.mk
# Build the rest of font files as prebuilt. # $(1): The source file name in LOCAL_PATH. # It also serves as the module name and the dest file name. define build-one-font-module $(eval include $(CLEAR_VARS))\ $(eval LOCAL_MODULE := $(1))\ $(eval LOCAL_SRC_FILES := $(1))\ $(eval LOCAL_MODULE_CLASS := ETC)\ $(eval LOCAL_MODULE_TAGS := optional)\ $(eval LOCAL_MODULE_PATH := $(TARGET_OUT_PRODUCT)/fonts)\ $(eval LOCAL_PRODUCT_MODULE := true) \ $(eval include $(BUILD_PREBUILT)) endef font_src_files := \ Alibaba-PuHuiTi-Regular.otf \ Alibaba-PuHuiTi-Medium.otf $(foreach f, $(font_src_files), $(call build-one-font-module, $(f))) build-one-font-module := font_src_files := include $(CLEAR_VARS) LOCAL_MODULE := fonts_customization.xml LOCAL_MODULE_CLASS := ETC LOCAL_PREBUILT_MODULE_FILE := $(LOCAL_PATH)/fonts_customization.xml LOCAL_PRODUCT_MODULE := true include $(BUILD_PREBUILT)- 字体文件通过
BUILD_PREBUILT安装到$(TARGET_OUT_PRODUCT)/fonts fonts_customization.xml作为 ETC 模块安装到/product/etc/- 所有模块设置
LOCAL_PRODUCT_MODULE := true确保安装到 product 分区
3.fonts.mk
PRODUCT_PACKAGES := \ Alibaba-PuHuiTi-Regular.otf \ Alibaba-PuHuiTi-Medium.otf \ fonts_customization.xml4. device.mk
在产品 Makefile 中引入字体模块:
$(call inherit-product-if-exists, vendor/[product]/fonts/fonts.mk)遇到的问题及原因
问题:使用 overlay 方式修改 config_bodyFontFamily 不生效
方案描述
最初尝试两步配合:
fonts_customization.xml中定义自定义字体族name="puhuiti"- 通过 RRO(Runtime Resource Overlay)覆盖
frameworks/base中的config_bodyFontFamily为"puhuiti"
<!-- overlay/frameworks/base/core/res/res/values/config.xml --><stringname="config_bodyFontFamily"translatable="false">puhuiti</string>现象
adb shell cmd overlay list显示 overlay 已启用([x] android.auto_generated_rro_product__),但字体未生效。
原因
时序问题:系统字体加载发生在 Zygote 启动阶段,此时 RRO 可能还未生效。config_bodyFontFamily的值在字体系统初始化时读取,但 overlay 的应用晚于字体加载,导致系统仍使用默认的sans-serif。
解决方案
不依赖 overlay,直接在fonts_customization.xml中将name设为"sans-serif"覆盖系统默认字体族。这样字体替换在FontListParser解析阶段就完成了,不受 RRO 时序影响。
底层原理
customizationType 取值
源码位置:frameworks/base/graphics/java/android/graphics/fonts/FontCustomizationParser.java
目前唯一支持的值是"new-named-family"。其他任何值会抛出IllegalArgumentException。
覆盖机制
源码位置:frameworks/base/graphics/java/android/graphics/FontListParser.java
FontListParser.readFamilies()解析/system/etc/fonts.xml时的处理逻辑:
- 先解析
/product/etc/fonts_customization.xml,得到 OEM 自定义的 named families(Map 结构,key 为 name) - 遍历
fonts.xml中的 named family 时,检查 OEM Map 中是否存在同名项 - 如果存在同名项,跳过系统的 family,不加入结果列表
- 最后将 OEM 自定义的 families 全部追加到结果列表中
// FontListParser.java 关键逻辑if(!oemNamedFamilies.containsKey(name)){// OEM 有同名定义时,跳过系统的resultNamedFamilies.add(namedFamilyList);}// ...// 最后追加 OEM 自定义的resultNamedFamilies.addAll(oemNamedFamilies.values());因此当fonts_customization.xml中定义name="sans-serif"时,系统原有的sans-serif字体族会被完全替换为自定义字体。
验证方法
# 确认字体文件已安装adb shellls-la/product/fonts/Alibaba-PuHuiTi-*.otf# 确认配置文件已安装且内容正确adb shellcat/product/etc/fonts_customization.xml# 检查字体解析是否有错误adb logcat-d|grep-iE"FontCustomization|FontListParser|SystemFonts"视觉验证:打开设置或任意中文界面,阿里巴巴普惠体与默认 Noto Sans CJK 在字形上差异明显,肉眼可辨。