【免费下载链接】plx-native
Fast, unofficial Plex client for LG webOS TVs — native 60 fps UI, 4K HEVC/Dolby Vision, no Chromium or WebView.
PlxNative 是一款为 LG webOS 电视打造的快速、原生 Plex 客户端——不依赖 Chromium 或 WebView,仅凭 Rust 就能跑出 60 fps 的流畅界面。它的秘密武器是retui:一个用 Rust 手写、仿 UIKit 的保留模式视图树(retained-mode view tree)框架。本文带你拆解它的核心契约、三层视图结构,以及为什么在弱 ARM 芯片上刻意选择静态分发、拒绝"脏区追踪"。
为什么电视客户端要自研一个 UI 框架 📺
先说背景:PlxNative 的运行环境非常苛刻。它跑在一台已 root 的 32 位 ARM 电视(LG webOS 4.5)上,用 SDL2 + OpenGL ES 2 渲染类似 Apple TV 的画廊式界面,并要在全应用内解码播放来自 Plex 媒体服务器的视频。
在这种设备上,"直接套一套 Web 技术" 或 "塞一个 WebView" 都行不通——要么内存撑不住,要么帧率上不去。于是项目把几乎一切都写成了 Rust,并自己造了一个轻量级 UI 框架:retui,一个"保留模式、UIKit 风格"的视图树。
保留模式(retained-mode)指的是:界面被表达成一棵常驻内存的视图树,每个视图知道自己是谁、处于什么状态,每帧只需"刷新"这些状态即可。这正是 UIKit 的思路,而 retui 把它用 Rust 重写了一遍。
设计源头记录在 ui-framework.md,它解释了"为什么选保留树、为什么用静态分发代替盒子化组件、为什么坚持像素级还原"三大核心决策。
retui 的核心契约:一个 View 就是 update → layout → draw 📐
整个框架最精华的地方,是它用极小的类型把整条规则说清楚了。一个视图每帧要经历三步:update(推进状态)→layout(算几何)→draw(画像素)。而且它从不直接碰 GL,只通过底层的gfx/text图元来画。
支撑这一切的只有三个小类型:
Painter:一个可Copy的值类型,把"级联透明度 + 可选平移"折叠进每一次绘制调用。比如 hero 区域的整段淡入,就是一次p.alpha(hero_a)覆盖整棵子树,而不用手工去改十几个颜色值。- ``Spring{pos, vel}`:对底层 C 弹簧函数的薄封装,保证运动逐字节一致。规则是"弹簧住在视图里"(springs live in views)——每个视图为它要动画的值各持有一个 Spring。
Env:每帧上下文,从全局状态一次性桥接进来,让整棵树读到的是同一次快照。
核心契约本身短到可以一眼看完(见 rust-modules/ui/src/lib.rs):
trait View { fn update(&mut self, env: &Env) {} fn layout(&mut self, frame: Rect, env: &Env) {} fn draw(&self, env: &Env, p: Painter); }叶子节点通常只需实现
draw;需要推进动画的视图再补上update。默认方法让"简单视图"保持简单,这正是它易于上手的地方。
视图树长什么样:Home 屏幕的三层结构 🏠
以首页为例,整棵树是自顶向下、类型固定的结构:
Home ├─ Backdrop (View) 背景,逐元素自管透明度 ├─ Hero (View) 顶部大图,随滚动整体淡入淡出 └─ Grid (View) 海报网格 └─ Shelf ×5 └─ Card ×10 (View) 每张卡片持有自己的缩放 Spring这里藏着一个关键设计:父视图持有子视图时,用的是具体类型字段,而不是Box<dyn View>。Grid就是"一个视图里装着固定数组[Shelf;5]×[Card;10]"——没有 50 个盒子化的小控件,每帧零堆分配。
这套结构在 rust-modules/screens/src/home/mod.rs 中落地,迁移的完整步骤(含每一步如何保证可编译、可截图比对)记录在 ui-viewtree-plan.md。
为什么故意不用Box<dyn View>:静态分发的取舍 ⚡
在桌面 Rust 里,Vec<Box<dyn View>>是很常见的"万能容器"写法。retui 早期确实规划过这样一个逃生舱Stack,用于杂糅的线性布局——但它最终从未被构建。
原因很直接:目标设备是一台弱 ARM 芯片。动态分发的虚函数跳转、每帧的盒子化分配,在这种平台上都是"看不见的税"。于是 retui 选择:
- 父视图用具体类型字段持有子视图,静态分发、零间接跳转;
- 集合类(网格)用固定数组,而非每帧
new一堆堆对象; - 保持"每帧零堆分配"这条性质在整棵树处处成立,而不是"到处成立、就差一处"。
用项目自己的话说:如果你哪天发现自己想要那个
Stack逃生舱了,那说明这是一次全新的决策,而不是"设计里本来就有的东西"。这种克制,正是它能在电视上稳定 60 fps 的底气。
像素级还原:从 C 迁移到 Rust 的"不变量"规则 🎯
PlxNative 的 UI 最初是 C 实现的。迁移到 retui 时,团队立下了一条硬规则——像素级还原(pixel-identity rule):
凡是原来手调过的绝对偏移量、缩放系数,新代码原样照搬同一组字面量,绝不用"差不多对"的间距。每一步都要在电视上截图与基线逐像素比对。
这意味着迁移不是"重写一个长得像的界面",而是"换一种结构,画出一模一样的像素"。运动之所以能逐字节一致,靠的就是Spring::step直接委托给底层 C 弹簧函数;转场之所以平滑,靠的是Ramp(线性倒计时)与Xfade(内容交叉淡入淡出)。这些不变量清单(共 20 条)同样记录在 ui-viewtree-plan.md。
"只画变化的部分"?retui 明确拒绝了脏区追踪 ✂️
一个自然的优化念头是:UIKit 里setNeedsDisplay能只重绘脏区,retui 为什么不做?
项目的结论文档 perf-damage-tracking-verdict.md 给出了明确答案——不做,而且不是"懒得做":
- UIKit 的脏区之所以"免费",是因为它搭在保留层树上:
CALayer有缓存位图、有 frame、有身份,矩形可以"挂"在上面。 - 而 retui既没有层、也没有缓存位图,它的信条写得很清楚:"每帧都是清空后整树重绘,所以'少画'的办法是剔除(CULL)不可见的东西,而不是追踪(dirty-track)谁变了。"
那么它靠什么省 CPU?答案是整帧 present 门(whole-frame present gate):一帧里只要有任何一个Spring 在响、Ramp 在跑、Phase 被读取、Timer 触发或邮件箱有落点,就呈现这一帧;否则不呈现。它省下约 38 个点的单核占用,而成本只是每帧多算约 20 次内存操作。
这套机制的完整设计(Spring/Ramp/Phase/Timer/Landing五种时间来源、Tick能力类型)见 retui-invalidation-design.md。其中有个巧妙设计:把时间封装成一个没有dt访问器的Tick,让"手写self.x += dt * 1000"这种容易忘记重绘的写法直接编译不过——把"忘了动画"从"靠自觉"变成"编译错误"。
设计系统四条铁律:让每个屏幕保持同一张脸 🎨
retui 不只是视图树,它还是一套设计系统:首页、详情页、播放器 HUD 都是"同一组 token + 组件"的组合。它的行为宪法写在 rust-modules/ui/src/CLAUDE.md,浓缩成四条铁律:
- 绝不写裸色值——所有颜色来自
theme.rs的命名 token,新颜色要先加 token 再用。 - 绝不写裸字号——每个字号都是
theme::size的命名档位(HERO 72 / TITLE 40 / CAPTION 24 …)。 - 绝不靠"魔法 y"摆文本——文本按"帽线带(cap band)"定位,杜绝下伸字(g、j、y、p)时的错位。
- 改进组件,而不是分叉组件——共享控件"差一点"就够用时,给它加个 builder 方法,而不是复制一份。
这四条规则的价值在于:当你为一个新屏幕做 UI 时,你会自然地复用首页已经调好、验证过的组件,而不是粘贴一段带硬编码颜色、手调文本偏移的draw_*——而这恰恰就是这套系统要消灭的"漂移"。
从设计稿到代码:关键文档与模块导航 🧭
想顺着这条线索继续深挖,以下是值得收藏的入口:
| 主题 | 文件 |
|---|---|
| 框架原始设计(为什么保留树、为什么静态分发、像素还原) | docs/ui-framework.md |
| 视图树迁移分步计划(Home/Detail/App 三阶段) | docs/ui-viewtree-plan.md |
| Token 与组件迁移、设计系统现状 | docs/ui-system-migration.md |
| 整帧 present 门与动画时钟设计 | docs/retui-invalidation-design.md |
| "为什么不做脏区追踪"的实测结论 | docs/perf-damage-tracking-verdict.md |
retui 框架入口(Viewtrait、Painter、Spring) | rust-modules/ui/src/lib.rs |
| UI 行为宪法(四条铁律 + 模块地图) | rust-modules/ui/src/CLAUDE.md |
| 首页视图树落地实现 | rust-modules/screens/src/home/mod.rs |
小结 ✨
retui 的故事,本质上是"在一台弱 ARM 电视上,如何用 Rust 写出既流畅又一致、还易于长期维护的界面":
- 用极小契约(
update → layout → draw+Painter/Spring/Env)把保留模式讲清楚; - 用具体类型字段 + 固定数组坚持静态分发、每帧零堆分配;
- 用像素级还原保证迁移不改变观感;
- 用整帧 present 门代替脏区追踪来省 CPU;
- 用四条设计铁律锁住视觉一致性。
它没有炫技,而是把"在受限硬件上把一件事做对"做到了极致——这正是理解"保留模式 UI 框架"时,一个非常值得借鉴的范本。
【免费下载链接】plx-native
Fast, unofficial Plex client for LG webOS TVs — native 60 fps UI, 4K HEVC/Dolby Vision, no Chromium or WebView.
相关推荐
Xilem 响应式架构解析:视图树 diff 驱动的 Rust 原生 UI 框架
Xilem 响应式架构解析:视图树 diff 驱动的 Rust 原生 UI 框架 本文基于 xilem/ARCHITECTURE.md https://link
前端桌面应用跨平台Xilem架构解析:基于Rust的响应式UI框架设计模式
Xilem架构解析:基于Rust的响应式UI框架设计模式 Xilem是一个实验性的Rust原生UI框架,采用函数式响应式架构设计,为开发者提供构建高性能、类型安
前端桌面应用跨平台Theme UI未来发展方向:构建下一代设计图框架的完整指南
Theme UI是一个基于约束设计原则构建可主题化用户界面的库,它正在成为现代前端开发中不可或缺的设计系统工具。作为Styled System的进化版本,The
前端UI组件设计系统
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考