安卓修改大师智改工坊 · 字体与品牌落地专题
字体换对了,品牌才真正出现在屏幕上
设计稿里的字,落到手机上常常变成另一副样子:标题还是系统黑体,金额数字突然变宽,生僻字直接显示成方框。字体不是一张图,它要经过资源编译、样式继承、运行时回退和签名安装四道关。本文把这条流程拆成可以照着做的步骤,每一步都写清预期结果和失败怎么查。
了解智能修改电脑版
只需说话,就能让应用变成你想要的样子。
改字体是最像“设计”的一项改包工作,实际上它更接近工程。你得先确定字体文件的授权和字符集,再决定放进资源目录还是资产目录,然后用主题和样式把它铺到所有页面,最后为缺字准备一条退路。任何一步想当然,结果都会在某一台设备、某一个弹窗、某一个生僻字上暴露出来。
最常见的翻车有三种。第一种是编译就过不去:文件名带了大写字母,或者把字体集合文件硬改了扩展名,反编译回编时直接报资源错误。第二种是装上去只有一半页面变了:布局里写了样式的变了,代码里自绘的控件、弹窗里的文本、图表库的坐标轴还是旧字体。第三种最难查:大部分字正常,偏偏用户的名字、地名或者一个特殊符号变成了方框,测试时没人注意到,上线后截图才被发现。
这三种问题分别对应三个层次:资源层、样式层、回退层。资源层解决“字体能不能被系统认出来”,样式层解决“哪些文字用这套字体”,回退层解决“这套字体没有的字怎么办”。三层都过了,才轮到签名和安装验收。顺序不能乱,因为后一层的问题常常被前一层掩盖。
本文只讨论自有版权或已获得授权的应用。给自家产品换品牌字体、给内部工具统一标题字形、给已授权的客户应用做交付版本,都是正常的技术工作;请勿把同样的方法用于破解他人付费应用、去除授权校验或绕过安全机制。字体本身也是有版权的软件,只使用你确实获得嵌入授权的字库,这一点比任何技术细节都重要。
全文使用智能修改电脑版演示。它运行在 Windows 10/11 64 位环境,需要 .NET Framework 4.6.2 或更高版本,安装包约 6.86 MB。把 APK 拖进去,它会自动反编译、按需求修改代码和资源、回编、对齐、签名、校验,并可以自动安装到手机或模拟器上预览。我们要用的,是它的项目目录、附件系统、打包日志和安装预览,把“换字体”从一句模糊的需求,变成一组可以逐项确认的文件和配置。
阅读顺序建议按施工顺序来:先分清字体来源,再做授权与字符集清点,然后把文件送进项目、放进正确目录,接着处理样式覆盖和缺字兜底,最后回编签名、逐页验收。如果你只想解决某一个具体现象,也可以直接跳到后面的排查表,再回头补对应章节。
先分清三种字体来源:系统、资源、资产
在动手之前,先把“字体从哪来”这件事分清楚。Android 应用里的文字,字形可能来自三个地方:设备自带的系统字体、编译进资源表的资源字体、原样打包进资产目录的资产字体。三者的引用方式、体积代价和可控程度完全不同,改包时的处理方式也不同。
系统字体是设备自带的。你在布局里写 sans-serif、serif、sans-serif-medium,最终用哪套字形由厂商和系统版本决定。它不占安装包体积,兼容性最好,但品牌控制力最弱:同一份布局,在不同品牌手机上字宽、字重、行高都可能不一样。很多“设计稿和真机对不上”的争议,根源就在这里。
资源字体是把字体文件放进 res/font 目录,由构建工具编译成资源,再用 @font/名字 去引用。它支持在 XML 里按字重、字形组织成一个字体族,可以被主题、样式、布局属性直接使用。Android 8.0 起原生支持这种写法;更低的系统版本需要 AppCompat 兼容库配合,这也是改老包时必须先确认 minSdk 的原因。
资产字体是把文件原样放进 assets 目录,代码通过路径字符串加载,例如 fonts/brand_regular.ttf。构建工具不会编译它,所以文件名可以带大写、可以有子目录,但代码里的路径必须一个字符都不差。它无法在 XML 里直接引用,必须由代码设置 Typeface,适合自定义控件、动态换字体或者工程结构不方便改资源的场景。
还有一种常被提到的“可下载字体”:应用只声明字体提供方和字体名,运行时向系统服务请求。它听起来省体积,但依赖设备上的字体提供方服务,国内大量设备并不具备,网络不通时还会退回到默认字体。品牌字体要求确定性,不建议走这条路。
| 字体来源 |
典型位置与引用 |
适合与不适合 |
| 系统字体 |
不打包,写 sans-serif 等族名 |
适合正文兜底;不适合强调品牌 |
| 资源字体 |
res/font 下放文件,引用 @font/名字 |
适合全界面统一;需要处理低版本兼容 |
| 资产字体 |
assets 下放文件,代码按路径加载 |
适合自绘控件与动态切换;要自己管缓存 |
| 可下载字体 |
声明提供方,运行时请求 |
省体积但不确定;品牌字体不建议 |
图 1:同一份设计稿,字体来源不同,落到真机上的字宽、字重和行高都会不同。
改包场景还有一条额外约束:你拿到的是反编译出来的工程,不是原始源码。资源目录里可能已经有同名的 font 目录,assets 里可能已经躺着旧字体,代码里可能已经写死了旧路径。正确做法是先盘点、再增量,而不是新建一套平行结构。新增的字体文件要和原有文件区分命名,引用的地方要一处一处改到位,避免出现“新旧两套字体同时存在、哪套生效看运气”的局面。
还有一个容易忽略的边界:字体文件的格式。TTF 兼容面最广;OTF 在现代 Android 上也能用;TTC 是字体集合,一个文件里装着多个字重,直接改扩展名当单体字体用,往往只能读出一个,甚至解析失败。WOFF 和 WOFF2 是给网页用的,放进 res/font 不会被当成 Android 字体资源。选格式的原则很简单:交付给 Android 的,用 TTF 或单体 OTF;TTC 先拆成单体再放进去。
动手前清点:授权、字重、字符集与体积
字体文件不是随便下载一个就能塞进应用的。第一步永远是确认授权:这套字库是否允许嵌入应用、是否允许随安装包分发、是否允许子集化、是否允许修改。品牌方提供的字体文件通常附带授权说明,设计师给的字体包则未必包含商用嵌入权。确认不了授权,就不要继续往下做,换一套确定可用的字库,成本远低于事后处理纠纷。
第二步是列字重清单。一个成熟的品牌字体族通常有细体、常规、中黑、粗体等多个字重,每个字重是一个独立文件。界面里真正用到的往往只有三档:正文常规、强调中黑、标题粗体。先把这三档定下来,分别命名为 brand_regular、brand_medium、brand_bold,后面所有配置都围绕这三个名字展开,沟通和排查都会清楚很多。
第三步是字符集。中文字库动辄两万多字,全套打包进去,安装包会明显变大;只保留常用三千五百字,体积能降下来,但生僻人名、地名、古文、专业术语就可能缺字。这不是“选大选小”的问题,而是要拿应用里真实出现的文案去比对。把界面文案、本地 HTML、内置 JSON、服务端下发的字段都导出成一份字符清单,再和字体的字符映射表对照,缺哪些字一目了然。
字符集和体积的关系,可以用一张经验表来估算。注意这只是量级参考,具体取决于字形复杂度和压缩方式,最终以实际文件为准。
| 字库范围 |
大致体积 |
适用判断 |
| 拉丁字母与数字 |
几十到两百 KB |
只用于数字、金额、英文标题 |
| 常用汉字三千五 |
一到两 MB |
正文以常用字为主,需配兜底 |
| 全量中文 |
五到十 MB 起 |
人名地名多、不能缺字 |
| 每个额外字重 |
约等于再放一份 |
只保留真正用到的档位 |
如果决定做子集化,务必先看授权是否允许修改字体文件。允许的话,可以用 fontTools 之类的工具按字符清单裁剪。命令本身不复杂,关键是保留布局特性,否则连字、等宽数字、标点挤压这些细节会丢失。下面这条命令把字符清单里的字裁出来,并保留常用的排版特性,仅作示例,参数要按自己的文件调整。
pyftsubset Brand-Regular.ttf --text-file=chars.txt --output-file=brand_regular.ttf --layout-features='*'
裁剪之后必须重新核对一遍字符映射表,看看清单里的每个字是否都还在。字体工具默认会丢掉没有映射到字形的字符,清单里如果混进了换行、制表符或者全角空格,结果可能和预期不同。更稳妥的做法是先跑一遍对照脚本,把“清单里有、字体里没有”的字符打印出来,再决定是补字、换字库,还是交给系统兜底。
最后把这些结论写成一张交接单。交接单不需要很正式,一张表格就够:字体文件叫什么、授权范围是什么、包含哪些字重、覆盖哪些字符、准备用在哪几个页面、缺字时怎么兜底。改包时最容易出的问题,不是技术难,而是信息在传递中丢了。把这张表随需求一起提交,后面无论谁接手,都能按同一份约定验收。
| 交接项 |
要写清的内容 |
示例 |
| 字体文件 |
文件名、格式、字重 |
brand_regular.ttf,常规 400 |
| 授权范围 |
是否允许嵌入与子集化 |
品牌方书面授权,允许嵌入 |
| 字符覆盖 |
全量还是子集,缺哪些字 |
常用字加教材字表,缺字走系统 |
| 覆盖页面 |
标题、正文、数字、弹窗 |
首页、列表、详情、支付弹窗 |
| 兜底策略 |
缺字回退到哪套系统字体 |
中文回退系统黑体,符号回退系统 |
图 2:授权、字重、字符集、覆盖页面、兜底策略,五项写清再开工。
把项目建起来:字体文件怎么送进工作区
准备工作做完,进入实际操作。第一步不是急着改文件,而是把项目建在一个干净、可回溯的位置。打开智能修改电脑版,它会自动挑选工作盘,顺序是 D、E、F、G、C,取第一个可读写且剩余空间不少于 1GB 的盘,并在下面建立 tools 和 Project 两个子目录。如果你的机器盘符比较特殊,可以在参数设置里手动指定工作目录,避免中途因为空间不足失败。
工作目录定好之后,把准备好的 APK 直接拖进主窗口。软件会用 aapt 解析图标、应用名、包名、版本、SDK 和启动页,图标取最高密度的原图。这一步的预期结果是:列表里出现这个应用,名称、包名、版本号和安装包里的一致。如果名称显示成乱码或者图标是空白,先不要往下走,检查 APK 是否完整、是否为分卷包的一部分,必要时换一个完整包重新导入。
导入后,每个项目会在 Project 下得到一个 8 位随机命名的目录。目录里有 config.ini 和 history.ini,保存项目配置和历史记录;有 source.apk,是原始安装包的副本;还有反编译出来的 apktool 工程目录和 apktool.log。这个结构很重要:原始包始终保留,改坏了可以回到 source.apk 重来,不会被中间产物覆盖。反编译速度可以做参考,12 MB 左右的包实测约 3 秒;如果超过 10 分钟仍未结束,软件会中断并报错,这时去看 apktool.log 的最后几十行。
接下来进入详情页写需求。输入框旁边有 3000 条话术,分 6 类、每类 500 条,其中界面美化类与字体、配色、图标相关。点选一条就会填进输入框,你可以在此基础上改成自己的话。字体这件事有很强的上下文,套用现成话术只能开个头,真正要写清的是:用哪个字体文件、放在资源目录还是资产目录、覆盖哪些页面、缺字怎么兜底、原有字体要不要保留。
一段合格的字体需求,可以按下面这个结构写。它不要求文采,只要求每一项都能被验证。
需求示例:给自有应用换品牌字体
1. 附件里的 brand_regular.ttf、brand_medium.ttf、brand_bold.ttf 是已获嵌入授权的品牌字体,只用于本应用。
2. 放到资源字体目录,按 400、500、700 三个字重组织成一个字体族,统一命名。
3. 全局正文用常规字重,标题用粗体,按钮和强调文字用中黑;主题默认文本样式一起覆盖。
4. 首页金额、列表金额、图表坐标轴的数字要对齐,开启等宽数字特性。
5. 中文缺字时回退系统字体,不要出现方框;保留系统表情符号。
6. 不要改动包名、版本号和现有功能,改完输出安装包并保留打包日志。
字体文件不要粘贴成文本,也不要把二进制内容塞进输入框。用详情页的附件系统:一次可以多选文件,每个文件都要写用途,用途不少于 10 个字。软件会校验每个附件是否存在、不是目录、不是 0 字节、可读,然后把它们拼成“序号 + 路径 + 说明”随需求一起发出。这个校验看起来琐碎,实际上拦住过不少“选错文件”“选到快捷方式”“文件还没同步完”的低级错误。
附件说明要写到能独立看懂。不要写“字体文件”,而要写“品牌字体常规字重,用于正文,已获嵌入授权”;不要写“字表”,而要写“教材用字清单,用于核对缺字,共三页”。这些说明会和需求一起进入右侧的 AI 工作区,写得越具体,改出来的结果越接近你的预期。history.ini 里只保留你的原话,附件路径和说明不会混进历史正文,后续回填时不会把无关内容带回来。
检查完附件和需求,点“立刻修改”。软件会把需求原文和日期写进 history.ini,然后把需求送进右侧的 AI 工作区执行。执行期间,主窗口每 2 秒轮询一次项目目录,AI 完成后会留下一个完成标志文件,主窗口读到就删除,等待上限是 1 小时。这个机制的好处是你可以切去做别的事,不用盯着进度条;坏处是不要在中途手动改工程文件,否则 AI 的修改和你的修改会互相覆盖。
等待期间可以看什么?先看 apktool.log,确认反编译阶段没有资源解析错误;如果日志里出现字体文件相关的报错,说明文件本身或命名有问题,早点停下来比等一小时更划算。再看项目目录里是否出现了新的资源文件,确认改动确实落在工程里,而不是只写进了聊天记录。改完之后不要急着安装,先按下一节检查资源放置是否正确,再进入打包。
“以前换字体最怕漏页面,装上去才发现设置页和支付弹窗还是旧字形。现在我把字体文件和一张页面清单一起交进去,改完先看这三个页面,再全量走一遍。”
—— 阿泽 · 消费电子品牌 Android 开发
资源放置:res/font 与 assets 两条路的具体写法
字体文件进工程之后,放在哪里决定了后面怎么引用。两条路各有明确的规则,混着用最容易出问题。先把资源字体这条路走通:在 app 模块的 res 下找到或新建 font 目录,把 TTF 或单体 OTF 文件放进去。文件名只能用小写字母、数字和下划线,不能有大写字母、中文、空格或连字符,这一点和 drawable、layout 的命名规则一致。
三个字重就是三个文件:brand_regular.ttf、brand_medium.ttf、brand_bold.ttf。为了让系统知道它们属于同一个字体族,需要再建一个 XML,通常命名为 brand_family.xml,同样放在 res/font 下。这个 XML 不包含字形,只负责把字重和文件对应起来。写法如下,注意属性名和命名空间不要写错。
<font-family xmlns:android="http://schemas.android.com/apk/res/android">
<font android:fontStyle="normal" android:fontWeight="400" android:font="@font/brand_regular" />
<font android:fontStyle="normal" android:fontWeight="500" android:font="@font/brand_medium" />
<font android:fontStyle="normal" android:fontWeight="700" android:font="@font/brand_bold" />
</font-family>
写进样式时,引用字体族名而不是单个文件:把 android:fontFamily 设成 @font/brand_family,系统会按控件请求的字重去挑对应文件。如果你只引用了 @font/brand_regular,那么样式里写 700 也不会自动变粗,系统只能做合成加粗,边缘会发虚。反过来,如果字体族里只登记了 400 和 700,而样式请求 500,系统会在可用的档位里找最接近的,结果可能和你以为的不一样。所以字重档位要么补齐,要么在样式里只用已登记的档位。
资源字体的引用位置有三层:控件属性、样式、主题。控件上直接写 android:fontFamily="@font/brand_family",只影响这一个控件;写在样式里,所有引用该样式的控件生效;写在主题的默认文本样式里,才会影响那些没有显式指定字体的控件。改包时常见的“改了样式但页面没变”,往往是布局里的控件自己写了字体属性,优先级比样式高,必须逐处检查。
版本兼容要提前确认。res/font 这套写法在 Android 8.0 及以上是原生支持的;如果应用的 minSdk 更低,又没有引入 AppCompat,低版本设备可能读不到资源字体。AppCompat 提供了向下兼容的字体加载能力,但布局里要用 app:fontFamily,或者在代码里通过兼容接口加载。改老包之前,先在 APK 信息里看清楚 minSdk,再决定是走资源字体还是退到资产字体。
再看资产字体这条路。把字体文件放进 assets 下的一个子目录,例如 assets/fonts/brand_regular.ttf。构建时文件原样保留,文件名大小写和目录层级都不变。代码里用 Typeface.createFromAsset 加载,路径必须和实际文件完全一致,包括大小写。Android 的资产文件系统区分大小写,Windows 上开发时可能没感觉,打包到设备上就会抛“字体资产找不到”的异常。
val tf = Typeface.createFromAsset(assets, "fonts/brand_regular.ttf")
textView.typeface = tf
资产字体有两个必须自己处理的点。第一是缓存:Typeface.createFromAsset 每次调用都会读文件、解析字形,放在列表里逐项设置会明显卡顿,正确做法是在应用启动或首次使用时加载一次,保存成单例,后续复用。第二是释放:自定义控件如果在 onDetachedFromWindow 里没有清掉引用,频繁创建销毁的页面会积累内存。字体对象本身不大,但组合起来的画笔、布局和缓存可能不小。
两条路怎么选,可以按下面的表来判断。核心原则是:能在 XML 里统一配置的,优先资源字体;必须由代码动态决定的,才用资产字体。同一个应用里两条路可以共存,但同一个字体族最好只走一条,避免出现两套名字、两套缓存、两套行为。
| 对比项 |
资源字体 |
资产字体 |
| 引用方式 |
@font/名字,XML 直接引用 |
代码按路径加载,XML 无法引用 |
| 命名限制 |
只能小写字母、数字、下划线 |
保留原名,可带大写与子目录 |
| 系统版本 |
Android 8.0 起原生,低版本需兼容库 |
各版本通用 |
| 适合场景 |
全界面统一、主题与样式覆盖 |
自绘控件、动态换字体、WebView 资源 |
| 常见风险 |
命名不合规导致回编失败 |
路径大小写不一致导致运行时找不到 |
如果原应用已经有一套字体资源,先盘点再动手。可以用 aapt 列出安装包里的资源,或者直接看反编译工程。命令行的办法是在工具链目录里执行 aapt dump resources,把输出重定向到文本,再搜 font 关键字;Windows 下也可以配合 findstr 过滤。看清楚现有字体叫什么、被哪些样式引用、是不是还有代码里写死的路径,再决定新增还是替换。直接覆盖同名文件看起来省事,但一旦新旧字形的字宽不同,原本调好的行高和按钮宽度都会跟着变。
aapt dump resources app.apk > resources.txt
findstr /i "font" resources.txt
WebView 里的字体要单独算一条线。网页的字体由 CSS 决定,和原生控件的字体互不影响。如果应用里有本地 HTML 页面,把 WOFF2 放进 assets/fonts,在 CSS 里用 @font-face 声明,再在 font-family 里给出回退列表。注意 WebView 加载本地页面时,资源路径的写法要和页面所在目录对应;用 file:///android_asset/ 开头是最稳的写法之一。字体文件较大时,font-display 用 swap 可以让文字先显示、字体后替换,但品牌标题可能出现一次字形跳动,标题类页面更适合 block。
还有一类字体是变量字体:一个文件里包含多个字重,通过 wght 轴调节。它体积友好,但需要 Android 8.0 及以上,样式里用 fontVariationSettings 指定轴值,低版本设备读不懂就会退回默认字重。如果应用还要覆盖 Android 7 及以下,准备一套静态字重作为兜底,比赌设备都支持变量字体更稳妥。
图 3:资源字体靠目录和字体族 XML 说话,资产字体靠路径字符串说话,两套规则不能混。
样式覆盖的五个入口:主题、样式、文本外观、控件与代码
字体文件放对了,只完成了“能被使用”,还没完成“到处都在用”。Android 的字体设置分布在五个入口上,从大到小依次是主题、样式、文本外观、控件属性,运行时还有代码这一层。改包时只改其中一层,就会出现“部分页面生效、部分页面没变”的典型现象。把这五个入口的关系理清,比盲目全局替换有效得多。
主题是影响面最大的一层。它挂在应用或 Activity 上,决定控件在没有显式样式时用什么默认样式。想让所有普通文本都换字体,可以在主题里指定 textViewStyle,指向一个把 fontFamily 设成品牌字体族的样式。注意按钮和输入框有各自的默认样式入口,只改文本的默认样式,按钮文字和输入框提示语不会自动跟着变,需要分别覆盖。
样式是第二层。它是一组属性的集合,可以被布局里的 style 属性引用,也可以被主题当作默认样式引用。样式支持继承,可以写一个基础样式放字体和颜色,再派生出标题、正文、按钮等子样式,只覆盖差异项。改包时如果原工程已经有完整的样式体系,最稳妥的做法是找到最底层的那个基础样式,在里面换字体,而不是在每个页面里逐个加属性。
文本外观是第三层,专门管文字相关的属性,比如字号、颜色、行距和字体。它通常被样式引用,也可能被控件直接引用。很多应用把标题的字号、颜色、字重都放在文本外观里,字体却留在样式里,这时只改样式可能被文本外观里的字体设置覆盖;反过来只改文本外观,又可能被控件上的直接属性覆盖。判断办法很简单:顺着控件往上找,看它引用了哪个样式、样式又引用了哪个文本外观,逐层确认字体最终来自哪里。
控件属性是第四个入口,写在布局文件里,优先级最高。只要某个 TextView 自己写了 android:fontFamily,外面的主题和样式都不再起作用。这也是“全局换了字体,首页大标题还是旧字形”的最常见原因。五个入口的生效顺序大体如下,冲突时以更靠近控件的一层为准。
| 层次 |
写在哪里 |
影响范围与漏点 |
| 主题 |
应用或 Activity 的主题定义 |
全局默认;按钮、输入框需单独覆盖 |
| 样式 |
res/values 下的样式定义文件 |
被引用的控件;继承链要逐层看 |
| 文本外观 |
样式里的 textAppearance 项 |
只管文字属性;可能盖住样式里的字体 |
| 控件属性 |
布局文件里的 fontFamily |
只影响当前控件;优先级最高 |
| 运行时代码 |
setTypeface 或自绘控件的画笔 |
一旦执行就覆盖以上所有层 |
代码这一层最容易被忽略,因为它不在资源文件里。常见的三处:列表适配器在绑定数据时调用 setTypeface,把字体重新设回旧值;自定义控件在构造时创建画笔,字体是写死的;富文本用 Spannable 给局部文字加样式,其中的字体跨度指向系统字体名。前两处要在代码里替换字体对象,第三处要换成能承载自定义字体的跨度实现,因为按系统字体名构建的跨度找不到品牌字体。
自绘控件是字体覆盖的重灾区。TextView 会自己处理字体,自定义 View 不会:它在 onDraw 里用画笔绘制文字,画笔用什么字体完全由代码决定。图表库的坐标轴、价格标签、倒计时、进度环上的数字,大多属于这一类。样式层改得再全,也影响不到一支自己创建的画笔。处理办法是在控件初始化时把品牌字体传进去,或者对外暴露一个设置字体的接口,在页面里统一调用。
弹窗和对话框也要单独检查。系统对话框通常跟随 Activity 主题,自定义对话框如果用的是独立布局,布局里的控件会走自己的样式链,不一定继承页面的设置。底部弹层、下拉菜单、日期选择器同理。验收时不要只截首页,把“从哪个页面弹出什么”列成清单,逐个打开确认,才能发现那些藏在二级交互里的旧字体。
在动手改样式之前,还要判断界面是用什么技术栈写的。传统的 XML 布局加 View 体系,字体由主题和样式控制;Compose 界面不读 XML 样式,字体由代码里的字体族和排版对象决定;WebView 页面由 CSS 决定;跨平台框架又各有各的字体声明方式。技术栈判断错了,改再多资源文件也不会生效。
| 界面技术栈 |
字体由谁决定 |
改包时先看什么 |
| XML 布局与 View |
主题、样式、控件属性、代码 |
布局里的 fontFamily 与自定义控件 |
| Compose |
代码里的字体族与排版配置 |
资源是否被代码引用,改样式无效 |
| WebView |
CSS 的 font-family 与 @font-face |
本地页面、资产路径与回退列表 |
| 跨平台框架 |
框架自己的主题与资源清单 |
字体文件位置与清单是否可改 |
这一层层的覆盖听起来繁琐,实际有一套很省事的检查方法:准备一段同时包含中文、英文、数字和标点的样例文字,把它放进首页、列表、详情、弹窗四个位置,每改完一层就在真机上截一次图。四张图放在一起对比,哪一层没生效、哪个控件漏了,一眼就能看出来。截图比肉眼扫视可靠,也比反复卸载重装省时间。
“最坑的一次是样式全改了,图表里的数字还是旧字体。后来才知道那个图表库是自己拿画笔画的,根本不读 TextView 的样式。现在我会先把自绘控件列出来,一个个确认。”
—— 林工 · 企业信息化运维
缺字兜底:方框从哪来,回退链怎么排
字体覆盖做完,最后一个技术问题是缺字。用户看到的是一个空心方框,开发者看到的是字形缺失:这个字符在字体文件的字符映射表里没有对应字形,系统在回退链里也没找到能画的字体,最后只能画一个“缺字标记”。它可能是一个方框,也可能是一个空白,具体长什么样取决于字体和渲染引擎。
Android 的文字渲染本身带有回退机制。控件指定了品牌字体后,系统会先用它排版;遇到它没有的字符,再到系统的回退字体里找,中文、日文、韩文、表情符号各有对应的系统字库。所以一套只包含拉丁字母和数字的品牌字体,并不会让中文变成方框,中文会自动落到系统字体上。真正会出方框的,是品牌字体和系统字库都没有的字符,比如生僻人名、古籍用字、部分专业符号。
理解这一点,就能理解三种常见的兜底策略。第一种是“品牌字体管拉丁和数字,中文交给系统”,体积最省,部署最简单,缺点是中英文的字重和字面大小可能不协调。第二种是“全量中文字库”,视觉最统一,缺点是安装包明显变大,而且仍可能缺生僻字。第三种是“子集加清单”,按应用实际用字裁剪,再为清单外的字保留系统回退,体积和统一性比较平衡,但需要持续维护字符清单。
| 策略 |
体积与统一性 |
适合的应用 |
| 拉丁品牌加中文系统 |
体积小,中英文风格可能不一致 |
数字和英文为主的工具、报表 |
| 全量中文字库 |
体积大,风格统一 |
品牌要求高、文案固定的应用 |
| 子集加清单 |
体积可控,需要维护清单 |
教材、医疗、政务等专业用字场景 |
不管选哪种策略,都要有一个能自动发现缺字的办法。Android 提供了判断字形的接口,可以在调试版里做一个自检页:把应用里可能出现的字符逐个交给画笔判断,缺哪个就打印出来。这个自检页不需要上线,只在测试包里保留,每次换字体、换文案时跑一遍,比等用户截图反馈快得多。
val paint = Paint().apply { typeface = brandTypeface }
val missing = textChars.filterNot { paint.hasGlyph(it.toString()) }
if (missing.isNotEmpty()) Log.w("FontCheck", "缺字:" + missing.joinToString(""))
自检页要覆盖三类字符:界面文案里的全部汉字、业务数据里的动态内容(人名、地名、商品名)、以及符号和标点。第三类最容易被忽略,勾选标记、箭头、圆点、货币符号、数学符号都可能不在品牌字体里。它们在系统字体里通常有替代,但如果品牌字体里有一个“长得不一样”的同码位字形,排版会突然错位,这种问题比方框更难发现。
中英文混排的行高和基线也要专门看。西文字体的字面高度、上下伸部和中文字体不同,混排时如果行高由西文字体决定,中文会显得偏上或偏下;如果由中文字体决定,西文又会显得小。解决办法不是逐行调,而是给文本设置统一的行高,标题和正文各用一个固定值,再在真机上对照设计稿微调。中文字体常见的上下留白偏大,标题类文本可以关掉额外的字体内边距,让视觉重心更稳。
数字的对齐是另一类细节。品牌字体里的数字可能是比例宽度的,1 和 8 占的宽度不同,金额列表一列下来就会参差不齐。财务、订单、报表类界面要开启等宽数字特性,让每个数字占同样的宽度。Android 的文本控件支持字体特性设置,把 tnum 打开即可;WebView 里用 font-feature-settings 声明同样的特性。开启之后再看小数点、千分位和负号,确认它们也在同一套宽度体系里。
WebView 的兜底靠 CSS 的字体列表,顺序就是优先级。把品牌字体放在第一位,中文系统字体放在后面,最后写一个通用族名收尾。这样即使品牌字体加载失败,页面也不会回到衬线字体或者出现方框。注意通用族名要写对,sans-serif 表示无衬线,serif 表示衬线,写错一个字母整条回退链就断在最后一步。
@font-face { font-family: "Brand Sans"; src: url("file:///android_asset/fonts/brand.woff2") format("woff2"); }
body { font-family: "Brand Sans", "Noto Sans SC", sans-serif; }
把缺字现象、原因和处理方式整理成一张表,排查时按现象对号入座,比从头翻代码快。
| 现象 |
常见原因 |
处理方向 |
| 个别汉字显示方框 |
品牌字体与系统字库都缺该字 |
补字、换全量字库或调整文案 |
| 符号变成方框或空白 |
符号不在字符集,系统也无替代 |
换用常见符号或改为图片资源 |
| 字重发虚、边缘糊 |
缺对应字重,系统合成加粗 |
补齐字重或改用已登记档位 |
| 金额列对不齐 |
数字宽度不等,未开等宽特性 |
开启 tnum,或改用等宽数字字体 |
| 中英文高低不齐 |
两套字体基线、行高不同 |
统一行高,微调内边距 |
图 4:缺字的本质是回退链走到尽头,补字、换字库、改文案是三条不同的路。
回编、签名与安装:把字体改动变成可验收的成品
资源和样式都改完,接下来是把工程变回安装包。智能修改电脑版的打包分四步:先用 apktool 回编出未签名的包,再用 zipalign 做四字节对齐,然后用 apksigner 签名,最后用 apksigner verify 复核证书。四步的完整输出都写进 pack.log,出问题时先看这个日志,比在界面上猜要快。多出 verify 这一步,是为了确认“到底签没签上”,密钥格式不对时,只有复核会明确报出来。
apktool b -f -o build\unsigned.apk
zipalign -f -p 4 unsigned.apk aligned.apk
apksigner sign --ks brand.jks --out signed.apk aligned.apk
apksigner verify --print-certs signed.apk
对齐这一步容易被当成走过场,其实它影响安装后的加载效率。安装包里的未压缩资源要按固定边界对齐,系统才能直接映射读取;对齐没做或者做错,应用仍可能装上,但读取资源会多一次拷贝,字体文件较大的应用更明显。签名这一步要选密钥:默认签名、JKS 或 keystore、PEM 加 PK8、随机签名,四种方式按交付场景选。签名版本支持 v1 到 v4,默认是 v1、v2、v3 一起签,覆盖大多数设备。
签名口令通过环境变量传入,不拼进命令行,也不会写进日志。这一点在团队协作里尤其重要:打包脚本会被复制、日志会被上传,明文口令一旦进去就很难收回。如果这次只是内部预览,可以用默认签名快速出包;如果是要交付给客户或上架的版本,必须用登记过的正式密钥,并在自家后台更新证书指纹。注意,这是更新自己系统的登记信息,不是绕过任何校验。
打包完成后可以自动安装。流程是先用 adb 找设备,找到后装包,再用 am start 拉起主界面;启动命令带等待参数,能拿到启动耗时和结果。组件名有三档查找方式:先读项目配置里的启动 Activity,找不到就用系统命令解析,再找不到才走旧方式兜底。拉起之后,软件会用系统状态复核前台应用是不是它,避免“装上了但没起来”被误判成功。手机通过投屏窗口显示,模拟器会被提到最前。
字体改动的验收,不能只看“能打开”。下面这张表把要看的页面、看什么、通过标准列清楚,照着走一遍,比凭感觉可靠。
| 验收位置 |
看什么 |
通过标准 |
| 启动页与首屏 |
标题、副标题、按钮文字 |
字重正确,无闪烁换字 |
| 列表与详情 |
正文、标签、空状态文案 |
全页统一,长文本不截断错位 |
| 金额与数字 |
小数点、千分位、负号、币种 |
同列数字等宽,右对齐整齐 |
| 弹窗与菜单 |
确认框、底部弹层、下拉项 |
不再出现旧字体 |
| 图表与自绘控件 |
坐标轴、图例、标签 |
字体已替换,刻度不被裁切 |
| 生僻字与符号 |
人名、地名、勾选、箭头 |
无方框,回退后仍可读 |
| 小字号与大字号 |
十号字注释、系统放大字体 |
笔画不糊,布局不重叠 |
| 签名与安装 |
证书信息、覆盖安装、冷启动 |
证书正确,启动无异常 |
如果回编失败,先看 apktool.log 的尾部。字体相关的高频错误有两类:文件名不符合资源命名规则,报的是资源名非法;字体格式不被识别,报的是解析失败。前者改名即可,后者要转成 TTF 或单体 OTF 再试。如果打包成功但安装失败,先看签名是否冲突,卸载旧包再装;再看 minSdk 是否高于设备版本。日志文件各管一段:apktool.log 管反编译与回编,pack.log 管对齐签名,error.log 管未处理异常,dock.log 管窗口布局,不要拿错日志查错问题。
图 5:对齐、签名、复核三步都留有日志,字体改动是否真正进入成品,以此为准。
“我现在固定看三个日志:反编译结束看一次,回编失败看一次,签名之后用证书复核看一次。字体的问题基本在前两步就暴露,不会拖到装到手机上才发现。”
—— 小满 · 独立应用开发者
自家应用实例:从以前的做法到现在的一遍过
原理讲完,落到具体项目上才有意义。下面先看两个完整的例子,都是自有应用:一个是自家记账工具,一个是内部教学应用,授权清晰,改动范围可控。每个例子都按“以前怎么做、现在怎么做、改完怎么验证”三段写,方便你对照自己的项目替换,最后再补一个更小的例子。
实例一:自家记账应用换品牌字体,重点在金额和图表
这个应用是团队自己维护的记账工具,用户量不大,但界面里数字密集:首页总资产、账目列表、账单详情、图表坐标轴、导出预览,全是金额。品牌升级后,设计给了一套带三个字重的字体,要求正文用常规、标题用粗体、按钮用中黑,金额必须等宽对齐。
以前的做法很原始:把三个字体文件放进 assets,在应用启动时逐个加载,然后在每个 Activity 里手动给控件 setTypeface。首页改了,列表页忘了;列表页改了,详情页的金额控件是自绘的,根本没走 TextView;图表库的坐标轴文字藏在内部画笔里,找了两天才找到设置入口。最麻烦的是每次调整都要重新打包三遍,分别验证页面、弹窗和图表,稍不留神就漏掉一处。金额虽然换了字体,却没有开等宽数字,列表右对齐看起来还是参差不齐。
现在的做法是把字体和页面清单一起交给智能修改电脑版。附件里放三个字体文件,每个都写清用途和授权;需求里列明五件事:字体族怎么组织、正文标题按钮分别用哪个字重、主题默认样式要覆盖、自绘金额控件和图表坐标轴要补上字体、数字开启等宽特性。改完之后,字体文件落在资源目录,按 400、500、700 登记成一个字体族,主题里的默认文本样式指向它,自绘控件和图表入口也补了对应的设置。
验证分四步走。第一步看首页总资产,确认数字等宽、千分位和小数点对齐,负号不会把整行推歪。第二步看账目列表,随机挑十条不同金额的记录,比较同一列数字的右边缘是否在一条线上。第三步看详情页和导出预览,确认小字号注释没有糊成一团,币种符号没有变成方框。第四步打开图表页,看坐标轴刻度和图例,确认字体换了、刻度没有被裁切。四步都过,再对比安装包体积和冷启动时间,确认增加的字体文件在可接受范围内。
实例二:内部教学应用加品牌标题,重点在生僻字兜底
第二个例子是内部教学应用,面向老师和学生,界面里既有英文单词,也有古诗文和地名。之前做过一轮中文本地化,文案统一了,字体还是系统默认。品牌更新后,要求标题和英文单词用品牌字体,正文中文保持系统字体,理由是教学场景对生僻字的可读性要求高,不能为了统一风格牺牲覆盖率。
以前的做法是直接把一套中文字体全局替换上去,结果第一版就出问题:古诗文里的生僻字变成了方框,老师截图发到群里,问“这个字怎么没了”。开发者只好临时换回系统字体,品牌感又没了。后来试过手工比对字表,把教材里的字一个一个复制到字体预览工具里看,效率极低,而且每次教材更新都要重来一遍。
现在的方案是分层:品牌字体只负责标题、英文单词和数字,正文中文明确交给系统字体;同时把教材用字整理成一份字表,随字体文件一起作为附件提交,需求里写明缺字必须回退、不允许出现方框。改完之后,标题的字形统一了,正文仍然是覆盖面最广的系统字库,生僻字由系统兜底。为了长期可控,测试包里加了一个自检页,用判断字形的接口把字表跑一遍,缺哪个字直接打印到日志。
验证时重点看三处。第一处是字表页,把三个年级的教材用字按字号从大到小渲染一遍,确认没有方框、没有空白、没有突然变形的字。第二处是低版本设备,找一台 Android 10 的旧机,确认字体资源能正常加载,标题不回退成默认字体。第三处是平板,横屏下标题的字号和行高是否协调,英文单词和中文混排时基线是否稳定。三处都过,再让老师用真实课件走一遍,才算验收完成。
补充一个短例子:内部工具的标题字重
还有一个更小的例子:内部运维工具只要求把页面标题从系统粗体换成品牌粗体,正文不动。以前是在每个布局里找标题控件,逐个加属性,漏了设置页和日志页。现在把字体文件作为附件,需求写清“只替换标题样式,正文与代码不动”,改完检查设置页、日志页和关于页三个入口即可。改动越小,越要写清边界,否则很容易被顺手扩大到全界面。
图 6:字体验收要覆盖不同页面、不同字号和不同系统版本,单看一张首页截图不算完成。
排查表与上线清单:把问题挡在发布之前
字体问题有很强的规律性,相同的现象往往对应相同的几处配置。把下面这张表放在手边,遇到问题先按现象找到首查位置,再顺着资源、样式、回退、打包四条线往下查,能省掉大量反复安装的时间。表里的处理方向不是唯一解,但足以把问题缩小到一两个文件。
| 现象 |
先查哪里 |
处理方向 |
| 回编报资源名非法 |
res/font 下的文件名 |
改成小写字母、数字、下划线 |
| 装上后全是系统字体 |
字体族引用与样式链 |
确认引用名正确、低版本有兼容 |
| 只有部分页面生效 |
控件直接属性与自绘代码 |
逐页清点,自绘控件单独补 |
| 弹窗仍是旧字形 |
弹窗的主题与独立布局 |
给弹窗样式补字体,不要只改页面 |
| 个别字变方框 |
字体字符映射与系统回退 |
补字、换全量字库或调整文案 |
| 粗体边缘发虚 |
字体族里登记的字重 |
补 700 档位,别让系统合成 |
| 网页字体不生效 |
CSS 的 @font-face 与资产路径 |
核对路径大小写与回退列表 |
| 安装包明显变大 |
字库范围与字重数量 |
子集化,只留真正用到的档位 |
| 安装失败或覆盖不上 |
签名与设备系统版本 |
卸载旧包,改用正式密钥重签 |
表格解决“出了问题怎么定位”,清单解决“发布前别漏项”。下面十条按施工顺序排列,每一条都能在十分钟内确认,全部打勾再交付。
- 字体授权已书面确认,允许嵌入与分发,子集化另有约定时也已确认。
- 三个字重的文件齐全,格式为 TTF 或单体 OTF,没有用字体集合改名充数。
- 资源文件名全部小写,只含字母、数字和下划线,和字体族 XML 一一对应。
- 主题、样式、文本外观、控件属性、运行时代码五处都已核对,没有互相覆盖。
- 自绘控件、图表、弹窗、底部弹层已列入清单,逐个确认字体来源。
- 缺字自检页跑过一遍,界面文案、业务数据和符号三类字符都覆盖。
- 金额与统计数字开启等宽特性,小数点、千分位、负号对齐。
- 目标系统版本与平板各验一台,低版本不回退成默认字体。
- 打包日志完整,证书复核通过,安装包能覆盖升级。
- 安装包体积与冷启动时间有对比记录,增量在可接受范围。
用户评价里,有一位做教学产品的同行说得很实在:字体好不好看是第二步,第一步是每个字都能显示出来。这句话值得放在所有字体项目的第一行。
“我们最怕的不是不好看,是学生名字里那个字显示不出来。现在先把字表跑一遍,缺字在测试阶段就能发现,不用等老师截图。”
—— 陈老师 · 职业教育机构产品经理
最后再强调一次使用边界:本文所有方法只适用于自有版权或已获得授权的应用。请勿用于破解他人付费应用、去除授权校验或绕过安全机制,也不要把从别的安装包里提取字体当成素材来源。字体是有版权的软件,改包的自由建立在授权清晰的前提上,这一点没有例外。
回头看,把品牌字体带进自家应用并不神秘:文件放对位置,样式覆盖到最后一层,缺字有明确的回退,成品经过签名和逐页验收。四件事做完,设计稿上的字才会真正出现在用户屏幕上。安卓修改大师智改工坊把这条流程收进一个工作台,从导入、修改到打包、安装预览都在同一个项目里完成,需要核对细节时,可以到 https://www.apkeditor.cn/ai-version.aspx 了解智能修改电脑版的完整能力,也可以先访问 www.apkeditor.cn 看看其他教程。
字体的统一不是一次性的工作,而是一套可以重复的流程。把交接单、字表、验收清单留在项目里,下一次品牌更新、下一次系统升级,你只需要按同样的顺序再走一遍。只需说话,就能让应用变成你想要的样子。
安卓修改大师智改工坊
智能修改安卓版:在手机上完成改包与验收
没有电脑也能改:导入 APK,写清需求,本地完成反编译、回编、对齐与签名,成品直接安装或分享。适合现场改图标、换文案、调字体的轻量任务。
立即下载