---
title: "问题排查 — zenit Zig UI 文档"
description: "从构建错误、状态生命周期到渲染异常，按最短路径定位原因。"
url: https://zenit.z.express/zh/docs/reference/troubleshooting
language: zh-CN
alternate_en: https://zenit.z.express/docs/reference/troubleshooting.md
alternate_es: https://zenit.z.express/es/docs/reference/troubleshooting.md
alternate_ja: https://zenit.z.express/ja/docs/reference/troubleshooting.md
alternate_ko: https://zenit.z.express/ko/docs/reference/troubleshooting.md
alternate_fr: https://zenit.z.express/fr/docs/reference/troubleshooting.md
alternate_de: https://zenit.z.express/de/docs/reference/troubleshooting.md
project: zenit v0.1.0-alpha (Zig 0.15.2, macOS)
source: https://github.com/version-next/zenit
---

# 问题排查

从构建错误、状态生命周期到渲染异常，按最短路径定位原因。

## 构建错误

**invalid fingerprint**

模板的指纹不能直接复用。删除 `build.zig.zon` 里的 `.fingerprint` 行，运行一次 `zig build`，把 Zig 输出的唯一值填回去。

**import of file outside module path**

不要对框架内部文件直接执行 `zig test`。使用 `zig build test-ui` 等仓库定义的测试 step（见[命令速查](https://zenit.z.express/zh/docs/reference/commands)）。

**@import("ui") / @import("zenit_app") not found**

确认 `build.zig.zon` 声明了 `.zenit` 依赖，并且创建 executable 之后调用了 `zenit.attach(zenit_dep, exe)`——它负责添加 `ui` / `zenit_app` 模块、编译 macOS 桥接并链接系统框架。

**build API field mismatch**

先运行 `zig version`。zenit 要求 Zig 0.15.2（`build.zig.zon` 的 `minimum_zig_version`），其他版本的 build API 不在支持范围。

**-Dtest-mode=true has no effect**

Zig 的依赖选项是隔离的。在 `b.dependency("zenit", ...)` 中显式转发 `.@"test-mode"` 与 `.@"e2e-port"`，参考模板 `templates/minimal-app/build.zig`。

## 运行时与状态

**error.StateNotFound**

显式 ID 的 `cx.handler(State, id, method)` 在对应 state 创建之前被调用。优先改用 `cx.bindState` + `cx.on`：直接传状态指针，不再管理 ID。

**leaked ArrayList / HashMap at exit**

`bindState` 的状态归 Cx 所有，Cx 释放时一起释放；如果 T 声明了 **pub** 的 `deinit(self: *T)`，框架会自动调用它。泄漏通常意味着 `deinit` 不是 pub，或者资源本该跟随页面生命周期。写法见下一节。

**crash after switching pages**

检查是否在全局状态里保存了上一个页面的 `*Node` 或 `*Scope`。页面 Scope dispose 之后这些指针全部失效，必须同时清掉。

**Invalid free after updating text**

节点原有文本可能是节点自己持有的副本。不要把 `getText()` 拿到的 props 改了 `content` 再 `setText` 回去；用 `try node.setTextContent(cx.allocator, "Updated")`，它会复制字符串并正确标记所有权。

## 状态清理的正确写法

在状态里保存 allocator 字段，让 pub `deinit` 自己完成清理。初始值不能已经持有资源——在 `bindState` 之后再分配。

`state_deinit.zig`

```zig
const Editor = struct {
    allocator: std.mem.Allocator,
    lines: std.ArrayList([]const u8) = .empty,

    // pub: the Cx calls this automatically when it frees the state.
    pub fn deinit(self: *Editor) void {
        self.lines.deinit(self.allocator);
    }
};

// The initial value must not own resources yet; allocate after binding.
const editor = try cx.bindState(Editor, .{ .allocator = cx.allocator });
```

只有两种情况才用 `scope.onCleanup`：清理函数不是 pub `deinit`，或者资源必须跟随某个 Scope（页面）而不是整个 Cx 的生命周期。

`scope_cleanup.zig`

```zig
// Resources that must follow a page (Scope), not the whole window (Cx).
const PageCache = struct {
    allocator: std.mem.Allocator,
    map: std.StringHashMapUnmanaged(u32) = .empty,

    // Not named `pub fn deinit`: the Cx would call it again and double-free.
    fn release(self: *PageCache) void {
        self.map.deinit(self.allocator);
    }
};

const cache = try cx.bindState(PageCache, .{ .allocator = cx.allocator });
try scope.onCleanup(PageCache, cache, PageCache.release);
```

> WARNING
> 
> **不要重复注册 deinit。** T 已经有 pub `deinit` 时，再用 `scope.onCleanup` 注册同一个函数会导致重复释放：Scope dispose 调一次，Cx 释放状态时又调一次。

## 布局与绘制

**state changed but the text did not**

`setText` 本身会比较新旧签名，并按需标记 sizing / render dirty，不需要手动 `markRenderDirty`。文本没刷新通常是改了一份 `TextProps` 副本却没有调用 `setText`，或者显示的值没有订阅对应的 Signal。更推荐用 `ui.textFmt(cx, scope, fmt, .{ signals }, props)` 直接订阅 Signal / Memo。

**some colors ignore a theme switch**

`boxStyled` / `textStyled` 等构造器会挂 `on_theme` hook，`cx.setTheme` 只在 `cx.root` 子树上重放它们；普通节点和组件库的样式是 mount 时的快照。主题切换时重建相应组件树，或在 Effect 里订阅 `cx.themeSignal(scope)` 并更新样式；没挂在 `cx.root` 下的节点也不会被更新。

**clicks fall through an overlay’s empty area**

没有交互行为的纯视觉容器默认是 `pass_through`。在浮层根节点的样式扩展上设置 `hit_behavior = .@"opaque"`——或者直接使用自带 barrier 的 [Modal](https://zenit.z.express/zh/components/modal) / [Sheet](https://zenit.z.express/zh/components/sheet)。

**a huge invisible hit area in the window**

挂上 `ui.devtools.overlay` 检查命中的节点，并用 `node.globalRect()` 打印它的屏幕矩形。通常是父容器的 fill / grow 配置或绝对定位范围过大；浮层未显示时也要确认它已摘出命中测试——用 `node.setDisplay(.none)` 隐藏的子树会整棵退出布局、绘制、命中与 Tab 遍历。

`opaque_overlay.zig`

```zig
// Plain visual containers are pass-through by default.
// Make an overlay root swallow clicks on its empty area:
(try panel.style.ensureExtFallible(cx.allocator)).hit_behavior = .@"opaque";
```

## 仍然无法解决

提交问题时至少附上：

-   **版本**：zenit revision、`zig version` 与 macOS 版本。
    
-   **复现**：最小复现代码与执行的 build step。
    
-   **输出**：完整的错误输出，不要只截最后一行。
    
-   **渲染问题**：截图，以及 DevTools 中相关节点的尺寸。
