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

绘制流程其实不复杂:

  1. 打开framebuffer设备,获取屏幕参数(分辨率、位深、行偏移)
  2. mmap映射显存到用户空间
  3. 直接往显存里写像素数据
  4. 调用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_lenline_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镜像,以及如何实现多语言支持。到时候见。