Skip to content

Latest commit

 

History

History
518 lines (398 loc) · 24.5 KB

hiro-compose-arch.md

File metadata and controls

518 lines (398 loc) · 24.5 KB

Hiro Compose 架构

设计目标

Hiro Compose 的目标,是让 Android API 28 及以上直接运行 Skiko/Skia 版 Compose,而非 AndroidX Compose。

这使得:

  • 低版本 Android 也能使用 SkSL Shader
  • Android、Desktop、WASM、iOS 可复用同一套 Skiko Compose UI 与视觉实现

核心模式:

Hiro Compose
= 固定上游底板
+ 顶替(剥离不适用实现 + 提供同包同名实现)
+ Android/Skiko 胶水
+ Hiro 自有能力

固定底板

Hiro 目前以 CMP 1.12.2 的 Desktop 变体作为固定底板。Desktop 本就是 Skiko Compose 的一具体平台实现,并且与 Android 同属类 JVM 体系,适合作为移植基线。

Hiro 不直接维护一份 Compose Multiplatform 源码自行老改,原因是上游代码和 Hiro 介入点会混在一起,难以审计真实修补面、形成成熟的修补方案,且难以升级上游:升级时需要重新拉取上游源码并重新大量修改源码

Hiro 将官方二进制工件作为输入,通过声明式处理为 Processed Jar 依赖。升级上游时,只需要重新小量调修顶替与垫片。

构造模式

底板

我们在 packages/compose/build.gradle.kts 中如上文所述地固定了上游工件为底板。

顶替

我们的顶替,严格等于:

从 Processed Jar 剥离原实现
+ 在 Hiro 源码中提供同包、同名、同 ABI 实现

典型顶替包括:

  • Android 不兼容的 Compose 平台实现
  • Lifecycle Compose 的线程绑定实现
  • collectAsStateWithLifecycle 的调度实现
  • Navigation3 的 ViewModel 装饰器
  • Savable(注:官方拼写成 Saveable,而我采取当前拼写)序列化入口
  • 字体加载器:使安卓 Font(R.font.xxx) 能经 Skia 加载
  • Material3 的平台水波纹入口:接入 Hiro 着色器水波纹
  • JetBrains Compose 资源的读取器与系统环境:先读 assets 再读类加载器,环境取自安卓 Configuration

另有一类「门面转发」:Skiko 版 UI 按桌面版 Lifecycle、NavigationEvent 的门面类取 HostDefaultKey,而 Hiro 使用的是它们的安卓版工件,Hiro 便以桌面门面类名转发到安卓版的同一个键,使二者判等。

顶替不是运行时 Hook,也不是依赖解析的偶然性覆盖,而是确定的、稳定的。原类在 Jar Processing 中精准剥离,再由 Hiro 提供船新实现。

胶水

胶水负责连接 Android 宿主与 Skiko Compose 世界:

  • MotionEvent -> ComposeScenePointer
  • View 尺寸、Density、Insets、安卓主题模式、触摸输入与IME等等 -> PlatformContext 状态
  • Activity Lifecycle -> Hiro Lifecycle
  • Android BackDispatcher 与 Hiro Navigation 的双向握手
  • Android Context、Configuration、Resources -> LocalContext、LocalConfiguration、LocalResources
  • Android Bundle 与 Hiro SavedState 的快照运输
  • Choreographer 帧信号驱动 Skia 按需绘制

胶水不拥有 Composition、ViewModelStore 或 SavedStateRegistry,也不在 Android 主线程执行 Compose 行为。

Hiro 自有能力

Hiro 独立提供:

  • HiroComposeView
  • HiroSkiaLayer 与 HiroSkiaSurfaceView(实现在 Hiro Skia 中,严格来说不在 Hiro Compose)
  • Compose、Skia、GL 同线程的直通渲染管线
  • Hiro Architecture Components Owner
  • 输入合流、IME、窗口信息、Insets、Back 等 Android 适配
  • 安卓 R 与 Assets 资源接口:字符串、颜色、尺寸、矢量(含渐变)、位图、九宫格、Shape、动画矢量与字体
  • 日志门控 HiroLog:Release 只输出警告与错误,R8 剥离其余调用
  • 添头:Savable 自动 Kotlinx Serialization 编解码、可客制化的 HiroSavableStateCodec

