修改Recovery中的文字提示(二)
上一篇我们把Recovery界面上那些"死文字"逐个翻了个底朝天,讲了怎么在资源文件里找到文案、替换成自己想要的说法,也提了一句"改完刷不进去多半是签名问题"。但说实话,那一篇只能解决最表面的需求——把现有文字换成另一行文字。真正到了生产环境,你会发现更麻烦的问题一箩筐:为什么中文替换上去变花屏?为什么想显示"刷入成功 耗时12秒"这种动态内容不知道怎么下手?为什么在A设备上显示正常的文案,放到B设备上就跑到屏幕外面去了?
这篇文章就把这些更底层的东西一次性说清楚。
1. 先补一个前提:Recovery里的字到底是谁画的
在动手改任何文字前,得先明白一件事:Recovery界面里的文字,并不像普通App那样走系统的TextView渲染。Recovery环境里没有完整的图形系统,屏幕上每一个像素基本都是Recovery自己的UI引擎直接画上去的。这个引擎在官方Recovery源码里叫minui,它负责屏幕初始化、画点划线、刷新画面,以及——把字体点阵摆到屏幕上。
minui渲染文字的方式非常原始:它会从一份预先编译好的字体数据里,根据字符编码找到对应的点阵位图,然后逐位判断哪些像素需要点亮,最后画到framebuffer上。也就是说,Recovery环境下的字不是矢量字体,是点阵字体。你看到的每一个字,本质上都是一组0和1组成的小格子图。
这就引出了一连串问题。首先,官方Recovery内置的字体数据通常只覆盖了基础拉丁字符和少量符号,字库数量很少。你如果直接往字符串资源里塞中文,minui在字体表中找不到对应字符,常见的表现是显示成空白、方框、问号,或者干脆乱码花屏。这跟"文字没替换成功"完全是两码事,是两个层面的故障。
另一个要注意的点是,Recovery里的文案来源其实不止一处。上一篇我们主要讲了res目录下的字符串资源,但实际操作中你会发现,除了资源文件,还有相当一部分文字是硬编码在UI源码里的——比如某些状态提示、确认对话框的标题、进度条旁边的说明文字。改递归扫描一下源码里所有字符串字面量,往往比改资源文件更能覆盖全面。我见过不少朋友改了半天资源文件,界面上还是老样子,最后发现要改的那句话写死在C++代码里,这类问题排查起来很费时间。
所以拿到一个修改文字的需求,我的第一反应一定是先归类:这句话是资源文件控制的,还是源代码写死的,还是终端输出流打到屏幕上的?三条路走不同的改法。这也是上一篇和这一篇的衔接点——上一篇解决资源文件这条线,这一篇我们重点攻字体渲染和动态显示这条线。
2. 中文字体与自定义字库:从"改字"到"换字库"
2.1 为什么官方字库打不了中文
Recovery的minui在编译时会生成一组静态字体数据,本质上就是一个巨大的C数组,数组里按字符编码依次存放每个字的点阵信息。官方默认的字体覆盖范围非常保守,连中文的边都不沾。你就算把资源文件里的字符串全换成中文,字体表里没有这些字符,渲染层画出来的只能是空白或乱码。
要支持中文,核心工作是替换整个字体数据。这一步绕不开,也不建议绕。
常见做法是引入一套完整的点阵字库。中文点阵字库最经典的是16x16规格,也就是每个汉字用16行、每行16个像素点来表示,共32字节。早年DOS时代的HZK16文件就是这个格式。Recovery场景下用16x16字库有个现实好处:内存占用小,整个常用汉字字库也就几百KB级别,对Recovery这种精简环境比较友好。
如果你对显示效果有更高要求,可以用24x24甚至32x32的字库,笔画细节会好很多,但内存和绘制开销会明显上升,部分低端设备的Recovery可能扛不住。我实际测试下来,绝大多数设备的Recovery用16x16字库渲染中文,清晰度完全够用,毕竟Recovery界面本来信息密度就低,字体稍微大一点反而更好读。
2.2 从TTF字体到Recovery可用的点阵数据
拿到一个TTF字体文件后,不能直接扔给Recovery用,必须转成点阵数组。转换流程大致分三步:栅格化、取模、生成C头文件。
栅格化就是把矢量字体的轮廓在指定尺寸下渲染成黑白位图。取模是把位图按行按字节打包成点阵数据。生成C头文件则是把打包结果输出成Recovery源码能引用的静态数组。
实际操作中我习惯用脚本批量处理,把常用汉字从GB2312或者GBK码表里逐个取出并转换。这里的关键是码表覆盖要够用,不能只转几个汉字就收工,不然界面上某个冷僻字又变空白。一般建议至少覆盖3500个常用汉字,稳妥起见直接覆盖GB2312全部6763个汉字。
转换时有一个参数要特别注意:抗锯齿。系统桌面上看惯了灰阶抗锯齿字体,但Recovery的点阵字库是纯黑白的,不能带灰度。带了灰度意味着每个像素不再是非黑即白,需要额外的灰度层级渲染支持,大部分Recovery的minui根本不支持。所以转换时务必关闭抗锯齿,字体边缘会有一些锯齿感,这是点阵字库的正常特性,不用纠结。
2.3 把新字库挂进渲染链路
生成好的C头文件,替换掉源码中原来的字体数据文件,然后在构建配置里把它编进去。这里有个细节:minui的字体结构通常包含字体宽度、高度和字符数据表,替换时这三者必须保持一致,尤其是字段顺序。我见过有人把数据替换对了但忘了更新字体尺寸的宏定义,结果渲染出来的每个字都错位错得离谱,像被切了一半。
替换字体后还需要测试几个点:西文字符和中文字符混排时的对齐是否正常、英文数字的显示是否还清晰、标点符号是否落在了预期位置。因为中文是全角字,和半角英文混排时行宽计算容易出问题,表现为句子中间突然多出半个字位的空隙。这个现象不影响阅读,但追求完美的话,可以在渲染层针对全角/半角切换做一下额外偏移处理。
如果你用的是第三方触摸Recovery,字体替换路径会略有不同,但底层逻辑是类似的:找到它内置的字体文件或字体数组,把中文点阵合并进去或者整体替换。第三方Recovery往往已经把中文字体问题解决了,所以如果你是在第三方Recovery基础上改文案,字体这步可以直接跳过;但如果你维护的是官方Recovery,这一关必须过。
3. 把静态提示改成动态状态文本
3.1 动态文字到底能干什么
静态替换玩熟了之后,很多人自然会想:能不能让Recovery界面上显示一些实时变化的内容?最常见的需求有三类:显示当前操作的耗时、显示正在安装的包名或脚本名、显示剩余电量或日志的最后几行。
这些需求本身不难,但难点在于Recovery的UI结构和普通程序不一样。Recovery没有后台线程池,没有消息队列,一切操作都在一个相对封闭的流程里跑。要显示动态文字,你得搞清楚它的UI刷新时机,在正确的节点插入自己的更新逻辑。
3.2 UI线程与操作回调:在哪里塞逻辑
官方Recovery的UI核心有一个事件循环,负责轮询按键、刷新画面、执行菜单回调。你看到的每一个界面状态切换,本质上都是这个循环驱动的。想显示动态信息,思路通常是:在自己的操作流程中,每一步进度更新时调用UI的更新接口,把新的文本内容传进去,让UI引擎在下一帧刷新时画出来。
比如刷入ZIP包时想显示"正在写入分区..."后面跟一个倒计时或文件名,就需要在安装脚本执行的循环里,获取当前安装项的信息,拼接成字符串,然后调用UI的刷新函数。安装过程本身是阻塞的,所以要注意刷新调用必须放在主流程中,而且更新后要主动触发一次画面重绘,不然文字会一直停在第一次画的旧状态。
我踩过一个典型的坑:更新文字之后没有触发重绘,导致屏幕上显示的还是旧内容,排查了很久才发现问题是刷新函数没被调用,而不是字符串拼接错了。在Recovery这种环境里,画了不等于刷了,刷了不等于你看见了,这两层关系一定要理清。
3.3 进度条背后的文字联动
Recovery界面上常见的进度条旁边通常有一条说明文字。默认情况下这条文字是固定的,比如"正在安装更新…"。如果想让这条文字跟着进度变化,比如每写一个分区就显示分区名,就需要把进度更新和文本更新绑定在同一个回调链上。
我自己常用的做法是:在进度更新的回调里,把当前分区名格式化成一行文本,同时调用两个接口——一个更新进度条位置,一个更新文字内容。两个操作放在同一处,可以保证视觉上进度条和文字的刷新是同步的。如果分开放,经常会出现进度条已经走完,文字还停在上一行的尴尬情况。
这里还要注意内存复用。Recovery环境内存紧张,频繁创建新字符串是大忌。建议提前分配好一块固定大小的字符缓冲区,每次更新时往里面填充内容,而不是动态拼接。多数Recovery内核里malloc频繁调用没问题,但在极端情况下会碰到内存碎片,稳妥起见用静态缓冲区最放心。
3.4 花屏与闪烁的真相
动态刷新文字时最怕两个现象:花屏和闪烁。花屏的根子通常是绘制区域没有完全清空,新文字画上去时旧文字的残影还在,一层叠一层就成了花屏。解决办法很简单:每次更新文字前,先把目标区域的矩形填充成背景色,再画新内容。
闪烁则和刷新频率有关。Recovery的framebuffer刷新是直接整屏刷新的,如果你一秒钟更新几十次文字,人眼看到的就会是明显的闪动。解决思路是降低刷新频率,或者只在内容真正变化时才触发重绘。比如给文本设一个简单的比较逻辑,内容跟上一次相同就跳过重绘。我见过有人在Recovery里放了一个实时时钟,结果整个界面闪得没法看,后来改成每分钟刷新一次才算正常。
4. 多语言提示与语言切换:一套Recovery,多套文案
4.1 官方资源目录与硬编码字符串的分裂
Recovery确实支持多语言资源目录,比如res-zh-rCN、res-pt-rBR这种带地区码的目录。但这些资源目录只覆盖了一部分文案,前端UI里仍有大量硬编码字符串不会跟着语言资源走。这就导致一个很搞笑的情况:菜单和按钮变成中文了,某些提示框里还是英文。
想真正实现多语言,必须两条线一起改。资源目录这边做常规翻译,硬编码字符串那边就得在源码里按语言条件判断返回不同文本。工作量比想象中大,但这是躲不掉的。我的建议是优先覆盖用户看得到的高频文案——主菜单、安装确认、擦除确认、重启选项这几个地方,低频的调试日志可以放一放。
4.2 按需切换语言的实现方式
Recovery环境里没有运行中的Android系统帮你决定语言,它只能依赖少数几个信息来源。一个常见机制是读取一个配置文件,这个文件可能是刷机工具写入的,也可能是Recovery启动时从缓存分区读取的。文件内容通常就是一个语言标记,比如zh_CN或en_US。Recovery启动早期会尝试读取这个标记,用它初始化全局语言选项,之后所有界面文本都据此选择。
如果读取不到标记,就会回退到编译时指定的默认语言。这解释了为什么很多定制Recovery默认就是中文:不是因为它自动识别了用户系统语言,而是它在编译时直接把默认语言设成了中文。
想测试多语言效果,不需要重新编译整个Recovery。我把语言标志文件放到相应的位置手动改内容,然后重启进Recovery,就能快速看到不同语言下的界面效果。这个调试方法非常省时间,强烈推荐。
4.3 语言和字体必须配套
多语言切换里最容易翻车的是字体配套。Recovery只有一套点阵字库,切换语言只换了文字串,字库没换,某个语言特有的字符就会变成空白方块。最常见的就是切到中文时发现大部分汉字变豆腐块,原因是字库还是官方那套只有西文字符的默认字库。
所以做多语言支持的完整流程是:字库先行,翻译跟上。先确认目标语言的全部字符都在字库里覆盖了,再做文案翻译。顺序反过来的话,你会在调试时看到一堆方块字,还以为是翻译文件格式错了,定位半天才明白是字库问题。这个教训我印象太深了。
5. 屏幕适配与文本溢出:改完文案后最常见的"显示事故"
5.1 坐标基准与UI布局的计算
Recovery的UI布局在很多实现里是用写死的坐标或者简单的比例计算完成的。屏幕宽高不同,同一个提示框的位置可能完全跑偏。修改文案时尤其要注意:新文案比原文长或短,可能导致文本绘制区域超出预定的框体范围,甚至覆盖旁边的按钮。
更隐蔽的问题是Recovery界面里有些元素是屏幕坐标硬编码的,不随分辨率缩放。你在开发机上用的测试分辨率可能和设备实际分辨率不一致,结果测试时一切正常,一到目标设备上文字就跑到屏幕外去了。
我的习惯是拿到设备的分辨率后,先画一张简单的网格图,把Recovery里各UI元素的坐标范围标出来,再对照自己的文案长度做调整。这样能一眼看出哪里会溢出。
5.2 超长文案的换行与滚动策略
Recovery的文字绘制接口大多数不支持自动换行,字符串长了它就往右画,画到屏幕外也不报错,非常坑。所以长文案必须自己处理换行。一般做法是:把长文本按空格或标点切分,测量每一段的像素宽度,如果加上已有内容超出屏幕宽度,就插入换行符。
这里有个测量宽度的麻烦事:点阵字体下每个全角字符宽度是固定的,但半角字符和空格宽度不同。如果你不仔细算,混排文本很容易出现某行太满、某行太空的情况。有一个讨巧的办法:全角字符统一按一个字宽算,半角字符按半个字宽算,四舍五入取整。这样误差很小,视觉上基本看不出问题。
滚动显示是另一个方案,适合那种不换行但确实太长的文本。实现方式也很原始:在固定的显示窗口内,每隔一段时间把文字起点往上平移几像素,实现视觉上的滚动效果。注意滚动速度要合适,太快看不清,太慢等得着急。而且Recovery里很少用滚动,因为要处理定时器和重绘的配合,代码复杂度会上升不少。
5.3 不同分辨率下的等比缩放误区
有人想偷懒:根据目标分辨率对基准分辨率,把所有坐标做等比缩放不就行了。实际测试发现这招经常失灵。原因很简单:Recovery界面元素不是纯图形,它里面有大量文字,文字的点阵字号是固定的,不能跟着分辨率等比缩放。你按分辨率把布局放大了,但字体没变大,结果文字在整体比例里显得变小了,布局也比例失衡。
更稳妥的做法是:布局坐标按实际像素设计,文字字号独立选择,不要试图做全局等比缩放。不同分辨率之间,宁可让某个元素的位置在不同设备上有细微差异,也不要强行构建一个虚假的"完美等比"体系。Recovery的界面使用场景本来就简单,用户只需要看得清楚、点得准,不追求像素级一致。
6. 最后一道坎:修改后的Recovery为什么刷不进去
6.1 分区镜像的完整性校验
改完字体、改完文案、编译打包,高高兴兴去刷机,结果刷机工具报错或者设备直接拒刷。多数情况下是镜像文件的完整性校验没过。
Recovery镜像通常包含表头信息、内核、ramdisk等部分。很多设备的引导加载程序在刷入Recovery时会校验镜像的头部结构和签名信息。你只是改了ramdisk里一个文件,表头的部分校验值可能就得跟着更新;如果忽略了,引导程序直接判定镜像损坏,拒绝写入。
处理办法说穿了也不复杂:用官方提供的解包工具先解包原厂Recovery镜像,修改完成后再用配套的打包工具重新打包,工具会自动更新表头信息。这里最忌讳的是手动拼接镜像,稍微一个字节算错就废了。
6.2 签名校验与自签名的配合
另一道常见的坎是签名校验。很多设备要求Recovery镜像由特定密钥签名,否则不认。如果你没有官方签名密钥,就得把设备引导程序的签名校验关掉,或者用自签名密钥重新签名,并让引导程序信任你的密钥。
具体操作要看设备情况,但思路是一致的:你可以用源码里自带的签名工具给修改后的镜像签名。如果引导程序的校验比较严格,还要一并处理vbmeta或者其他防回滚机制,这个就要按特定设备来具体对待了。不同设备的处理方式天差地别,没法给一个通用命令,建议以设备的官方文档为准。
6.3 我修改Recovery文字后的最终检查单
每次改完,我都会按这个清单走一遍,能省下大量返工时间:
- 检查改动是否涉及需要同步更新的校验信息(表头哈希、签名哈希、防回滚计数)
- 检查ramdisk里的文件权限和SELinux上下文是否被意外改动
- 检查新增的字体数据数组是否占用了过多空间,导致镜像超出分区大小限制
- 在多个分辨率设备上各测一遍文本布局,确认没有溢出
- 保留改动前后的镜像哈希值,方便随时对比排查问题
其中文件权限这条极易被忽视。解包ramdisk后,如果某个目录的属主或权限位发生了变化,即使文字改对了,设备也可能因为权限问题进入异常状态。我习惯解包后先记录所有关键文件的权限值,改完再核对一遍,基本不会出错。
7. 写在最后的实操心得
Recovery里的文字修改,表面上是个翻译活儿,实际上考验的是对底层渲染机制、资源加载、分区打包校验这一整条链路的理解。我见过有人只用十六进制编辑器直接改镜像里的字符串,改显示文本是能凑效,但一旦涉及换字体、动态信息就完全没法做。
给你一个最直接的建议:进入Recovery开发前,先花半天时间把minui的渲染流程读一遍,把字体数据的结构彻底搞明白。这半天花的绝对值,后面你能少踩百分之八十的坑。
另外,做任何修改前,先给原厂Recovery镜像做一个哈希备份。万一改坏了,随时能回到初始状态。这个习惯我从第一次刷砖之后就再也没丢过。最后再提醒一句:不同设备之间的差异远比想象中大,我在某台设备上验证没问题的方案,换到隔壁设备就翻了车。所以每台设备都要独立走一遍完整的验证流程,别指望一份编译产物通吃所有机器。