资讯详情

Unity UIManager设计实战:从散装界面到统一调度框架

📅 2026/10/13 4:36:57 | 华诺云谱 👁 阅读
Unity UIManager设计实战:从散装界面到统一调度框架
1. 从散落各处的面板到统一入口UIManager到底在管什么做Unity项目的人几乎都经历过这样一个阶段一开始只有两三个界面随手在脚本里SetActive(true/false)就能跑通谁也没觉得有问题。等到项目做到中期登录、主城、背包、商店、设置、结算、排行榜、活动弹窗……界面数量冲到三四十个问题就集中爆发了。某个面板被两个脚本同时打开关掉一个另一个也跟着消失某个弹窗叠在另一个弹窗上面点关闭把底下的也一起关了返回键按下去不知道该退到哪一层更头疼的是某个界面明明已经销毁了回调还在往里写数据直接报空引用。这些问题的根源不是代码写得不够多而是缺少一个统一的界面调度层。UIManager界面管理器要解决的核心问题就一句话把谁负责打开界面、谁负责关闭界面、界面之间怎么叠、数据怎么传这四件事从业务逻辑里抽出来集中到一个地方管。它本质上是一个中介者Mediator业务代码不直接碰界面对象只跟UIManager打交道。我见过不少团队的做法是每个界面自己管自己界面脚本里既写业务逻辑又写显示隐藏还写层级设置。这种写法在界面少的时候确实快但一旦界面数量上来维护成本是指数级上升的。举个很典型的场景策划要求打开背包时如果商店已经开着先把商店关掉。如果每个界面自己管自己你就得在背包的打开逻辑里写一句找到商店并关掉它背包脚本就依赖了商店脚本耦合就这么产生了。而有了UIManager你只需要在配置里声明背包和商店互斥调度逻辑由管理器统一处理背包脚本完全不需要知道商店的存在。这篇笔记适合两类人看。一类是正在从散装界面往框架化界面迁移的开发者你可能已经意识到需要UIManager但不确定该管到什么粒度另一类是想自己动手写一套UIManager、但被网上各种实现方案绕晕的人。我会从职责边界、数据结构、层级设计、生命周期、异步加载、踩坑经验几个角度把一套能真正落地的UIManager讲透。代码示例用C#思路对任何Unity版本都通用。需要先明确一点UIManager不是越复杂越好。我见过有人把UIManager写成了一个迷你操作系统里面塞了动画系统、事件系统、资源系统、本地化系统最后谁都不敢改。好的UIManager应该只做调度不做业务。它知道有个叫Bag的界面要打开但不知道背包里装了什么它知道这个界面属于弹窗层但不关心弹窗里显示什么内容。守住这条边界框架才能长期可维护。2. 职责边界划在哪UIManager该管与不该管的事2.1 必须由UIManager接管的三件事第一件是界面的注册与寻址。业务代码说打开背包UIManager得知道背包对应哪个Prefab、哪个脚本、放在哪个路径。这就需要一个映射关系通常用配置表或者字典来维护。我倾向于用配置表ScriptableObject或JSON因为策划改界面路径、改层级的时候不需要动代码改配置就行。字典硬编码在代码里的做法界面一多就是灾难。第二件是层级Sorting Order管理。Unity的UGUI靠Canvas的sortingOrder和Hierarchy里的顺序决定渲染先后。如果每个界面自己设sortingOrder迟早会撞车。UIManager应该维护几个固定的层级层Layer比如背景层、主界面层、弹窗层、提示层、加载层每个界面注册时声明自己属于哪一层由管理器统一分配sortingOrder。这样弹窗永远盖在主界面上这种规则就是天然成立的不需要每个界面自己去抢。第三件是生命周期回调。界面从打开到关闭中间会经历加载资源→实例化→初始化→显示→隐藏→销毁这些阶段。UIManager要在合适的时机调用界面脚本的对应方法比如OnInit、OnShow、OnHide、OnClose。业务脚本只实现这些回调不关心自己什么时候被调用。这套回调机制是解耦的关键也是后面讲异步加载时最容易出问题的地方。2.2 坚决不该塞进UIManager的东西业务数据不要放进来。我见过有人在UIManager里存当前选中的角色ID背包里的物品列表理由是多个界面都要用。这是典型的职责错位。共享数据应该放在独立的Model层或者数据管理器里UIManager只负责界面调度。一旦UIManager开始存业务数据它就从调度者变成了上帝对象后面想拆都拆不掉。动画逻辑不要放进来。界面的打开动画、关闭动画应该由界面自己或者一个专门的动画组件处理。UIManager只负责通知界面该显示了至于显示的时候是淡入还是缩放那是界面自己的事。把动画塞进UIManager会导致管理器里全是DOTween的调用跟调度逻辑混在一起非常难读。网络请求不要放进来。有些实现会在打开界面时自动发一个网络请求拉数据这看起来很方便但把网络层和UI层绑死了。正确的做法是界面自己在OnInit里发起请求UIManager不参与。下面这张表是我总结的职责划分可以直接对照检查自己的实现有没有越界职责项归属理由界面Prefab寻址UIManager统一入口便于配置化层级与sortingOrder分配UIManager避免层级冲突生命周期回调触发UIManager解耦业务与调度界面互斥/栈管理UIManager全局规则统一处理业务数据存储Model层避免上帝对象打开/关闭动画界面自身表现与调度分离网络请求发起界面/业务层避免UI与网络耦合本地化文本本地化系统独立职责2.3 一个判断标准UIManager能不能被单独测试我判断职责有没有划清有个很土但很有效的标准能不能在不启动任何具体界面的情况下单独测试UIManager。如果UIManager的代码里到处FindObjectOfType、到处依赖具体界面类那它就没法单独测。反过来如果UIManager只依赖一个抽象的界面接口比如IUIView所有具体界面都实现这个接口那你完全可以写一个假的界面对象塞进去验证打开→显示→关闭的调用顺序对不对。这个标准帮我砍掉了很多不该塞进管理器的逻辑。3. 数据结构选型字典、栈、队列各自解决什么问题3.1 用字典维护已打开界面的索引UIManager内部至少要维护一个当前已打开的界面集合。最常用的是Dictionarystring, IUIViewkey是界面名或者界面IDvalue是界面实例。为什么用字典而不是列表因为判断某个界面是否已经打开这个操作极其频繁——打开界面前要检查是否重复打开关闭界面时要检查是否存在互斥逻辑要检查对方是否开着。字典的查找是O(1)列表是O(n)界面一多差距就出来了。这里有个细节key用什么。用字符串界面名最直观但字符串容易拼错而且重构时改名很麻烦。我后来改成用枚举或者常量类编译期就能发现拼写错误。如果界面数量特别多上百个可以考虑用int类型的界面ID配合配置表性能更好但可读性差一些。中小项目用字符串常量类就够了。public static class UINames { public const string Login Login; public const string MainCity MainCity; public const string Bag Bag; public const string Shop Shop; public const string Settings Settings; }用常量类的好处是IDE能自动补全改名字用重构功能一键替换比裸字符串安全得多。这个习惯我从第二个项目开始坚持后面再也没出现过界面名拼错导致打不开的低级问题。3.2 栈结构管理返回逻辑有返回键需求的界面比如设置、背包、各种二级页面需要一个栈来记录打开顺序。用户按返回键时弹出栈顶界面露出下面的界面。这个栈和字典是配合使用的字典负责快速查找某个界面在不在栈负责记录打开顺序。但栈不是万能的。有些界面是互斥的比如主界面上的几个Tab背包、商店、任务它们不应该进栈切换Tab时直接关掉旧的、打开新的返回键不应该在Tab之间来回跳。所以我的做法是给界面加一个属性IsStackable只有标记为可入栈的界面才进栈。这个属性在配置表里声明策划能控制。还有一种情况是弹窗和页面的区别。弹窗比如确认框、奖励展示通常不进栈关掉就回到原来的界面页面比如设置、背包进栈返回键能退回去。这个区分很重要混在一起会导致返回逻辑混乱。3.3 队列处理排队打开的并发场景有个容易被忽略的场景同一帧内连续打开多个界面。比如登录成功后先弹一个每日签到弹窗签到完再弹一个公告弹窗。如果直接连续调用Open两个界面会同时实例化、同时显示叠在一起。这时候就需要一个队列把打开请求排起来前一个关闭后再处理下一个。我一开始没做队列结果活动期间经常出现两个弹窗叠一起点关闭关错了的bug。后来加了一个简单的打开队列Open请求先入队UIManager每帧检查队列如果当前没有正在处理的打开请求就取出一个执行。这个改动不大但解决了一类很烦人的问题。private QueueOpenRequest _openQueue new QueueOpenRequest(); private bool _isProcessingOpen false; public void Open(string uiName, object data null) { _openQueue.Enqueue(new OpenRequest(uiName, data)); } private void Update() { if (_isProcessingOpen || _openQueue.Count 0) return; var req _openQueue.Dequeue(); _isProcessingOpen true; StartCoroutine(DoOpen(req, () _isProcessingOpen false)); }这段代码很朴素但解决了大问题。注意_isProcessingOpen这个标志位它保证同一时间只有一个打开流程在跑。异步加载完成前队列里的其他请求会等着不会插队。3.4 三种结构的配合关系把上面三种结构串起来看字典是状态索引回答现在开着哪些界面栈是顺序记录回答返回键该退到哪队列是请求缓冲回答多个打开请求怎么排队。三者各司其职缺一不可。很多简易版UIManager只做了字典结果返回逻辑和并发打开都出问题。我建议一开始就把这三个结构都搭好后面省事。4. 层级设计为什么你的弹窗总被别的界面盖住4.1 UGUI的渲染顺序到底由什么决定要讲层级得先把UGUI的渲染规则说清楚。一个Canvas下的元素渲染顺序由两个因素决定Canvas的sortingOrder和Hierarchy中的兄弟顺序。sortingOrder大的盖住小的sortingOrder相同时Hierarchy里靠下的盖住靠上的。很多人只记得sortingOrder忽略了Hierarchy顺序结果出现明明sortingOrder更大却被盖住的怪事——因为两个界面在同一个Canvas下sortingOrder根本没起作用比的是Hierarchy顺序。所以层级管理有两种思路。一种是每个界面一个独立Canvas通过设置各自的sortingOrder来控制层级。这种方案层级清晰但Canvas数量多了会有性能开销每个Canvas都是独立的合批单元会打断合批。另一种是所有界面共用一个Canvas通过调整Hierarchy顺序来控制层级。这种方案合批友好但调整顺序需要调用Transform.SetSiblingIndex而且顺序管理容易乱。我的选择是分层Canvas把界面按层级分成几个固定的Canvas背景层、主界面层、弹窗层、提示层、加载层每个层一个Canvas层内界面共用这个Canvas通过Hierarchy顺序控制层内先后。这样Canvas数量控制在5个左右合批影响可接受层级关系也清晰。层与层之间的顺序由Canvas的sortingOrder固定层内顺序由SetSiblingIndex动态调整。4.2 五个固定层级的划分与用途我常用的层级划分是这样的层级名sortingOrder用途典型界面Background0背景、场景底图主城背景、战斗背景Main100主界面、常驻界面主城HUD、战斗HUDWindow200全屏页面背包、商店、设置Popup300弹窗、对话框确认框、奖励展示Toast400飘字、提示获得物品提示、错误提示Loading500加载遮罩切场景加载页这个划分覆盖了绝大多数项目需求。注意Loading层放在最上面因为加载遮罩必须盖住一切。Toast层在Popup之上因为提示信息要盖住弹窗。如果项目有特殊需求比如新手引导的高亮层可以在Toast和Loading之间再插一层。层级的sortingOrder我习惯留出间隔100、200、300方便中间插入新层。如果设成1、2、3后面想插一层就得改所有值很麻烦。4.3 层内顺序的动态调整同一个层内后打开的界面应该盖住先打开的。实现方式是在界面实例化后调用transform.SetAsLastSibling()把它移到同层Canvas的最后一个子节点。这样新界面自然就在最上面。但有个坑如果界面A打开着界面B打开盖住A然后A因为某个逻辑需要置顶比如A是一个重要的提示你得把A再SetAsLastSibling一次。这个操作要谨慎因为会打乱用户的心理预期。我的经验是层内顺序尽量只由打开顺序决定不要频繁手动置顶否则用户会觉得界面在乱跳。还有一个细节关闭界面时如果直接Destroy同层其他界面的Hierarchy顺序不变这没问题。但如果用对象池复用界面关闭时只是SetActive(false)再次打开时如果不重新SetAsLastSibling它可能还停留在原来的位置被新界面盖住。所以对象池复用的界面每次打开都要重新设置兄弟顺序。4.4 一个真实的层级踩坑案例早期项目里我把所有界面放在一个Canvas下靠sortingOrder控制层级。结果发现弹窗有时候盖不住主界面。排查了半天发现是因为主界面和弹窗在同一个Canvas下sortingOrder是Canvas级别的属性同一个Canvas下的所有元素共享同一个sortingOrder根本没法用sortingOrder区分。这就是没搞清楚UGUI渲染规则的典型后果。后来改成每个界面独立Canvas问题解决了但DrawCall上去了。再后来改成现在的分层Canvas方案既解决了层级问题又控制了性能。这个演进过程让我明白层级设计不是拍脑袋定的得先搞清楚渲染规则再根据项目规模选方案。小项目一个Canvas加SetSiblingIndex就够了大项目才需要分层Canvas。5. 生命周期与异步加载界面打开那一瞬间发生了什么5.1 完整的生命周期链路一个界面从被请求打开到完全显示中间经历了一系列步骤。把这些步骤定义清楚是UIManager稳定运行的基础。我定义的生命周期是这样的请求打开业务代码调用UIManager.Open(Bag)。查重检查Bag是否已经打开。如果已打开根据配置决定是忽略还是置顶还是重新加载。入队把打开请求放入队列等待处理。加载资源从Addressables或Resources加载Prefab。这一步是异步的可能耗时几帧。实例化把Prefab实例化到对应的层级Canvas下。绑定脚本获取界面上的IUIView组件。调用OnInit传入打开时携带的数据界面做初始化。设置层级顺序SetAsLastSibling。调用OnShow界面执行显示动画开始接收输入。出队标记本次打开完成处理下一个请求。关闭流程类似OnHide→播放关闭动画→OnClose→回收或销毁→从字典移除→如果是栈顶则处理栈。这套流程看起来繁琐但每一步都有存在的理由。比如OnInit和OnShow分开是因为初始化比如绑定按钮事件、拉取数据应该在界面显示之前完成而显示动画应该在初始化之后播放。如果混在一起会出现界面还没初始化完就开始播动画的闪烁。5.2 异步加载带来的时序问题资源异步加载是UIManager里最容易出bug的地方。核心问题是加载是异步的但用户的操作是同步的。用户在加载过程中可能又点了别的按钮或者把游戏切到后台或者网络断了。这些情况都要处理。第一个坑是重复打开。用户手快连点两下背包按钮触发了两次Open。如果没做查重会加载两个背包实例。我的做法是在请求入队时就标记Bag正在打开中后续的请求直接忽略。这个正在打开中的状态用一个HashSetstring维护加载完成后移除。第二个坑是加载过程中界面被关闭。比如加载背包的过程中用户按了返回键。这时候加载还在进行等加载完成界面已经不该显示了。处理方式是在加载完成后检查一个取消标志如果被取消了直接销毁加载出来的实例不执行后续流程。private IEnumerator DoOpen(OpenRequest req, Action onComplete) { _loadingSet.Add(req.UIName); var handle Addressables.LoadAssetAsyncGameObject(GetPath(req.UIName)); yield return handle; // 加载期间可能被取消 if (req.IsCancelled) { Addressables.Release(handle); _loadingSet.Remove(req.UIName); onComplete?.Invoke(); yield break; } var go Instantiate(handle.Result, GetLayerParent(req.UIName)); var view go.GetComponentIUIView(); view.OnInit(req.Data); go.transform.SetAsLastSibling(); view.OnShow(); _openDict[req.UIName] view; _loadingSet.Remove(req.UIName); onComplete?.Invoke(); }这段代码里IsCancelled标志和_loadingSet是两个关键。前者处理加载中被取消后者处理加载中重复请求。这两个机制加上去之后异步加载相关的bug基本绝迹了。5.3 对象池什么时候该复用什么时候该销毁界面关闭后是销毁还是缓存起来复用这是个需要权衡的问题。销毁的好处是内存干净坏处是下次打开要重新加载、重新实例化有卡顿。缓存的好处是打开快坏处是内存占用高而且缓存的对象可能持有过期数据。我的策略是按界面类型区分。高频打开的界面比如背包、商店、主界面用对象池缓存低频界面比如设置、关于、一次性活动页直接销毁。这个策略在配置表里声明每个界面一个CachePolicy字段。对象池复用的界面关闭时不能只SetActive(false)还要调用OnHide清理状态。因为下次打开时界面上的文本、列表可能还是上次的数据如果不清理会显示错误内容。我踩过这个坑背包关闭后再打开里面还是上次的物品列表因为列表没清空新数据追加在后面了。后来规定所有可缓存的界面OnHide里必须重置所有动态内容。还有一个内存泄漏的隐患界面脚本里如果订阅了全局事件比如EventManager.OnItemChanged Refresh关闭时必须取消订阅。如果界面被缓存复用订阅还在会导致重复订阅如果界面被销毁订阅没取消会导致空引用。这个在OnHide或OnClose里统一处理我习惯在OnClose里做因为OnHide可能只是临时隐藏。5.4 生命周期回调的调用时机表为了不搞混各个回调的时机我整理了一张表实现时对照着写回调调用时机典型用途注意事项OnInit实例化后、显示前绑定事件、初始化数据只调用一次OnShow初始化后、显示时播放动画、刷新显示每次打开都调用OnHide隐藏时、关闭前停止动画、保存状态每次关闭都调用OnClose销毁前取消订阅、释放资源只调用一次OnRefresh数据变化时局部刷新可选按需调用这张表贴在我的工位上很久每次写新界面都看一眼避免把逻辑放错回调。比如取消事件订阅必须放OnClose放OnHide的话界面被缓存后再次显示订阅就没了收不到事件。6. 踩坑实录那些让我加班到深夜的UIManager问题6.1 界面打开顺序错乱队列没做导致的并发问题前面提过队列这里展开讲一个具体的排查过程。项目上线前测试反馈有时候打开背包商店也跟着弹出来。这个bug复现率不高大概十次里有一次。我一开始怀疑是按钮绑定错了查了半天没发现问题。后来在Open方法里加日志发现同一帧内确实有两次Open调用一次是背包一次是商店。原因是某个活动逻辑在打开背包的同时也触发了一个检查商店红点的逻辑那个逻辑顺手把商店也打开了。两次调用在同一帧两个界面同时开始异步加载加载完成后同时实例化就叠在一起了。修复方案就是加打开队列。但加队列之后又发现新问题如果商店的打开请求排在背包后面背包关闭后商店才打开用户会觉得我明明关了背包怎么又弹出商店。所以队列还需要一个优先级或者合并机制。我的处理是同一帧内的多个打开请求如果它们属于同一层级且互斥只保留最后一个。这样背包商店的请求最终只打开商店。这个规则不一定适合所有项目但至少保证了不会叠界面。6.2 返回键失灵栈和字典不同步另一个经典bug是返回键按了没反应。排查发现是栈里记录的界面和字典里实际打开的界面对不上。原因是某个界面关闭时只从字典移除了没从栈里移除。等返回键去取栈顶时取到一个已经关闭的界面调用它的OnHide就报空引用了。这个问题的根源是关闭逻辑没有统一入口。有的地方直接调Destroy有的地方调UIManager.Close绕过了栈的维护。修复方式是规定所有界面关闭必须走UIManager.Close禁止业务代码直接操作界面对象。同时在Close方法里字典和栈的移除操作写在一起保证同步。public void Close(string uiName) { if (!_openDict.TryGetValue(uiName, out var view)) return; view.OnHide(); view.OnClose(); _openDict.Remove(uiName); _stack.Remove(uiName); // 关键栈和字典同步移除 if (view.IsCacheable) ReturnToPool(view); else Destroy(view.GameObject); }_stack.Remove这行看起来简单但漏掉它就是bug。我后来养成的习惯是任何对_openDict的修改都检查一下_stack要不要同步改。这两个结构就像一对孪生兄弟必须一起动。6.3 内存只涨不降对象池的隐性泄漏有段时间发现游戏运行久了内存一直涨切场景也不降。用Profiler抓了一下发现是界面对象池里的对象越来越多。原因是对象池只进不出关闭的界面都塞进去了但从来没有清理机制。对象池需要两个东西容量上限和定期清理。容量上限是每个界面的池子最多缓存几个实例通常1个就够因为同一界面同时只会开一个。定期清理是在切场景或者内存告警时清空所有池子。我加了一个ClearAllPools方法在场景切换时调用。还有一个更隐蔽的泄漏界面脚本里用了static变量或者单例引用导致界面对象无法被GC回收。比如某个界面在Awake里把自己注册到一个静态列表里关闭时忘了移除。这种问题Profiler不一定能直接看出来得靠代码审查。我的经验是界面脚本里尽量不用static如果非要用必须在OnClose里清理。6.4 层级错乱的排查清单层级问题排查起来很烦因为现象和原因往往对不上。我整理了一个排查清单遇到层级问题按顺序检查界面是否在正确的层级Canvas下检查父节点同层内的兄弟顺序对不对检查SetAsLastSibling有没有调用Canvas的sortingOrder有没有被意外修改检查有没有代码动态改sortingOrder有没有嵌套Canvas子Canvas的sortingOrder是叠加的容易乱有没有用transform.SetParent时传了worldPositionStaysfalse这个参数会影响位置间接影响视觉按这个清单走一遍九成的层级问题都能定位。剩下的一成通常是多个因素叠加需要逐个排除。7. 从能用 to 好用几个提升开发效率的实践7.1 配置表驱动让策划也能改界面把界面的路径、层级、是否缓存、是否入栈这些属性放到配置表里是提升效率的关键一步。我用ScriptableObject做配置每个界面一条记录策划在Inspector里就能改。改完不用重新编译运行时就生效配合热重载。配置表的结构大概是这样[CreateAssetMenu(fileName UIConfig, menuName UI/UIConfig)] public class UIConfig : ScriptableObject { [System.Serializable] public class Entry { public string Name; public string PrefabPath; public UILayer Layer; public bool IsStackable; public bool IsCacheable; public bool IsMutex; // 是否与其他界面互斥 } public ListEntry Entries; }这个配置表看起来简单但它把界面怎么打开这件事从代码里抽出来了。新加一个界面策划配一条记录程序写一个界面脚本两边并行不用互相等。这个协作模式在项目后期特别重要因为界面需求变化频繁如果每次都要改代码程序会成为瓶颈。7.2 编辑器工具一键生成界面脚本模板新加界面时重复写OnInit、OnShow这些空方法很烦。我写了一个编辑器工具右键菜单Create UI Script输入界面名自动生成一个实现了IUIView接口的脚本模板并且自动在配置表里加一条记录。这个工具省下的时间不多但省下的心智负担很大——不用记接口有哪些方法不用手动改配置表。工具的实现不复杂核心就是AssetDatabase.CreateAsset和字符串拼接。但它体现了一个思路凡是重复的、有固定模式的操作都应该工具化。UIManager相关的重复操作主要有两个新建界面脚本、新建界面Prefab。这两个我都做了工具团队里新人上手快了很多。7.3 调试面板运行时查看界面状态开发期最怕的就是不知道现在开着哪些界面。我加了一个调试面板按F1呼出实时显示当前打开的界面列表、它们的层级、栈的顺序、对象池的缓存数量。这个面板在排查界面没关掉层级不对这类问题时特别有用。调试面板的实现很简单一个OnGUI或者一个简单的UGUI面板遍历_openDict和_stack显示出来。但它带来的效率提升是巨大的。以前排查界面问题要加日志、重新运行现在看一眼面板就知道状态。这个习惯我从第三个项目开始保持每个项目都会做一个类似的调试面板。7.4 界面打开的性能优化预加载与懒加载的平衡界面打开卡顿是常见问题尤其是第一次打开某个界面时。优化手段主要有两个预加载和懒加载。预加载是在进入某个场景时提前把可能用到的界面资源加载好。比如进入主城时预加载背包、商店、设置的Prefab。这样用户点开时是秒开的。但预加载会拖慢场景加载时间而且如果预加载了用户根本不打开的界面就是浪费。懒加载是打开时才加载省内存但第一次打开慢。我的平衡策略是主界面上的一级入口界面预加载二级、三级界面懒加载。一级入口背包、商店、任务用户大概率会点预加载值得二级界面比如背包里的某个子页面打开频率低懒加载就行。预加载的实现是在场景加载完成后调用UIManager.Preload(Bag, Shop, Settings)内部就是异步加载Prefab但不实例化把handle缓存起来。真正打开时直接用缓存的handle实例化省去加载时间。8. 写在最后一些个人体会UIManager这个东西说复杂可以很复杂说简单也可以很简单。我见过用几百行代码就搞定核心功能的实现也见过几千行的全能管理器。我的体会是框架的复杂度应该匹配项目的复杂度。小项目界面少于20个用一个简单的字典加层级管理就够了没必要上队列、对象池、配置表这一整套。大项目界面超过50个才需要完整的方案。另一个体会是UIManager的很多问题根源不在UIManager本身而在团队没有统一的界面开发规范。比如有人直接Destroy界面有人走UIManager.Close有人在OnHide里取消订阅有人在OnClose里取消。规范不统一再好的框架也会被用出bug。所以我现在带项目第一件事不是写框架而是定规范把界面必须实现IUIView关闭必须走UIManager订阅必须在OnClose取消这几条写进代码审查清单。最后分享一个小技巧UIManager的日志要分级。开发期打开详细日志能看到每次打开关闭的完整链路发布期关掉详细日志只保留错误日志。这个用条件编译或者日志级别控制都行。我吃过日志太多的亏——线上出问题时日志文件几百兆根本没法看。分级之后排查效率高了很多。这套UIManager方案我在几个项目里迭代过从最初的几十行到现在相对完整的版本中间踩的坑基本都写在这篇笔记里了。如果你正在搭自己的UIManager建议先从最小的字典加层级开始跑通了再逐步加队列、对象池、配置表。不要一上来就追求大而全那样很容易在细节里迷失最后框架没搭好项目进度也耽误了。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