构建期纯洁性

采用 Hiro 时,Compose 相关包只能由 Hiro 提供。我们另提供了 HGP 负责治理消费侧依赖:

Hiro 自身模块 → 直接放行 官方 Compose 模块 → 阻断 官方 Lifecycle/SavedState 适配层 → 阻断 Skiko(含桌面版 skiko-awt)→ 阻断,由 Hiro Skia 提供 纯安卓模块应用 Compose 插件 → 代为生成 JetBrains Compose 资源的 Res 第三方 KMP Compose 库 → 选择 Skiko/Desktop 变体 桌面变体中的桌面平台类型 → 重新链接到安卓 最终 classpath → 做所有权与类泄漏校验

基础 androidx.lifecycle.ViewModel、Lifecycle、SavedState 等类型仍由 Android 与 Hiro 共用。Hiro 接管的是它们的 Compose 适配层,以及运行在 Hiro 行政区内的 Owner 实例。

平台重新链接

共用安卓的基础类型,带来一个缝隙:公共层 expect 的某些类型在桌面上是真类、在安卓上却是别名。典型如 SavedState——桌面上是包着 Map 的类,安卓上是 Bundle。第三方包换成桌面变体后,其字节码按桌面真类链接,到了安卓便会 NoSuchMethodError。

HGP 以 Gradle 产物转换处理:Jar 产物默认“未链接”,Hiro 接管的类路径请求“已链接”,转换时依平台别名表把桌面类型改名为安卓类型,相当于按安卓 actual 重新链接;不含这些类型的 Jar 原样放行。

桌面真类的专属成员会经 inline 函数漏进调用方(如 savedState {} 内联出的 new SavedState(map) 与 state.map),改名后安卓上并不存在,故改写到 Hiro Compose 中的桥接类 HiroSavedStateBridge。改写不改变操作数栈形状,栈映射帧无需重算;遇到表外的专属成员访问则直接报错,不放任其带病进包。

一个世界,两个“行政区”

Hiro 不隔离 ClassLoader,也不克隆基础架构类型。Android 与 Hiro 位于同一进程、同一类型世界,可以共享业务对象和依赖注入容器;但各自拥有线程敏感的可变 Owner。

行政区 持有者线程 持有内容
Android 主线程 Activity、Android Lifecycle、BackDispatcher、Android SavedStateRegistry
Hiro Hiro Render Thread ComposeScene、Composition、Hiro Lifecycle、Hiro ViewModelStore、Hiro Navigation、Hiro SavedStateRegistry

二者共享:

  • 同一个 androidx.lifecycle.ViewModel 类型
  • 同一个 Koin 容器
  • 普通业务对象、Repository 和不可变数据

二者不共享:

  • 同一个 LifecycleRegistry
  • 同一个 ViewModelStore
  • 同一个 SavedStateRegistry
  • 任何具有固定线程所有权的可变 Owner

因此,Activity ViewModel 与 Hiro 屏幕 ViewModel 可以使用同一种类型和同一个 Koin 容器,但它们由不同的 ViewModelStoreOwner 定义作用域,不会互相清理或越过线程边界。

线程所有权与跨区通道

“两条核心线程”特指 Hiro Compose 关键路径上的两个线程所有权域,并不表示应用进程不能存在 IO、网络或业务工作线程。

同一进程内的多个 Hiro View 共用一个 EGL 执行器、Skia DirectContext 和渲染线程,各自持有 Scene、Owner 与 Surface;最后一个渲染目标释放后,共享线程退出。

