4、Recovery模式UI系统:UI框架概览、屏幕驱动与绘制、事件处理机制
好,咱们进入Recovery模式最“看得见”的部分——UI系统。
说实话,很多人觉得Recovery就是个黑底绿字的命令行界面。但你看现在的手机Recovery,哪个不是带触摸、带动画、甚至还有主题皮肤的?这背后就是一套完整的UI框架在支撑。
我个人习惯把Recovery的UI系统拆成三块来看:框架怎么搭的、屏幕怎么画的、用户怎么点的。咱们一个一个说。
4.1 UI框架概览:从minui说起
Recovery的UI框架,核心是一个叫minui的轻量级库。注意,它不依赖Android的SurfaceFlinger,也不走View体系。说白了,它直接操作framebuffer。
为什么会这样?因为Recovery模式下,系统服务都没启动,你没法用正常的Android渲染管线。所以Google自己写了一套极简的UI工具集。
核心组件一览:
graphics.h/graphics.cpp:屏幕绘制接口,包括画点、画线、画矩形、渲染图片resources.h/resources.cpp:资源管理,加载字体、图片等ui.h/ui.cpp:高级UI组件,比如进度条、菜单列表、文本标签events.h/events.cpp:事件循环,处理按键和触摸
我记得第一次看minui源码时,发现它连个窗口管理器都没有。所有控件都是直接往framebuffer上画。嗯,这很嵌入式风格——简单、直接、可控。
4.2 屏幕驱动与绘制:直接操作framebuffer
屏幕驱动这块,Recovery走的是Linux内核标准的fbdev(framebuffer device)接口。设备节点通常是/dev/graphics/fb0或者/dev/fb0。
绘制流程其实不复杂:
- 打开framebuffer设备,获取屏幕参数(分辨率、位深、行偏移)
- mmap映射显存到用户空间
- 直接往显存里写像素数据
- 调用
ioctl触发刷新(或者等VSync)
来看一段核心代码,这是minui里初始化屏幕的部分:
// 来自 minui/graphics.cpp(简化版)
static GRSurface* fb_device_init() {
int fd = open("/dev/graphics/fb0", O_RDWR);
if (fd < 0) {
// 有些设备是 /dev/fb0
fd = open("/dev/fb0", O_RDWR);
}
struct fb_var_screeninfo vi;
ioctl(fd, FBIOGET_VSCREENINFO, &vi);
// 计算显存大小
size_t size = vi.xres * vi.yres * vi.bits_per_pixel / 8;
// mmap映射
void* data = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
// 封装成GRSurface结构
GRSurface* surface = new GRSurface();
surface->width = vi.xres;
surface->height = vi.yres;
surface->row_bytes = vi.xres * vi.bits_per_pixel / 8;
surface->pixel_bytes = vi.bits_per_pixel / 8;
surface->data = static_cast<uint8_t*>(data);
return surface;
}
避坑指南:我曾经在某个国产平板上遇到过framebuffer行偏移(line_length)不等于xres * bpp的情况。如果你直接按分辨率算行宽,画出来的图像会斜着跑。一定要用FBIOGET_FSCREENINFO获取真实的smem_len和line_length。
绘制函数就更直接了。比如画一个像素点:
// 画一个像素点
void gr_pixel(int x, int y, unsigned int color) {
if (x < 0 || x >= gr_fb_width() ||
y < 0 || y >= gr_fb_height()) return;
unsigned char* px = gr_surface->data +
y * gr_surface->row_bytes +
x * gr_surface->pixel_bytes;
// 根据像素字节数写入颜色
if (gr_surface->pixel_bytes == 4) {
*(unsigned int*)px = color;
} else if (gr_surface->pixel_bytes == 3) {
// RGB888 排列
px[0] = (color >> 16) & 0xFF; // R
px[1] = (color >> 8) & 0xFF; // G
px[2] = color & 0xFF; // B
}
}
你想想看,整个Recovery的界面——菜单文字、进度条、背景图——最终都是通过这种最原始的方式画出来的。没有GPU加速,没有硬件合成器,就是CPU往显存里写数据。
4.3 事件处理机制:按键与触摸
事件处理这块,Recovery支持两种输入方式:物理按键和触摸屏。
物理按键走的是Linux的input子系统,设备节点在/dev/input/eventX。Recovery会扫描所有输入设备,找到按键设备(通过判断EV_KEY能力位)。
触摸屏也是走input子系统,但处理逻辑更复杂一些——需要解析多点触控协议(MT Protocol)。
事件循环的核心逻辑在events.cpp里:
// 事件循环主函数(简化)
int ev_wait(int timeout_ms) {
fd_set fds;
FD_ZERO(&fds);
int max_fd = -1;
for (auto& dev : input_devices) {
FD_SET(dev.fd, &fds);
if (dev.fd > max_fd) max_fd = dev.fd;
}
struct timeval tv;
tv.tv_sec = timeout_ms / 1000;
tv.tv_usec = (timeout_ms % 1000) * 1000;
int ret = select(max_fd + 1, &fds, NULL, NULL,
timeout_ms > 0 ? &tv : NULL);
if (ret > 0) {
// 读取事件
for (auto& dev : input_devices) {
if (FD_ISSET(dev.fd, &fds)) {
struct input_event ev;
read(dev.fd, &ev, sizeof(ev));
// 处理按键或触摸事件
process_input_event(&dev, &ev);
}
}
}
return ret;
}
注意:Recovery的事件处理是单线程阻塞式的。这意味着如果你在绘制动画时没有及时处理事件,用户会感觉界面卡顿。我建议在长时间操作(比如OTA升级)中,定期调用ev_wait(0)来检查是否有中断事件(比如用户想取消操作)。
触摸事件的处理稍微复杂一点。Recovery支持两种多点触控协议:
| 协议类型 | 特点 | 常见设备 |
|---|---|---|
| Type A (MT Protocol A) | 每个触摸点独立上报,无Tracking ID | 老旧触摸屏 |
| Type B (MT Protocol B) | 使用Slot和Tracking ID跟踪触点 | 现代智能手机 |
我个人习惯优先支持Type B协议,因为现在绝大多数设备都用这个。但为了兼容性,minui里两种都做了处理。
触摸事件最终会被转换成UI动作:
- 点击:按下+抬起,位置在某个按钮区域内
- 滑动:按下后移动,用于滚动列表
- 长按:按下后保持一定时间,用于确认操作
4.4 实战经验:UI性能优化
最后分享几个我在项目中踩过的坑:
性能优化要点:
- 局部刷新:不要每次都全屏重绘。Recovery的framebuffer没有硬件合成,全屏刷新会明显掉帧。只更新变化区域。
- 双缓冲:如果设备支持,可以用两个framebuffer交替显示。一个用于绘制,一个用于显示,避免撕裂。
- 图片预解码:PNG图片解码很慢。我建议在初始化时就把所有UI图片解码到内存中的GRSurface,绘制时直接memcpy。
- 字体缓存:中文字体渲染开销大。把常用字符的位图缓存起来,别每次都去解析freetype。
嗯,UI系统这块内容其实挺多的。但核心就三件事:知道怎么画、知道怎么响应、知道怎么优化。掌握了这些,你就能在Recovery里做出任何想要的界面效果。
下一章咱们会深入讲资源管理——怎么把图片、字体、主题打包进Recovery镜像,以及如何实现多语言支持。到时候见。