1. 先承认一件事:多数 UIManager 活不过项目第二年
1.1 它开始的样子:一个单例,几个 Show/Hide
每个 Unity 项目的 UI 模块,几乎都是从同一个模子刻出来的:一个 UIManager 单例,一个存着所有面板预制体引用的字典,几个ShowPanel("MainMenu")、HidePanel("Settings")这样的公开方法。前三个月它确实好用,策划提个需求,你十分钟就能把新界面挂上去。项目做到中期你会发现,这个 UIManager 已经变成了一个谁都不敢动的核心类——打开它的脚本文件,3000 行起步,里面塞满了各种特殊逻辑:某个界面打开前要先关掉另一个、某个弹窗要等 0.5 秒才出现、某个界面的层级要动态调整……
这不是你的问题,而是几乎所有自研 UIManager 的宿命。UI 系统是游戏里最贴近业务、变动最频繁、耦合面最广的模块,它天然会吸收项目里所有的"特殊情况"。如果你没有在一开始就给它划出清晰的设计边界,它就会慢慢膨胀成一头怪兽。我见过太多团队在项目后期把大量时间耗在"改一个界面导致另一个界面显示异常"这种破事上,根子都在于边界不清。
这篇文章我想把过去几年在多个 Unity 项目里折腾 UI 架构的经验整理成 7 个设计边界。它们不是某个具体框架的源码解读,而是你在设计任何 UIManager 或 UI 框架时都必须回答的七个问题:层级归谁管、生命周期归谁管、数据归谁管、事件归谁管、资源归谁管、复用归谁管、性能归谁管。每个问题后面我都会给出踩坑记录和落地建议。
1.2 失控信号自查:你项目里的 UIManager 到哪个阶段了
在展开七条边界之前,先做个自查。如果你项目里出现下面这些现象,说明边界已经在失守了:
- 打开界面 A 的代码里,出现了一行注释"必须先关掉界面 B,否则层级会乱"
Canvas的Sorting Order字段被美术和程序反复改,谁都不知道当前最大值是多少- 同一个界面,在不同业务逻辑里被以三种不同的方式打开:直接
Instantiate、走 UIManager、藏在某个场景物体上 - 关闭界面时经常报"MissingReferenceException",因为你引用的物体在另一个界面关闭时被销毁了
- 界面上显示的数值,是 View 自己通过
GameObject.Find或者FindObjectOfType去拿的 - 切场景之后,某个面板的背景图变成洋红色(资源被卸载了)
- 每次 UI 打图集,都要全组开会协调,因为大家都在往同一个图集里塞东西
如果中了三条以上,这篇文章值得看完。就算一条没中,提前知道这些边界在哪里,也能让你在设计阶段少走弯路。
2. 边界一:层级边界——谁盖住谁,不该由 Add 顺序决定
2.1 SortOrder 失控曲线
UI 层级是所有边界里最先崩掉的。一开始大家的方案很简单:每个面板一个Canvas,通过Sorting Order控制前后。这个方案在前 20 个界面时完全没有问题,但界面数量到 50 个以后,你会发现一个问题——你根本记不住当前哪些数字被占了。
举个例子:弹窗系统用了Sorting Order = 100,新手引导用了 101,商城界面里的某个道具详情浮层用了 102,然后某天策划说要在弹窗上面加一个充值引导,你翻遍代码发现 103 已经被一个写死的飘字效果占用了,于是你改成了 105。项目里从此多了一个"层级敏感"面板,它只有在Sorting Order恰好大于 104 小于 106 时才能正常显示。这种硬编码排序号的维护成本,会随着项目生命周期呈指数级上升。
我见过最离谱的情况是,一个项目里Sorting Order被写到了 30000 多,全是"为了避免被别的界面盖住"而不断加大的。到后面美术加一个全屏特效,程序要试五六次才能找到合适的数字。这已经不是技术债了,这是技术高利贷。
2.2 用分层 Canvas 把排序号收敛成枚举
正确的解法不是消灭Sorting Order,而是把它收敛到极少数几个取值上。做法是:在 UI 根节点下预先建好若干层级的空Canvas节点,每个 Canvas 对应一个业务层级,程序只允许通过枚举选择界面挂载到哪个层级,不允许直接改Sorting Order。
以我常用的方案为例,大概分这么几层:
| 层级枚举 | 用途 | 说明 |
|---|---|---|
Background | 主界面底图、场景 UI | 最低层,通常常驻 |
Normal | 普通功能界面 | 绝大多数面板 |
Popup | 弹窗、对话框 | 需要压住普通界面 |
Toast | 飘字、提示 | 不参与点击阻挡 |
Guide | 新手引导、遮罩 | 压住所有交互 UI |
Topmost | 调试工具、紧急提示 | 保留值班层 |
这样设计有个隐藏好处:美术出图时可以直接对应到"这是哪一层的东西",程序在处理"弹窗上面能不能看到主界面"这种问题时,不需要去查任何排序号,只需要看枚举层级。层级之间的遮挡关系变成了一种团队约定,而不是一堆神秘数字。
每个层级 Canvas 内部如果还有上下级需求,可以在该层 Canvas 内再挂一个子 Canvas,而不是全局再加一个新的大排序号。记住原则:跨业务用层级 Canvas,同业务内部用子 Canvas 顺序。千万不要在业务进行中动态改层级 Canvas 的全局序号,否则你会重新掉进数字黑洞。
2.3 动态画线、飘字这类临时元素怎么插队
层级边界里最麻烦的是临时元素。比如即时战斗里的伤害飘字、技能范围指示的动态画线(Unity UI 动态画线)、某个特效要插到弹窗和普通界面之间。这类元素生命周期短、出现位置随机、又经常需要"压住某层但别压住另一层",处理不好就会变成层级事故多发地段。
我推荐的做法是给足临时元素专门的"插队通道"。放在 Normal 和 Popup 之间的Midground层、放在 Popup 和 Guide 之间的Overlay层,都可以预先建好。出现需要插队的特效时,申请挂到对应层级的 Canvas 下,用完立刻还回去。所有临时的层级需求都走这套通道,坚决不允许现场改排序号——一旦放开这个口子,你花力气建的层级体系就白费了。
另外要注意:带GraphicRaycaster的层级会影响点击穿透。Toast 层、特效层如果没有交互需求,不要挂GraphicRaycaster,否则你会发现某个看不见的透明 UI 挡住了所有按钮点击。这也是"TextMeshPro 会被 UI 挡到"这类问题的常见源头之一——其实不是被"挡到",而是某个透明射线检测节点横在中间,拦截了事件。
3. 边界二:生命周期边界——界面打开/关闭不是一行代码的事
3.1 异步加载、UI 栈与竞态
ShowPanel看起来是个同步操作,但在真实项目里它几乎总是异步的:面板预制体可能在 AssetBundle 或 Addressables 里,需要异步加载;打开过程可能要播入场动画;数据可能要等网络请求返回。这三个异步过程组合在一起,会产生大量肉眼很难察觉的竞态问题。
最常见的一个:玩家快速连点"打开商城"按钮,第一次点击触发了异步加载,加载还没完成玩家又点了一次,于是同一块界面被实例化了两次。又比如界面已经加载完成、正在播入场动画时,玩家按了返回键关闭界面,动画还没播完加载回调又触发了第二次打开。这些时序问题在单机 Demo 里根本不会出现,一旦接入真实网络和资源管理,就会变成排查起来极其痛苦的偶现 Bug。
3.2 一个可复用的 UI 生命周期状态机
解决思路是把界面看作一个有状态的对象,而不是一个"打开/关闭"的二元存在。我给每个界面定义了一套状态:Closed、Loading、Opening、Shown、Closing。所有外部调用只能触发"请求打开"或"请求关闭",实际的状态流转由框架统一控制,并且在每个状态切换点做防重入处理。
一个精简版的状态机逻辑是这样的:
Open请求到达时,如果当前状态是Loading或Opening,直接丢弃请求(防连点)- 如果当前状态是
Closing,先把"准备重新打开"标记置位,等关闭流程走完后再自动进入Loading - 打开流程内部,资源加载 → 实例化 → 绑定数据 → 播动画 →
Shown - 关闭流程反过来:播关闭动画 → 解绑数据 → 卸载资源 →
Closed
这套状态机的价值在于,所有时序判断都集中在一个类里,而不是散落在各处 if 分支中。我在项目里还加了一个简单的请求队列:如果玩家在 0.5 秒内连续请求打开三个窗口,框架按顺序排队,而不是三个窗口同时实例化再互相覆盖。这个体验细节对用户手感影响非常大。
3.3 关闭动画没播完就被销毁的坑
另一个生命周期边界问题是关闭动画与对象销毁的次序。很多人写ClosePanel是这么干的:播一段关闭动画,然后在动画结束事件里销毁对象。这个写法本身没错,但如果你在动画播放中途强制切换场景、重开同一界面、或者直接调用了Destroy,动画结束事件可能永远等不到,界面就卡在白屏状态。
我的做法是:关闭流程以"时间轴"驱动而不是"动画事件"驱动。关闭请求到达时,记录关闭动画时长,启动一个协程或Tween,时间走完无论如何都要执行销毁与资源释放。如果动画组件已经不存在了,也要正常走完释放逻辑。销毁动作永远放在框架层,业务代码不允许直接Destroy界面根节点。
这里顺带分享一个排查心得:如果你发现某个界面关闭后,旧数据还会在新界面里闪现一下,十有八九是关闭流程没有走完整——界面没了,但持有它的引用和数据没有清干净。生命周期边界守不住,数据残留问题就会跟着冒出来,这也是通往下一条边界的入口。
4. 边界三:数据边界——视图不该自己去找数据
4.1 三种经典结构在 Unity UI 里的取舍
UI 的本质是"数据可视化"。但在很多项目里,UI 脚本和游戏数据是长在一起的:PlayerInfoPanel里直接引用着PlayerData单例,BagPanel里直接调用ItemManager.GetAllItems()。这种写法在联调初期效率很高,但项目越大越难受:你改数据层的一个字段名,要全局搜有多少 UI 用到了它;你调整数据加载时机,所有界面都可能崩一遍。
MVC、MVP、MVVM 这套东西在 Web 前端已经被聊烂了,但在 Unity UI 里落地时有个特殊问题:Unity 的组件模型天然鼓励每个 View 自己持有数据引用。要强行套 MVC 往往会把简单事情复杂化。我的经验是:不追求严格模式,只守住一条数据边界——View 不直接访问数据服务,所有数据先经过一层薄的 ViewModel 或界面状态对象。
4.2 让 ViewModel 干活,让 View 闭嘴
具体落地时是这样的:每个面板对外暴露一个BindData(SomeViewData data)方法,SomeViewData是一个纯数据类,里面只包含这个面板显示所需的字段。面板自身的脚本只负责把SomeViewData的字段填到对应的Text、Image、列表项里,不关心数据是来自服务器、本地存档还是新手引导的临时状态。
调用方(通常是控制器或者业务系统)负责拼装SomeViewData。这意味着业务逻辑中"金币增加了 50"和"界面显示金币 1050"之间,隔着数据转换这一层。以后你换货币系统、改数值精度,只需要改模拟码那一层,UI 脚本完全不受影响。
有人会问:这不会增加很多样板代码吗?确实会,但也换来一个巨大好处:界面可以被"数据驱动测试"了。你不需要进到具体玩法里,就能把一个面板用各种边界数据(空数据、异常数据、超长文本)打到界面上,检查它是否崩溃或者错位。对于 UI 这块最频繁的改动区域,这个收益远超样板代码的成本。
4.3 刷新列表时常见的全量重建陷阱
数据边界还涉及列表刷新。我看到过太多背包、商店界面,数据一变就把整个列表的Item全部Destroy再全部重新生成。界面不卡才怪。正确做法是复用列表项:数据变更时只刷新受影响的项,或者用对象池承接列表项的实例化与回收。
这里多说一句:列表项本身也遵守数据边界。它只接收单项数据对象,自己不做任何数据查询。如果你发现某个列表项脚本里出现了GameObject.Find("MainCamera"),说明数据边界已经破了,赶紧修——这种代码在列表滚动时会反复执行,性能问题和逻辑问题都会同时找上门。
5. 边界四:事件边界——事件总线不是万能胶
5.1 耦合转移不等于解耦
事件系统是 Unity 项目里另一种常见的"刚开始很爽、后期很痛苦"的设计。很多人遇到跨模块通信需求就上事件总线:打开背包要刷新任务,广播一个OnBagChanged;任务完成要刷新背包,广播一个OnTaskCompleted。事件确实让两个模块解耦了,但代价是把耦合从"类与类的直接引用"转移成了"事件名称的统一维护"。
当你项目里有两三百个事件名散落在各个脚本里时,麻烦就来了:没人知道某个事件被谁监听、在哪里抛出、参数格式是什么。你改一个事件的参数签名,编译期根本不会报错,只有运行时才会炸。这比直接调用方法更危险——直接调用至少还是编译期可见的依赖。
我的原则是:事件总线只用于"一对多、没有返回值、业务方不确定"的广播场景,比如成就解锁、全局货币变化、红点更新。如果是一对一的调用、或者调用方明确知道要通知谁,就直接引用接口调用,不要绕事件。省得日后读代码时到处 Search。
5.2 注册与反注册必须成对出现
事件边界里最隐蔽的问题不是命名,而是泄漏。静态事件或单例事件总线会持有监听者的强引用,如果界面在关闭时没有反注册,那么这个界面对象就永远不会被 GC 回收。你以为界面已经关掉了,其实它和它引用的所有子物体、贴图都还活在内存里。
这也是我前面强调生命周期边界的原因:最稳妥的做法是把事件的注册放在OnEnable、反注册放在OnDisable,而不是放在"打开/关闭"这种业务生命周期里。因为OnDisable无论如何都会被 Unity 调用,哪怕界面是被场景切换强制卸载的。另外,事件回调里不要引用界面自身的成员方法,除非你确定反注册一定会在界面销毁前执行。
5.3 事件名与消息体收敛:救一救混乱的消息
事件命名也需要边界。我见过项目把事件名当成公共备注写:ON_CLICK_BTN_OPEN_PANEL_AND_REFRESH_TASK_AND_PLAY_SOUND。这种名字一旦超过 5 个,就说明业务耦合已经钻到事件系统里了。我推荐的做法是:事件名只表达"发生了什么"(PlayerCoinChanged、BagItemAdded),不表达"应该做什么"。至于收到事件后做什么,那是监听者自己的事。这样事件系统才是真正的消息层,而不是一个变相的调用链。
参数方面尽量传只读数据对象,别传可变引用。事件发出去之后,抛出方还在继续改这个对象,监听方读到的数据前后不一致,这种 Bug 极难排查。我一般会把事件参数定义成readonly或不可变类型,从结构上杜绝这类问题。
顺带说一句"TextMeshPro 会被 UI 挡到"这类现象的排查:很多人以为是事件系统的问题,实际上大部分是层级边界和射线检测边界的问题。如果你发现 TMP 文本显示正常但点击没反应,先看它所在 Canvas 的Sorting Order,再看路径上是否有不可见的Graphic拦截了射线,最后才怀疑事件总线。排查顺序反了,你会浪费很多时间。
6. 边界五:资源边界——界面关了,资源不一定会走
6.1 引用残留与泄漏的常见路径
资源泄漏是 UI 框架最容易踩的隐性深坑。界面关闭、场景切换后,你以为资源已经释放了,但 Unity Profiler 里内存曲线却一路上涨。最常见的原因有几个:
- 静态引用:某个静态类里存了界面的
GameObject或Transform,导致整个界面树都无法卸载 - 事件残留:前面说的事件未反注册,单例事件总线持有界面引用
- 委托残留:界面上某个按钮的
onClick.AddListener挂了另一个界面对象的方法,而那个界面已经销毁了 - 资源引用未清:界面代码里用公共字段拖拽了图集、字体等资源,界面销毁后这些资源引用还在别处残留
解决思路是给每个界面资源建立"接触面"协议:界面从打开到关闭的过程中,谁负责加载资源、谁负责释放资源、释放的时机是什么,都要提前约定好,不能靠"万一有人记得释放"。
6.2 Addressables + UI 框架的释放约定
如果你在用 Addressables,资源边界要设计得更细。我的约定是:每个界面打开时LoadAssetAsync拿到预制体,实例化后进入状态机;关闭流程走完时先ReleaseInstance,再Release预制体资源。加载和释放成对出现,并且在同一个生命周期状态机里完成,不允许业务代码自己异步加载 UI 预制体。
这里有三个容易出问题的细节。第一,Addressables.InstantiateAsync返回的AsyncOperationHandle要妥善保存,释放时要用同一个 handle 去释放,不能凭空Release。第二,界面上的子资源(图集、字体、Sprite)如果是在预制体里引用的,预制体释放时它们会跟着释放,但如果是运行时动态加载的,你需要单独跟踪并释放。第三,场景切换时可能有多个界面处于打开状态,框架要在场景卸载前统一走一遍关闭流程,而不是直接让场景卸载把界面带走——那样引用计数会乱。
6.3 图集与 TextMeshPro 动态字体:资源边界上的两个顽固分子
图集(SpriteAtlas)是 UI 资源管理里最顽固的。因为多个界面可能共用图集,你不能在一个界面关闭时就释放图集,否则别的界面会出现紫图。正确做法是给图集建立引用计数:界面加载时引用数 +1,界面关闭时引用数 -1,减到 0 才真正卸载。这个计数逻辑应该收敛在 UI 框架的资源管理模块里。
TextMeshPro的动态字体资源也是泄漏高发区。TMP 的TMP_Settings默认会动态生成字体图集,如果界面里频繁使用各种字号、字体样式,动态字体图集会不断膨胀且难以回收。我的经验是:常用文本字号和字体样式尽量收敛成少数几个预设,让 TMP 的 dynamic atlas 能复用;不同语言(比如中日韩)的字形集要单独管理,避免切换语言时动态字体暴涨。资源边界里最容易出问题的往往不是大资源,而是这些看起来不起眼的字体图集。
7. 边界六:复用边界——通用控件的诱惑与代价
7.1 Prefab 变体到底该怎么用
UI 开发里有个经典争论:什么时候该用Prefab,什么时候该用Prefab Variant,什么时候直接复制一份改?我的看法是:Prefab Variant适合"结构相同、视觉不同"的场景,比如不同品质的道具图标框、不同主题的按钮。它的好处是父级预制体改了结构,所有变体都会跟着变,维护成本低。
但变体也有边界:如果变体里出现了"把父级的一个节点删掉再重新放一个"这种操作,它在结构上已经跟父级分道扬镳了,你再用变体反而会让两边的差异越来越难理解。遇到这种情况,我的建议是拆成独立的 Prefab,不要硬套变体关系。判断标准很简单:你的变体是否只改动"参数值"而不改动"结构拓扑"?是就用变体,不是就独立。
7.2 通用控件库的建设原则:"等到第三个使用者再抽象"
另一个复用边界问题是通用控件库。项目里总有人想封装一个"万能列表""万能弹窗""万能 Tab",封装完之后所有人都得去学它的配置系统,遇到需求不满足还得给控件库打补丁,最后万能的控件变成了"万万不能动"的控件。
我的原则是:一个控件在被第三个地方使用之前,不要抽象成公共库。第一个使用者直接写页面代码,第二个使用者在复制的过程中自然会发现哪些部分是一致的,等到第三个使用者出现,你已经有足够样本去设计公共 API 了。过早抽象是 UI 框架腐化的重要来源,抽象过晚顶多是多复制几次,成本是可控的。
顺带说下,复用边界和性能边界经常打架。比如你封装了一个通用列表控件,为了复用性它支持多选、拖拽、排序所有功能,但某个界面只需要静态展示。这时候性能就吃亏了——大量用不到的监听器、布局计算都在空转。我给通用控件的建议是:核心功能做成可裁剪的模块,界面上不启用的功能要能通过开关关掉,而不是单纯"不管它"。
8. 边界七:性能边界——合批、重建与卡顿的真相
8.1 Canvas 重建的代价与动静分离
UI 性能优化里最重要也最容易被误解的概念是Canvas重建(Rebuild)。很多人以为 UI 卡顿是 DrawCall 太多,其实在 Unity 的 UI 系统里,Canvas.BuildBatch和Canvas.SendWillRenderCanvases的耗时往往更致命。一个 Canvas 里只要有一个元素发生变化(比如进度条数值、滚动列表位置),它所在的整个 Canvas 都可能触发重建和重新合批。
所以性能边界的第一条就是动静分离:把频繁变化的元素和静止不动的元素拆到不同 Canvas 下。比如主界面底图一层 Canvas、刷新的红点和飘字一层 Canvas、滚动列表单独一层 Canvas。这样某一部分变化时,不会拖累整个界面的合批缓存。
但你得控制 Canvas 数量。Canvas 并不是越多越好,每个额外 Canvas 都会打断前一个 Canvas 的合批,增加 DrawCall。动静分离的粒度需要配合 Profiler 实测:打开 Frame Debugger 看每一帧的 Batches 数量和 Canvas 重建耗时,找到一个合批开销与重建开销的平衡点。
8.2 动态画线与临时顶点内容的性能成本
动态画线(比如 UI 上画箭头、画技能范围、画拖拽轨迹)是 UI 性能边界的典型考验。这类效果的顶点数据每帧都在变,如果直接用UILineRenderer或者自定义Graphic每帧生成顶点,它所在 Canvas 的重建成本会直线上升。我的经验是:
- 动态画线单独放一层 Canvas,避免拖累其他静态界面
- 顶点生成频率和帧率解耦,不一定每帧都更新,可以 30Hz 或 15Hz 更新
- 线段数量要做上限保护,防止极端情况下顶点数爆炸
- 如果只是展示用、不需要射线交互,去掉它的
RaycastTarget
还有一个新手常踩的坑:任何继承Graphic的自定义 UI 组件,只要RaycastTarget是勾选状态,Unity 就会在每次射线检测时把它纳入计算。动态画线这类高频更新元素如果还开着RaycastTarget,点击检测的耗时也会被放大。没有交互需求的 UI 元素,一律关掉它。
8.3 Overdraw 与克隆体爆炸
性能边界最后说两个容易被忽略的点。一是 Overdraw:全屏半透明遮罩叠上好几层、粒子特效在 UI 上层大量绘制,这些都会让 GPU 压力变大。尤其是新手引导的镂空遮罩,遮罩算法本身有一定开销,如果再叠加多层界面,低端机上会出现肉眼可见的掉帧。
二是克隆体爆炸:对话框里的按钮、列表里的 Item,如果每次打开都重新生成且没有对象池,场景里会堆积大量残留物体。这些物体即使SetActive(false),它们的OnEnable/Update不会执行,但内存和层级遍历成本还在。对象池对于 UI 的 Item 和弹窗几乎是必需品,这是从性能角度对"复用边界"的一个重要呼应。
9. 最后聊聊落地顺序:七条边界不可能一次到位
9.1 先诊断,再约束:别在失控前重构
看到这里,你可能已经跃跃欲试想把项目里的 UI 体系推倒重来。我的劝告是:别急。七条边界的价值是给你一个完整的检查清单,而不是让你一次性全部落地。UI 框架重构的破坏力,基本等同于对游戏所有界面进行一次地毯式轰炸——如果你没有足够的测试覆盖和灰度计划,重构本身就是事故源。
正确的顺序是:先打开 Profiler 和 Frame Debugger,把项目里 UI 相关的问题量化出来。是层级乱导致的显示 Bug 多,还是打开界面卡顿明显,还是内存只增不减?诊断完哪条边界最先出血,就先补哪条。
9.2 从最疼的地方下手:小步重构的节奏
如果让我给一个保守的落地顺序,我会建议:先做生命周期边界,因为它能带来稳定性的快速提升,所有界面开关都走统一状态机,竞态问题会大幅减少。其次是层级边界,把排序号收敛成枚举,这一步对显示 Bug 的消除立竿见影。然后做资源边界,因为内存泄漏是积累性的,越晚处理越难清理。事件边界和数据边界可以在日常需求迭代中逐步渗入,每改一个界面就顺手把它的数据绑定方式规范化。最后是性能边界,性能优化必须基于数据,不要凭感觉。
每一步重构都要保证"行为不变":重构前界面长什么样、点起来什么手感,重构后必须一致。UI 重构最容易出现的错误是顺手改了一堆交互细节,然后连回归测试都不知道该以什么版本为准。
9.3 用机制守住边界:评审清单与静态检查
边界画得再好,没有约束机制也会在业务压力下慢慢失守。我在团队里推了两件小事,效果很好。一是 UI 相关的 Code Review 清单,里面固定列着层级枚举是否被绕过、关闭流程是否走框架接口、监听是否成对注册、资源 handle 是否配对释放这几项。Review 时逐项打勾,比漫无目的地看代码高效得多。
二是把"禁止GameObject.Find、禁止 UI 脚本里使用FindObjectOfType、禁止直接改SortingOrder"这类规则写进静态检查和团队规范里。Unity 项目里这些调用往往藏得很深,靠人工一个个盯是盯不过来的。
我自己的体会是,UI 框架的价值不在于代码写得多么炫技,而在于规则少而清晰,并且这些规则被整个团队理解。七条边界听起来很多,实际落到代码里就是几个枚举、一个状态机、一套加载释放协议、一个事件注册约束,外加一个对象池。真正困难的是让所有人遵守它们——哪怕是紧急改 Bug 时也不走捷径。守住这一点,你项目的 UI 模块就能平稳地活到上线,甚至活到下一个项目继续复用。