flowchart LR
    subgraph Android["Android 行政区 · 主线程"]
        Activity["Activity / ComponentActivity"]
        View["HiroComposeView"]
        Host["HiroAndroidHostBridge"]
        Choreographer["Choreographer"]
        AndroidState["Android SavedStateRegistry"]
        AndroidBack["OnBackPressedDispatcher"]
    end

    Commands["保序命令邮箱"]
    Snapshots["原子快照运输"]

    subgraph Hiro["Hiro 行政区 · Skia 渲染线程"]
        Controller["HiroComposeRenderController"]
        Scene["HiroSkiaComposeScene"]
        Context["HiroGoldenMambaContext"]
        Owner["Hiro Architecture Owner"]
        Compose["Composition / Measure / Layout"]
        Canvas["Skia Canvas / GPU Surface"]
    end

    Activity --> Host
    View --> Commands
    Host --> Commands
    Commands --> Controller
    Controller --> Scene
    Scene --> Context
    Context --> Owner
    Scene --> Compose
    Compose --> Canvas

    AndroidState <--> Snapshots
    Snapshots <--> Owner
    AndroidBack --> Commands
    Owner -->|"是否接管返回"| AndroidBack
    Compose -->|"invalidate / needRender"| Choreographer
    Choreographer -->|"requestRender"| Controller
Loading

线程不变量:

  • Android 主线程只采集平台事件、更新宿主关系和安排帧
  • Composition、Snapshot Apply、Measure、Layout、手势消费和 Draw 由 Skia 渲染线程执行
  • EGL、Skia GPU Surface、Canvas 只由 Skia 渲染线程访问
  • Compose Runtime 的全局 Snapshot Apply 由各场景 FrameRecomposer 向 GlobalSnapshotManager 登记的渲染调度器派发;平台层的延时任务若找不到当前调度器,则经 HiroSnapshotApplyDispatcher 投递给已注册的 Hiro 渲染调度器,尚无调度器时暂存,均不回落到 Android Main
  • 组合中可取得 LocalContext 等安卓对象,但它们也运行在渲染线程上,只宜做读取资源等线程安全的操作
  • 跨区可变数据只能通过命令或快照副本运输
  • 不把 Android Owner 暴露给 Hiro Composition,也不把 Hiro Owner 交给 Android 主线程操作

启动与关闭时序

启动

setContent 可以先于挂载。内容先保存在 View 中,挂载时创建 HiroComposeHostSession 并投递到命令邮箱;渲染线程可用且 Viewport 为正尺寸后才创建 Scene。

sequenceDiagram
    participant Main as Android 主线程
    participant View as HiroComposeView
    participant Mailbox as 命令邮箱
    participant Layer as HiroSkiaLayer
    participant Render as Skia 渲染线程
    participant Scene as HiroSkiaComposeScene

    Main->>View: 创建并 setContent
    View->>View: 保存内容
    Main->>View: onAttachedToWindow
    View->>View: 创建宿主会话
    View->>Mailbox: 投递 SetContent 命令
    View->>View: 挂载 Lifecycle、SavedState、Back 桥
    View->>Layer: attachTo
    Main->>View: onSizeChanged
    View->>Mailbox: 发布有效 Viewport
    Layer->>Render: 渲染线程开始执行任务队列
    Render->>Mailbox: 获取本轮命令快照
    Render->>Scene: 创建 Scene 与 Hiro Owner
    Render->>Scene: 先应用 Viewport,再执行 setContent
Loading

Scene 创建的关键前置条件是“渲染线程可执行任务”与“Viewport 为正尺寸”,不要求 GPU Surface 已经就绪。GPU Surface 由独立的 Renderer 状态机管理;首个可绘制 Surface 的尺寸仍会作为权威校正。

关闭

宿主会话的关闭不可逆:主线程先封闭输入端,再阻塞等待渲染线程完成资源销毁,最后拆除 Android 宿主桥。

View 脱离时只销毁当前会话,重新挂载会创建新会话、Scene 与 Owner,并从最近的 SavedState 检查点恢复。显式 close() 或宿主销毁才永久关闭 View。

