
如果你维护过一个稍微复杂一点的 Android 列表页大概率经历过那种数据不多但它就是卡的绝望。也就是在那种背景下我注意到了 HyperFrame——GitHub 和很多技术社区里被写成 hyperframes 的那个库。它出自 Facebook2018 年开源背后是 Litho 的兄弟体系。这篇东西不是官方文档翻译而是我从头到尾跟这个框架打交道的记录先讲它到底在解什么题再拆它三板斧一样的核心设计接着是接入和实测的细节最后聊聊它为什么没过多久就并回 Litho以及它留给 Android 开发者的东西。为什么值得翻一个已经停止迭代的框架因为框架会过时但设计思路不会。HyperFrame 是我见过的把所有性能友好设计浓缩得最干净的一个不可变 UI 描述、异步布局、增量挂载这三样东西放到现在看依然是 Jetpack Compose 等新一代 UI 框架的基本底色。对想搞明白UI 框架为什么能快的人来说直接啃它的源码比看一堆二手概念实在得多。1. 先搞清楚 HyperFrame 要解的核心痛点Android UI 的性能账1.1 掉帧的本质主线程被渲染流程卡住了Android 的界面渲染简单说是一个三阶段的流水线measure测量、layout布局、draw绘制。这三个阶段默认全部跑在 UI 主线程上而且顺序执行、不能并行。系统每一帧留给你的时间大约是 16.6 毫秒也就是 60fps 下每个帧的预算。一旦主线程在这 16.6 毫秒内没干完活这一帧就会被丢掉表现就是掉帧、卡顿。问题在于复杂页面里这三个阶段的开销是非常不可控的。一个嵌套了两三层 LinearLayout 的列表项在测量阶段可能要被反复遍历多次——因为父布局要知道子布局的尺寸子布局反过来又要依赖父布局的约束。层级越深这种相互计算就越重。再加上列表滑动时不断有新的 item 进入屏幕每一个 item 都要重新经历一遍 measure/layout/draw主线程很快就扛不住了。1.2 传统优化手段为什么都是治标我们平时用的优化办法这块其实有个残酷的事实大部分都只是在边缘修补。布局层级优化、用 ConstraintLayout 扁平化能减少一部分测量开销但改变不了渲染计算必须串行跑在主线程这个根本约束。RecyclerView 的 ViewHolder 复用把创建 View的成本摊薄了但 item 一旦变复杂bind 阶段的赋值和重绘依然耗时。AsyncLayoutInflater 异步加载布局听着美好可它只能把 inflate 挪到后台线程measure/layout/draw 这些真正吃性能的步骤仍然在主线程而且它对于嵌套布局的异步化支持很弱。图片加载用 Glide/Fresco解决的是 IO 和内存问题不是布局计算问题。换句话说这些手段都是在主线程串行渲染这件事的外围打转。而 HyperFrame 这类声明式 UI 框架选择的是直接换一种玩法既然你必须在主线程渲染那能不能把其中大部分计算提前做完、用空间换时间、用不可变性换线程安全这就是它整个设计的出发点。1.3 HyperFrame 的定位一套轻量的声明式 UI 运行时声明式 UI 并不是什么新鲜概念React 在 Web 端早就证明了这条路走得通。Android 这边Facebook 先做了 Litho又做了 HyperFrame。两者出发点一致把 UI 描述成一份普通的、不可变的数据结构而不是直接操作一堆可变状态的 View 对象。这样这份描述可以在任意线程被计算、缓存、比较框架只需在必要的时刻把描述变成真正的界面元素。HyperFrame 和 Litho 最大的区别是它非常克制。Litho 带了自己的布局引擎基于 Yoga有完善的事件系统、预取、缓存是一整套重型框架。HyperFrame 没有去碰布局引擎它更关注如何用最小成本把 UI 描述从业务线程搬到 UI 线程并渲染出来。我用下来最大的感受是它的源码量很小核心逻辑几个文件就能讲清楚这对想研究框架源码的人来说是巨大的优势。2. 拆解 HyperFrame 的三板斧不可变描述、异步布局、增量挂载2.1 不可变的 UI 描述像拍照而不是现场指挥传统 Android 写法是命令式的LinearLayout container new LinearLayout(context); TextView textView new TextView(context); textView.setText(你好); container.addView(textView);每行代码都在直接操作一个真实存在的 View 对象。View 内部的属性随时可变于是你在任何线程改它都会产生线程安全问题测量、布局、绘制过程中如果某个属性突然变了渲染结果也可能不对。为了安全所有操作只能锁在主线程。HyperFrame 换了一种思路你先构建一棵不可变的 UI 描述树它不绑定任何真实 View。val screen ColumnComponent( children listOf( TextComponent(你好HyperFrame), ButtonComponent(点我) ) )这棵树创建之后就不可再修改跟拍了一张快照一样。它可以在任何线程被创建、被传递、被组合没有任何线程安全顾虑。框架拿到这棵描述树之后才在内部决定如何生成对应的真实 View。这个思想和 React 的 Virtual DOM、Jetpack Compose 的 UI 树本质上是同一件事——先有描述后有实体。2.2 异步布局把重活放到后台线程View 系统的 measure/layout 之所以没法直接搬到后台线程根本原因在于 View 对象本身是可变且有状态的测量过程中依赖了很多运行时的全局状态。HyperFrame 的取巧之处在于既然 UI 描述是不可变的那基于这份描述做的布局计算也就不需要在 View 对象上进行。框架可以在后台线程里拿着这份不可变的 UI 描述树递归计算每个节点的大小和位置得到一份布局计算结果。这个过程完全不碰真实 View只操作轻量的数据和坐标。计算完了把结果交给主线程主线程只需要照着这份结果去创建、摆放真实 View 即可。主线程上执行的从measure layout draw变成了按结果 mount成本小了一个量级。这个思路放到生活里类比就像装修房子传统做法是工人边问你要什么边当场砌墙改水电每一步都要你拍板HyperFrame 是先在图纸上把每个房间尺寸、插座位置全部计算清楚工人照图施工就行。图纸可以反复修改、在办公室算好施工现场只做执行。2.3 增量挂载滚动到哪渲染到哪RecyclerView 的 ViewHolder 复用已经算一种增量化但它的粒度是item 级别而且 item 内部的 View 层级每一次 bind 时都可能重新赋值和绘制。HyperFrame 的增量挂载走得更远它把整个屏幕的内容当成一棵描述树根据当前滚动位置只把可见区域对应的描述节点挂载成真实 View。滚出去的内容可以销毁滚进来的内容再按需创建每个节点创建的成本都被压到最低。这里有一个关键细节因为描述树是不可变的框架可以非常安全地只挂载部分节点不需要担心某个节点被其他逻辑引用和修改。如果描述是可变的增量挂载就必须附带一堆复杂的同步逻辑。所以不可变不是洁癖它是整个方案的基石。2.4 和 Litho 的关系同一个目标下的不同取舍很多人会混淆 HyperFrame 和 Litho。两者确实同源但定位差异明显对比项LithoHyperFrame发布方FacebookFacebook布局引擎自带 YogaFlexbox不自行实现依赖 Android View 系统线程模型异步布局 异步挂载异步布局 主线程增量挂载API 风格注解处理器 代码生成Kotlin DSL / 普通代码框架重量偏重功能完整极轻核心精简学习门槛较高相对较低我当时选择研究 HyperFrame就是因为它轻。Litho 里很多复杂的性能优化策略在 HyperFrame 里都被简化成了不可变 异步 增量这三个点。把这三个点吃透了回头再看 Litho、Compose会觉得它们都是同一个思想的不同外壳。3. 动手写一个 HyperFrame 页面核心 API 与接入示例3.1 环境准备和依赖引入先说环境。HyperFrame 用 Kotlin 编写要求在 Android Studio 3.x 以上、Kotlin 插件正常工作即可。但要注意一个现实问题这个框架没有像主流库那样长期维护并推到 Maven Central在它归档之前官方主要发布渠道是 GitHub 仓库和 jcenter。我当时的做法是直接把源码 fork 下来用其中的 library 模块构建一个本地 aar然后放到自己项目的 libs 目录下引入。如果你也想试最稳妥的路径就是clone 官方仓库搜 facebook hyperframe 即可找到。打开工程构建 hyperframe-library 模块得到 aar 文件。在自己的 app 里implementation files(libs/hyperframe-library-release.aar)。这不算复杂但对不熟悉源码构建的同学是一个小门槛。其实研究这种归档框架我非常建议直接 clone 源码配合 demo 模块一起看因为仓库里的 sample 项目是最完整的用法文档。3.2 核心概念HyperView、HyperScreen、HyperViewControllerHyperFrame 的三个核心类必须理清HyperView一个普通的 View 子类它是 UI 的宿主容器。它在内部接收UI 描述负责把描述挂载成真实的 View 树。HyperScreen一份不可变的 UI 描述同时描述了这个界面由哪个 Controller 管理。它是业务和框架之间的边界。HyperViewController负责管理界面逻辑、生命周期和数据更新。我按自己的理解写了一个最简页面class GreetingScreen( private val name: String ) : HyperScreenGreetingController() { override fun createController(activity: Activity): GreetingController { return GreetingController(activity) } override fun createComponent(activity: Activity): Component { return ColumnComponent( children listOf( TextComponent(Hello, $name), ButtonComponent(点击我) ) ) } } class GreetingController(activity: Activity) : HyperViewControllerGreetingScreen(activity) { override fun onAttach(view: HyperView) { super.onAttach(view) // 这里可以注册点击事件、加载数据等 } }接入 Activity 的写法也很直接class MainActivity : AppCompatActivity() { private lateinit var hyperView: HyperView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) hyperView HyperView(this) setContentView(hyperView) hyperView.attachScreen(GreetingScreen(Android)) } }注意attachScreen是关键动作它触发 HyperFrame 去后台线程计算布局然后回到主线程挂载内容。3.3 组件接口Component 与 ComponentBuilderHyperFrame 的组件树由一个个 Component 组成。官方 API 里常见的组件包括TextComponent文本。ImageComponent图片。ColumnComponent / RowComponent布局容器。RecyclerCollectionComponent列表容器。Or / SelectComponent条件渲染。我印象最深的其实是ComponentBuilder机制它允许你用 Kotlin DSL 的方式组装组件树可读性非常强override fun createComponent(activity: Activity): Component { return buildComponent { column { child(header()) child(content()) child(footer()) } } }这种写法在当时看起来非常现代。后面 Jetpack Compose 把这种 DSL 风格发挥到极致但 HyperFrame 在 2018 年就能这么写了确实领先。3.4 一个列表页面从数据到渲染的完整链路列表是移动端最常见也最考验性能的场景。我用 HyperFrame 写过一个简单信息流示例关键代码大概是这样的记住思路为主框架归档后 API 细节以官方仓库为准class FeedScreen( private val items: ListFeedItem ) : HyperScreenFeedController() { override fun createController(activity: Activity): FeedController { return FeedController(activity) } override fun createComponent(activity: Activity): Component { return buildComponent { recyclerCollection { item { TextComponent(items[0].title) } item { TextComponent(items[1].title) } // 实际项目这里会遍历 items 批量生成 } } } }recyclerCollection是 HyperFrame 对滚动容器的封装它内部会按可见区域增量挂载子节点。也就是说数据量再大首屏只会渲染屏幕内的那几项理论性能和 RecyclerView 加最优配置是同一水平但因为整个描述树不可变、布局在后台算滚动时的计算压力要小很多。3.5 更新 UI不要直接改 View而是换一份描述传统写法更新界面是textView.setText(新数据)拿到 View 的引用改属性。HyperFrame 的规矩是整个描述树替换。class FeedController(activity: Activity) : HyperViewControllerFeedScreen(activity) { fun refresh(newItems: ListFeedItem) { // 把一份全新的 Screen 交给 HyperView view.attachScreen(FeedScreen(newItems)) } }这意味着你永远不会陷入某个 View 的状态没同步的泥潭。每一次刷新都是从数据生成一份新描述框架自己处理差异。这种数据是唯一真实来源的模式后来在 Compose 里被总结为 State 驱动 UI。当年在 HyperFrame 里体会一遍再学 Compose 会快很多。4. 实测体感、适用边界以及我踩过的几个坑4.1 我自己跑 Demo 的真实感受说实话第一次把 HyperFrame 跑起来最直观的感受不是性能起飞而是开发思路真的不一样。你写界面时脑子里想的是组件树和数据结构不是 View 的层级和 id。调试一个界面你首先想的是我的数据长什么样描述树构建得对不对而不是去布局文件里翻约束条件。性能方面我用一个包含 50 个不同样式 item 的列表做了简单对比同样内容RecyclerView 版本在低端模拟器上滑动有偶发掉帧HyperFrame 版本体感更稳。但这里我要诚实我的对比非常粗糙不是严格的分钟级 frame time 统计。更准确的结论来自官方给出的信息——他们内部用它承载复杂页面后卡顿率明显下降。个人体感和官方结论方向一致但我不建议你把快三倍之类的话当真没有严谨 benchmark 的数字都是情绪。4.2 它适合什么不适合什么适合信息流、动态表单、消息会话等结构复杂、频繁更新的页面。页面层级深、item 样式多、对启动速度和滑动流畅度要求高的场景。想研究声明式 UI 原理的人HyperFrame 是绝佳学习素材。不适合简单静态页面。引入一套异步挂载机制完全是杀鸡用牛刀。需要大量自定义 View、Canvas 绘制的场景。HyperFrame 的优势在结构化 UI不是自由绘制。生产级大型 App 新项目。框架已经停止迭代和现代 Android 生态比如 Compose整合困难新项目没有任何理由选它。4.3 我踩过的几个实际坑坑一依赖引入渠道不稳定。我一开始想直接通过远程依赖引入结果发现仓库归档后原来的发布渠道不再维护最后只能走源码构建。功课没做好浪费了小半天。坑二生命周期配合。HyperFrame 的 Controller 有自己的 attach/detach 流程和 Fragment 的 onDestroyView 配合需要小心。我在一个 Fragment 里 attach 了 ScreenFragment 视图销毁时没有及时 detach结果出现了内存泄漏。处理方式是在 onDestroyView 里显式调用hyperView.detachScreen()。坑三复杂事件系统缺失。HyperFrame 毕竟轻量点击事件、手势处理可以正常工作但它没有一个像 Litho 那样完备的事件分发机制。如果你要做复杂手势、嵌套滚动联动会发现自己需要补很多胶水代码。坑四调试习惯要改。因为 View 是在挂载阶段才创建的传统 Layout Inspector 里的层级和你写的组件树不是一一对应的。想定位布局问题不能只看视图层级得回到描述树里去看。这一点和后来用 Compose 遇到的问题几乎一模一样。5. 从为什么并回 Litho说起它留下的东西5.1 合并不是失败是资源聚焦2019 年前后Facebook 把 HyperFrame 的开发终止相关能力整合回 Litho。对外说法是团队要把精力集中在 Litho 这个统一框架上。以我和类似框架打过交道的经验来看这其实是开源项目的常态技术验证完成思想被吸收独立项目的使命就结束了。两个框架同属一家公司目标高度重叠长期维护两套类似的东西对团队是巨大的资源浪费。与其说 HyperFrame 失败不如说它是完成了自己的任务——用最简洁的方式验证了不可变 UI 描述 异步布局 增量挂载这套组合拳在 Android 上是可行的。同一时期Jetpack Compose 也在走类似的路说明这个方向得到了行业级验证。5.2 这些思想现在还能怎么用虽然 HyperFrame 本身不用了但它的核心思想可以直接指导日常开发哪怕你仍然在使用传统 View 体系第一尽量把UI 状态和View 实例解耦。不要到处拿着 View 引用改属性而是维护一份数据模型通过数据变化触发界面更新。哪怕只是简单封装一个bind(data)方法也比散落一堆tvName.text ...要规范得多。第二预先计算布局参数。如果你的列表 item 高度是固定值时可以在后台线程预算好 item 尺寸避免主线程重复 measure。这一点在很多性能优化手册里都有原理跟 HyperFrame 的异步布局同源。第三对滚动列表做真正的增量渲染。RecyclerView 帮我们解决了 View 复用但 item 内部的复杂布局绑定依然是性能瓶颈。如果 item 里有大量不可见的部分可以按需加载千万不要一个 item 把所有子 View 全部创建出来。5.3 值得读的源码片段如果你想进一步学习我建议按这个顺序读 HyperFrame 源码HyperView类看它如何接收 Screen、触发布局计算、最终 mount View。HyperViewController看生命周期管理和数据更新的边界。布局调度部分看后台线程如何计算结果并切换到主线程。源码量比 Litho 少一个量级适合一个周末啃完。我当年啃完之后最大的收获是终于理解了线程安全在 UI 框架里到底意味着什么、以及不可变性如何从根上规避了复杂同步问题。这种认知靠背八股文是学不来的。6. 最后分享一点实在的东西我个人的体会是hyperframes 这个名字在今天不会出现在任何新项目的依赖里但它背后的方法论是每一个做 UI 开发的人都值得花时间搞懂的。如果你想把 UI 性能吃透与其反复调 RecyclerView 的参数不如花一个周末读一遍这种轻量框架的源码——它花不了你多少时间但能把你对UI 如何变快的理解从技巧堆叠提升到模型层面。我也建议如果你看完这篇文章对 HyperFrame 产生了兴趣不要急着往项目里引先去 GitHub 把源码拉下来把 sample 跑起来亲手改一改组件树感受不可变描述和增量挂载带来的差异。等你真正理解了这套模型再回头去看 Compose 的浅谈、Litho 的深度就会觉得这些东西其实是同一个故事的不同版本。最后提醒一句生产环境选型永远要优先考虑维护活跃度HyperFrame 只适合出现在你的学习清单里不适合出现在生产依赖里。