sequenceDiagram
    participant Main as Android 主线程
    participant Controller as 渲染控制器
    participant Layer as HiroSkiaLayer
    participant Render as Skia 渲染线程
    participant Scene as HiroSkiaComposeScene
    participant Owner as Hiro Owner
    participant Host as Android 宿主桥

    Main->>Controller: beginClose
    Controller->>Controller: 状态进入 Closing,拒绝新命令
    Main->>Layer: close
    Layer->>Render: 释放当前渲染目标
    Render->>Controller: onRenderThreadClosing
    Controller->>Controller: 清空命令邮箱
    Controller->>Scene: close
    Scene->>Scene: 关闭 ComposeScene
    Scene->>Owner: 关闭 PlatformContext 与 Owner
    Owner->>Owner: Navigation dispose、Lifecycle DESTROYED、清空 ViewModelStore
    Controller->>Controller: 关闭 Dispatcher,状态进入 Closed
    Render-->>Main: 资源释放完成
    Main->>Host: 移除 Lifecycle、SavedState、Back 挂载
Loading

若 View 在渲染线程创建前就被关闭,则走无 Scene 的快速关闭分支,但状态仍然必须经过 Closing -> Closed。

正交状态机

运行时不是一张大状态机,而是几张职责不同、相互约束的状态机。Controller 是否拥有 Scene、宿主是否暂停、GPU Surface 是否可画、Hiro Lifecycle 位于什么等级,不混为一个状态。

Compose 渲染控制器

Running 表示 Scene 已创建,不表示 Activity 一定处于前台。

stateDiagram-v2
    state "Scene 尚未创建<br/>WaitingForRenderThread" as Waiting
    state "Scene 已创建<br/>Running" as Running
    state "正在关闭<br/>Closing" as Closing
    state "已经关闭<br/>Closed" as Closed

    [*] --> Waiting
    Waiting --> Waiting: 接收并暂存命令
    Waiting --> Running: 渲染线程可用且 Viewport 有效
    Waiting --> Closing: View 提前关闭
    Running --> Running: 处理命令、状态与绘制
    Running --> Closing: View 脱离或宿主销毁
    Closing --> Closed: 释放完成
    Closed --> [*]
Loading
Controller 状态 接受新命令 Scene 允许绘制
WaitingForRenderThread 是 无 否
Running 是 有 取决于宿主和 Surface 状态
Closing 否 正在释放或无 否
Closed 否 无 否

Skia 宿主与 GPU Renderer

Skia 宿主控制帧循环是否运行;GPU Renderer 控制当前是否存在可绘制 Surface。暂停宿主不销毁 Compose Scene,Surface 重建也不重建 Hiro Owner。

stateDiagram-v2
    state "宿主运行<br/>Running" as HostRunning
    state "宿主暂停<br/>Paused" as HostPaused
    state "宿主关闭中<br/>Closing" as HostClosing
    state "宿主已关闭<br/>Closed" as HostClosed

    [*] --> HostRunning
    HostRunning --> HostPaused: Hiro Lifecycle 离开 RESUMED
    HostPaused --> HostRunning: Hiro Lifecycle 进入 RESUMED
    HostRunning --> HostClosing: releaseRenderer
    HostPaused --> HostClosing: releaseRenderer
    HostClosing --> HostClosed: 渲染线程释放完成
Loading
stateDiagram-v2
    state "等待 Surface<br/>WaitingForSurface" as WaitingSurface
    state "Surface 可绘制<br/>SurfaceReady" as SurfaceReady
    state "Renderer 已关闭<br/>Closed" as RendererClosed

    [*] --> WaitingSurface
    WaitingSurface --> SurfaceReady: onSurfaceChanged 创建 Skia Surface
    SurfaceReady --> WaitingSurface: EGL 或 Surface 重建
    WaitingSurface --> RendererClosed: 释放 Renderer
    SurfaceReady --> RendererClosed: 释放 Renderer
    RendererClosed --> [*]
Loading

Hiro Lifecycle

Android Lifecycle 只在主线程读取。HiroComposeView 结合宿主状态与 View 可见性计算目标状态,再以命令形式交给渲染线程上的 Hiro Owner。

stateDiagram-v2
    [*] --> CREATED: Owner 创建、恢复 SavedState
    CREATED --> STARTED: Android 宿主至少 STARTED
    STARTED --> RESUMED: 宿主 RESUMED 且 View 可见
    RESUMED --> STARTED: View 不可见或宿主暂停
    STARTED --> CREATED: Android 宿主停止
    RESUMED --> CREATED: 宿主直接失活
    CREATED --> DESTROYED: Scene 关闭
    STARTED --> DESTROYED: Scene 关闭
    RESUMED --> DESTROYED: Scene 关闭
    DESTROYED --> [*]
Loading

映射规则:

Android 宿主与 View Hiro Lifecycle Skia 宿主
宿主 RESUMED 且 View 已挂载、可见 RESUMED Running
宿主为 STARTED,或宿主为 RESUMED 但 View 不可见 STARTED Paused
宿主低于 STARTED CREATED Paused
宿主 DESTROYED 或 View 脱离 DESTROYED Closing -> Closed

Hiro Lifecycle 的不变量:

  • Registry 由渲染线程创建并使用 LifecycleRegistry.createUnsafe
  • 活动期间只迁移到 CREATED、STARTED、RESUMED
  • DESTROYED 只由关闭流程产生
  • 迁移到 CREATED 或 STARTED 时建立 SavedState 检查点
  • ViewModelStore.clear() 只在 Hiro Owner 关闭时执行

若宿主没有 ViewTree LifecycleOwner,桥接层把宿主视为 RESUMED,由 View 的挂载与可见性提供降级驱动;这不会把 Android LifecycleRegistry 引入 Hiro 行政区。

按需直通帧循环

HiroSkiaSurfaceView 基于 SurfaceView,自行合并帧请求,经 Choreographer 驱动共享 EGL 渲染线程。没有失效、输入或平台状态变化时,不持续空转绘制。

sequenceDiagram
    participant Compose as Compose 或 Hiro 状态
    participant Layer as HiroSkiaLayer
    participant Main as Android 主线程
    participant Choreographer as Choreographer
    participant Render as Skia 渲染线程
    participant GPU as Skia GPU Surface

    Compose->>Layer: invalidate / needRender
    Layer->>Main: 合并为一次待安排帧
    Main->>Choreographer: postFrameCallback
    Choreographer->>Main: doFrame(nanoTime)
    Main->>Render: HiroSharedEglRenderTarget.requestRender
    Render->>Render: 消费帧时间与本轮命令快照
    Render->>Compose: FrameRecomposer.performFrame:Snapshot Apply、重组与帧动画
    Render->>Compose: ComposeScene.measureAndLayout
    Render->>GPU: ComposeScene.draw 当前 Canvas
    Render->>GPU: flushAndSubmit
Loading

直接渲染链路是:

ComposeScene -> 当前 GPU Surface 的 Skia Canvas -> flushAndSubmit

不存在 Compose 线程生成 SkPicture、GL 线程再回放的中间层。Composition、绘制指令生成、Skia Canvas 调用和 GPU 提交都发生在同一渲染线程。

每个 Hiro 场景持有自己的 FrameRecomposer,它以该场景的渲染调度器为宿主,并向 GlobalSnapshotManager 登记,使全局快照写入在渲染线程上合并派发。渲染本帧时引起的场景失效由帧末统一检查,仍有失效或动画等待时才请求下一帧。

命令邮箱与输入状态机

命令语义

主线程产生的内容、Lifecycle、Viewport、Insets、输入模式、Back 和指针事件统一进入 HiroComposeCommandMailbox。

  • 指针等离散事件严格保序
  • Environment、Viewport、Insets、InputMode 的数据使用原子快照表达“最新值”
  • 每轮 drain 只消费进入该轮前的命令快照
  • drain 期间新到达的命令留给下一轮,避免输入洪峰无限延长当前帧
  • Closing 后邮箱停止接收命令并被清空

指针输入

flowchart LR
    Motion["Android MotionEvent"]
    Adapter["指针 ID 映射与 Compose 事件转换"]
    Mailbox["HiroComposeCommandMailbox"]
    Batch["本轮不可变命令快照"]
    Scene["ComposeScene.sendPointerEvent"]
    Gesture["Compose 手势与滚动系统"]

    Motion --> Adapter
    Adapter -->|"Press / Move / Release / Cancel"| Mailbox
    Mailbox -->|"相邻且拓扑兼容的 MOVE 合流"| Mailbox
    Mailbox --> Batch
    Batch --> Scene
    Scene --> Gesture
Loading
stateDiagram-v2
    [*] --> Idle
    state "无活动指针" as Idle
    state "存在活动指针" as Active

    Idle --> Active: Press,建立稳定 Compose PointerId
    Active --> Active: Move,保序或与相邻 MOVE 合流
    Active --> Active: 多指 Press 或 Release,拓扑变化形成屏障
    Active --> Idle: 最后一个指针 Release
    Active --> Idle: Cancel,清空指针映射并取消 Compose 输入
Loading

MOVE 合流规则:

  • 只合流邮箱尾部相邻、类型均为 MOVE、指针拓扑兼容(“同一根手指”)的事件
  • 保留最新位置、压力和事件时间
  • 被折叠采样进入最近 100ms 的 HistoricalChange
  • Press、Release、Cancel、普通命令和指针拓扑变化都是合流屏障
  • 不损失总位移与速度追踪所需历史,因此慢速拖动与快速 fling 使用同一条正规输入路径

SavedState 快照运输

Android SavedStateRegistry 与 Hiro SavedStateRegistry 从不共享实例。HiroSavedStateTransport 只运输 Bundle 副本,不代理 Registry 操作。

sequenceDiagram
    participant Android as Android 主线程
    participant Registry as Android SavedStateRegistry
    participant Transport as HiroSavedStateTransport
    participant Render as Skia 渲染线程
    participant Owner as Hiro Architecture Owner

    Android->>Registry: HiroComposeView attach
    Registry-->>Android: consumeRestoredStateForKey
    Android->>Transport: 一次性写入恢复 Bundle 副本
    Render->>Transport: Scene 创建时一次性消费恢复快照
    Transport-->>Owner: performRestore
    Owner->>Owner: 恢复 SavedStateHandle 与 Savable 状态
    Owner->>Transport: checkpoint 发布最新 Bundle 副本
    Registry->>Transport: Android 保存时调用 Provider
    Transport-->>Registry: 返回最新 Bundle 副本
    Owner->>Transport: 暂停或关闭时再次 checkpoint
Loading

运输不变量:

  • 恢复快照只能由 Android 主线程设置一次,并由 Hiro Owner 消费一次
  • 每次跨线程读写都复制 Bundle
  • Android 保存过程不进入 Hiro Registry,也不阻塞等待 Hiro 现场序列化
  • Hiro 在初始化、离开前台和关闭时主动发布检查点
  • 没有 Android SavedStateRegistryOwner 时,Hiro 状态仍可在当前会话内工作,但不能跨 Activity 重建恢复

Savable 在 Hiro Registry 内完成。官方可直接保存的值沿用基础规则;带有 Kotlinx Serialization 序列化器的对象可自动编解码;其他类型可以通过 HiroSavableStateCodec 扩展。Pager、Navigation 或其他上层组件不需要特例。

Navigation Back 双向握手

Android 不直接访问 Hiro Navigation 栈。Hiro 先声明当前是否存在可处理返回的 Handler,Android 再决定是否启用 OnBackPressedCallback。

flowchart LR
    Handler["Hiro Navigation Handler"]
    Input["HiroNavigationEventInput"]
    MainCallback["Android OnBackPressedCallback"]
    SystemBack["系统返回"]
    Mailbox["NavigationBack 命令"]

    Handler -->|"Handler 可用性变化"| Input
    Input -->|"切到主线程启用或禁用"| MainCallback
    SystemBack --> MainCallback
    MainCallback --> Mailbox
    Mailbox -->|"渲染线程 dispatchOnBackCompleted"| Handler
Loading

此闭环保证:

  • 没有 Hiro Handler 时,Hiro 不拦截 Android 返回
  • Handler 状态只在 Hiro 渲染线程判断
  • Android Callback 只在主线程启停和接收系统事件
  • 真正的 Navigation 状态迁移仍在 Hiro 渲染线程完成

相关主要代码