恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
UE实战与高级主题:模块、反射、GC、网络同步全解析
首页
资讯中心
/
UE实战与高级主题:模块、反射、GC、网络同步全解析
UE实战与高级主题:模块、反射、GC、网络同步全解析
发布时间:2026/10/7 12:44:53
这个系列写到第五篇了。前四篇我聊了通用游戏引擎架构、资源管理、渲染流程和内存模型不少读者反馈说内容偏学院派概念多、落地少。这期我换个打法直接把镜头怼到UE的工程实战上。游戏引擎架构这件事如果只停留在分层图和高层概念上其实解决不了任何实际问题——真正决定一个项目能不能撑到上线、上线之后好不好维护的往往是模块边界怎么划、依赖方向怎么控、反射和GC怎么用、同步方案怎么取舍这些刀刃上的细节。这篇对应标题里的UE实战与高级主题我准备按六个主题展开模块体系、反射与GC、游戏性框架重塑、多线程与渲染架构、网络同步、性能剖析与工具链。适合正在用UE做中型以上项目或者刚从Unity转过来、总觉得UE又重又别扭的朋友。只用蓝图的朋友也能从中找到不少为什么蓝图会卡为什么GC会闪断之类问题的根源。前四篇偏理论这篇偏刀刃我会把每个主题往实际工程里砸。1. 模块体系UE的骨骼决定项目能长多大1.1 模块不是文件夹而是编译边界我见过太多刚上手UE的团队建好C项目之后把所有源码一股脑丢进Source目录下的几个文件夹里靠文件名和命名空间去假装分层。这不是UE的架构这只是貌合神离。UE里Module是一个编制度量单位每个模块有自己的Build.cs有自己的Public/Private目录最终编译成独立的静态库或DLL。模块之间要互相访问必须通过公开头文件并且要在Build.cs里显式声明依赖关系。换句话说#include在UE里不只是把这段代码粘进来它同时是一条依赖声明的架构契约。这个设计的架构意图很清楚隔离和热重载。UE虽然是单体引擎但模块体系把每个子系统变成了一台台小主机编辑器和运行时按拓扑顺序加载它们谁依赖谁、谁不能反向依赖谁都由这套体系管着。你可以把模块想象成公司部门Public目录是前台名片任何合作方都能拿到Private目录是办公室内部外人物理上就进不去。如果哪天你发现某个.cpp直接include了别人模块Private目录里的头文件编译器会丢出一屏报错——这不是编译器在刁难你是架构在报警。1.2 Build.cs里那些平时没人讲的参数直接拿一个真实项目的GameCore模块配置说话// GameCore.Build.cs using UnrealBuildTool; public class GameCore : ModuleRules { public GameCore(ReadOnlyTargetRules Target) : base(Target) { PCHUsage PCHUsageMode.UseExplicitOrSharedPCHs; bUseUnity true; PublicDependencyModuleNames.AddRange(new string[] { Core, CoreUObject, Engine, DeveloperSettings, GameplayTags }); PrivateDependencyModuleNames.AddRange(new string[] { EnhancedInput, Niagara }); } }PCHUsageMode.UseExplicitOrSharedPCHs这个配置很多教程不会展开讲但它直接影响你后期的编译效率。UE早期默认是共享大PCH所有模块共用一个预编译头好处是新模块几乎不用自己include写起来很爽坏处是项目一到中后期任何公共头文件的改动都会触发大规模重编而且依赖关系变得完全模糊。我的建议是项目创建第一天就开Explicit模式让每个模块的头文件依赖显式化。代价是你得老老实实在每个cpp顶部写出自己需要的头文件换来的是增量构建稳定、依赖关系可审计。bUseUnity是Unity Build把多个cpp拼成一个大文件编译能显著缩短全量编译时间。但这个开关有个很阴的副作用隐藏依赖。假设A.cpp和B.cpp被拼进同一个Unity块A.cpp写了一句#include GameplayAbilityTypes.hB.cpp里的代码也跟着能用上这个头文件里的类型——但B.cpp自己并没写这句include。正常单编时B.cpp必然编译失败靠Unity把它带过去了。这个债不一定会立刻还等某次文件调整改变了Unity分块B.cpp突然开始报错你才会意识到自己在给一段根本不知道该依赖什么的代码修编译错误。所以我的习惯是定期关掉bUseUnity做一次全量单文件构建把隐藏依赖全部逼出来验证干净之后再开回去。公有依赖和私有依赖的区别也必须一开始就讲明白PublicDependencyModuleNames声明的是我的公开头文件里会用到的东西其他模块include我的头文件时链接器要能看到这些依赖PrivateDependencyModuleNames则只是我cpp实现内部的使用外部感知不到。很多人图省事全部塞进公有依赖结果就是改一个模块头文件整条依赖链上的模块都跟着重编编译时间以指数级增长。1.3 模块循环依赖一个真实的架构事故这里讲一个我踩过比较久的坑。当时项目里技能系统GameplayAbility那套机制的内部封装需要向UI推送技能冷却状态于是它引用了GameUI模块而GameUI为了显示技能图标和冷却数值又反向引用了技能模块。从功能角度看双方互相调用完全自然但UE的模块依赖是带方向的启动时按拓扑排序加载。A依赖B、B依赖A的环静态链接阶段不一定报错可运行时模块加载顺序就会出问题A的StartupModule先跑初始化到一半发现B还没加载某些StaticClass返回空指针。最后的表现是神出鬼没的崩溃崩溃栈根本不指向你的业务代码。处理循环依赖的思路和我在服务端分布式架构里处理微服务环依赖的思路完全一样把共同的依赖下沉。我们抽了一个GameplayCommon模块里面只放共享接口、枚举、结构体、常见Tag技能模块和UI模块都只依赖它彼此互不感知。UE还有另一种更轻量的解法用UInterface接口解耦。UI模块不include技能模块的头文件而是include一个只包含抽象接口的模块运行时通过CastIAbilitySource(SomeActor)去拿数据。依赖方向就从双向环变成UI → 接口 ← 技能系统架构是单向、可维护的。对一个中大型UE项目来说模块依赖图应该是分层的不是网状的。我通常会在设计文档里画一张模块依赖方向图画图工具随意关键是把规则定下来底层是引擎和Core中间层是各类功能模块顶层是UI和玩法装配模块禁止任何向上的反向依赖。这张图比一百条团队规范都管用因为它把谁可以改变谁这件事变成了编译期检查。2. 反射系统与GCUE的半托管世界2.1 UHT生成的代码才是UE能看懂C的原因很多人第一次写UPROPERTY、UFUNCTION的时候只觉得这是加了个宏。实际上UE的处理流程是这个宏被UHTUnreal Header Tool在编译前扫描生成一堆带反射信息的辅助代码比如类型描述、属性编号、序列化函数。这些代码是UE编辑器能识别你的C类、蓝图虚拟机Blueprint VM能调用你的C函数的根基。理解反射对你日常写代码至少有三个直接帮助第一编辑器属性面板上能显示哪些字段不是看权限而是看你有没有用UPROPERTY暴露它第二蓝图能调用C的哪些函数取决于UFUNCTION描述符第三存档、网络复制、GC遍历引用最终都依赖这套反射生成的元数据而不是你自己手写协议。所以我在项目规范里有一条死规定凡是需要被蓝图、存档、网络、GC任何一个系统感知的C对象成员必须用UPROPERTY显式声明。裸指针、裸结构体、裸容器在UE里是可以写但那意味着你主动从反射世界逃出去了——编辑器看不见你、存档引擎帮你存不了、GC也不认识你出问题只能自己扛。这条规定听着简单做起来需要毅力尤其是从Unity转过来的团队很容易把UE当成普通C来写。2.2 UPROPERTY和UFUNCTION描述符背后的架构意图UPROPERTY不是一个宏这么简单它是一组描述符的组合。常用的几个我给个直接的表描述符组合架构含义常见用途VisibleAnywhere BlueprintReadOnly外部可读不可改属性和所有权分离状态展示如剩余弹药EditAnywhere BlueprintReadWrite可在编辑器/蓝图里配置数值、引用、曲线transient不参与序列化生命周期不由资产管理运行时缓存、临时句柄Instanced嵌套UObject实例资产随容器序列化DataAsset里的动态配置对象SaveGame参与存档系统玩家进度我在早期项目里有过一次教训把某个运行时计算出来的临时索引写在了存档字段上结果存档文件越存越脏读档之后角色状态错乱排查了两天才定位到是transient漏标了。这类问题架构上特别好防写UPROPERTY的时候多问一句这个值要不要被编辑器保存、要不要被存档、要不要被复制描述符本身就是架构决策的一部分。再看UFUNCTION。很多人只记了BlueprintCallable但引擎里还提供了两种更重要的架构级描述符BlueprintImplementableEvent和BlueprintNativeEvent。前者表示这个函数没有C实现具体逻辑由蓝图子类实现后者表示C提供默认实现但蓝图可以选择覆盖通过同名带_Implementation的函数名。这两个东西是UE插件架构最重要的扩展点机制——你的C框架定义好调用时机和流程把决策点暴露成蓝图事件让美术和设计在不碰C的前提下扩展玩法。这本质上就是设计模式里的模板方法只不过换成了引擎原生支持的方式。2.3 UE的GC标记清扫而不是引用计数UE的垃圾回收不是引用计数而是增量标记-清扫Incremental Mark Sweep。引擎的UObject系统维护着一张引用图GC线程依托游戏线程的切片周期性地从根集合出发标记所有可达对象然后清扫不可达对象。整个过程分摊到多帧执行所以你会看到项目里偶尔出现帧率小尖刺——那就是GC的标记阶段在干活。这个机制带来一个很关键的架构推论你不需要手动delete UObject但不能不告诉GC你的引用关系。凡是用UPROPERTY声明的成员反射系统会自动把你持有对象引用这件事记录进引用边凡是裸指针、裸容器里的UObject指针GC是看不见的它可能在某个清扫周期里误判对象不可达直接回收然后你在某个深夜游戏崩溃栈却指向一个明明还在用的变量。这也是为什么我前面坚持所有成员必须UPROPERTY。如果你实在有特殊数据结构需要存UObject引用UE提供了FGCObjectScopeGuard、AddReferencedObjects这类手动追踪途径但这属于少数情况不是偷懒的借口。另外TWeakObjectPtr要记住它专门用来存放可能随时消失的引用比如指向世界中某个Actor的指针查询前先做有效性判断否则访问时触发IsValid()检查就晚了。2.4 蓝图和C怎么分工才不别扭讨论UE架构绕不开蓝图。很多人把蓝图当成更容易写的C这是认知起点就错了。蓝图的核心价值在于它是数据驱动的资产一个蓝图类是一个可被编辑器序列化、可被内容浏览器管理、可被打包工具处理的资产它还天然支持热加载和动态创建。换句话说蓝图的定位应该是配置组装少量逻辑C的定位是稳定框架热路径算法跨模块调度。我见过比较健康的工程分工是这样的帧循环、输入映射、网络复制、核心状态机写在C数据表、数值平衡、技能编排、UI表现、关卡流程放在蓝图和DataAsset里。需要跨模块调度的逻辑放在C接口层蓝图只面向具体功能实现不感知模块依赖。这样设计下绝大多数日常修改改数值、调表现、改流程不需要重新编译C美术和策划能自行迭代而架构稳定性由C层保证。反过来要避免的做法是把大型状态机整个铺在蓝图里几千个节点连成蜘蛛网。蓝图的执行效率在复杂逻辑下明显低于C而且几乎没有重构工具支持改起来牵一发动全身。我在排查项目卡顿的时候经常看到某个蓝图节点的单帧耗时几百微秒——它不是单一瓶颈但它所在的巨型状态机让整个Profiling结果都无法直视。3. 游戏性框架的重塑GameMode不是万能胶3.1 五件套的职责边界是UE给架构师的礼物UE自带了一套游戏性框架五件套GameMode、GameState、PlayerController、PlayerState、Pawn。很多团队把这套东西当成UE给的默认模板随手用一个GameMode塞下所有规则。用当然能用但你没有真正吃到这套框架的架构红利。我把职责边界用一张表压实一下类谁拥有复制范围典型职责GameMode仅服务器不复制对局规则、生成逻辑、胜负判定GameState服务器全局复制对局进行中的共享状态比分、阶段PlayerState服务器全房间可见单个玩家的持久状态得分、队伍PlayerController服务器玩家仅对属主输入、视角控制、UI交互入口Pawn服务器玩家视情况物理实体、能力载体这套框架真正的架构意图是把你对规则和表现的注意力分离GameMode只管对局规则不关心某个角色怎么挥刀Pawn管能力执行不关心比分板怎么显示PlayerState负责跨网络传递玩家身份和进度。如果这些逻辑混在一个类里网络同步、状态回放、服务器和客户端逻辑拆分都会变得极其痛苦。我在架构评审时最常问的一句话是这段逻辑如果两个人同时触发了应该由谁来决定结果这个问题的答案决定你应该把它放在GameMode服务器权威还是放在客户端表现层。不会问这个问题的团队最后写出来的多人项目几乎都是谁快谁说了算的混沌状态同步bug修到怀疑人生。3.2 实战分层一个可维护的玩法模块组织方式基于前面说的五件套我通常会在项目里再补一层自己的模块结构。用目录说话Source/ GameCore/ # 纯逻辑技能、状态、数据模型不依赖UI Gameplay/ # 玩法框架五件套的扩展、AI、交互规则 GameUI/ # 所有界面和HUD只依赖GameCore暴露的接口 GameNet/ # 网络同步、房间逻辑、匹配对接 GameFX/ # 特效、音效等表现层装配 GameTools/ # 编辑器工具、自动化测试、数据验证这套分层的核心约束是GameUI可以依赖GameCore获取数据但绝不能反向include Gameplay里的Actor实现GameNet只知道GameCore的同步契约不知道UI怎么表现。依赖方向永远从上层指向底层。有人会问UI怎么知道技能冷却呢答案是技能系统在GameCore里发布事件用一个统一的委托总线或者GameplayTags标记状态变化UI订阅事件、查询数据。双方通过数据约定通信不通过类类型硬编码。事件总线这层很容易写过火变成到处发事件、到处订阅、根本不知道谁在听。所以我建议事件定义放在GameCore发布和订阅都靠近数据源UI层的订阅集中在UI管理类里统一注册和注销别让每个Widget自己满天飞地订阅。这样虽然代码量多了一点但是GC引用、生命周期、无效引用这一类问题会少一个数量级。3.3 三个架构反例看看你踩过几个先说巨型PlayerController。很多人会把输入、UI打开、交互检测、背包逻辑一股脑塞进PlayerController几千行起步。后果是任何一处改动都可能触发整个类的崩溃而且PC在网络环境下既跑服务器又跑客户端混在一起让同步判断完全不可读。改进方式是只保留输入映射和短流程调度其余逻辑下沉到具体模块。第二个反例是所有交互都RPC。客户端点了按钮一个Server RPC过去服务器执行完再一堆Client RPC广播回来。听起来很权威但会导致服务器CPU和网络带宽双双爆炸而且大量RPC本身有延迟和丢包重发风险。正确的架构是区分权威动作和状态呈现只有真正需要服务器裁决的才走RPC其余表现层动作本地直接做状态通过属性复制去对齐。第三个反例是核心循环写在蓝图里。帧循环是UE引擎的最高频路径蓝图在这里的字节码解释执行开销会被放得很大。我见过一个拆炸弹的小游戏炸弹倒计时和玩家交互判定全在蓝图的Tick里跑单个实体还好四个玩家同屏就开始掉帧。改成C核心循环、蓝图只做配置UI表现之后帧开销直接降了六十多倍。这个对比不是贬低蓝图而是告诉你热路径和数据密集型逻辑要压到引擎擅长的地方去。4. 多线程与渲染架构摸清游戏线程和渲染线程的脾气4.1 UE的线程模型不是随便开线程那么回事UE在运行时主要跑着游戏线程GameThread、渲染线程RenderThread、RHI线程以及一群工作线程Worker Threads。游戏线程负责逻辑和场景更新渲染线程负责生成渲染命令并提交给RHI这两个线程之间通过命令队列异步协作。引擎设计者把渲染命令排成FRHICommandList游戏线程往里塞命令渲染线程按顺序消费通过帧间同步保证不出现数据竞争。理解这套模型对写架构的意义在于你开一个裸std::thread很容易但它和引擎的每帧边界、内存分配器、GC线程之间没有任何协作约定几乎必然引入难以追踪的竞态。引擎提供的AsyncTask、ParallelFor、FGraphEventRef才是和架构对齐的并发工具它们挂在TaskGraph系统上能感知帧节奏和依赖关系。4.2 ENQUEUE_RENDER_COMMAND跨线程调度的标准姿势当你在游戏线程想给渲染线程派一个任务标准接口是ENQUEUE_RENDER_COMMAND。它的核心约定是捕获值和指针的时机。命令里的lambda会被延迟到渲染线程执行所以如果你捕获了某个引用类型成员、或者裸指针指望游戏线程此刻的数据在几帧后还能用那就危险了。正确做法是把需要的数据整份值拷贝进lambda让命令自包含。我在项目里吃过一次亏在游戏线程把某个组件的SceneComponent指针放进命令里渲染线程执行时组件已经被销毁结果直接访问野指针。后来改成在命令里传递组件的WeakPtr并在执行端做有效性检查问题立刻消失。这里要强调渲染线程执行命令时引擎不保证你引用对象的生命周期你要自己负责。如果确实需要渲染线程强制执行完所有已排队命令再继续用FRenderCommandFence做同步。但它会阻塞游戏线程属于性能破坏器我只在关游戏、切关卡、以及某些需要这一刻渲染结果绝对确定的场景用常规逻辑绝不碰它。4.3 实战中的线程安全检查与调试在UE项目里排查线程相关崩溃我的第一板斧是加断言。在关键函数入口写check(IsInGameThread())或check(IsInRenderingThread())让逻辑在第一时间暴露跑错线程了而不是等到变量被两个线程同时改烂之后才崩溃。第二板斧是规范共享状态跨线程读写的变量要么走引擎提供的TAtomic/锁要么用命令队列传递坚决不做我在游戏线程写、你在渲染线程读这种裸数据交换。调试线程问题时我一般会同时开着UnrealInsights的线程视图和定帧分析。线程视图能直观看到游戏线程和渲染线程的重叠情况如果渲染线程每帧等游戏线程很久说明逻辑太胖如果游戏线程每帧等渲染线程说明渲染命令提交太密或者GPU太满。这两类瓶颈在架构上的解法完全不同前者要把逻辑搬走或分帧后者要砍绘制命令、降分辨率或优化材质。5. 网络同步架构Replication的取舍与带宽管理5.1 属性复制是状态同步不是事件同步UE的Actor网络复制机制核心是属性复制Property Replication服务器定期比较Actor上被标记为Replicated的属性的新旧值把变化的部分打包发给客户端客户端通过RepNotify回调去响应。这个机制的设计前提是网络是不可靠的新加入的客户端永远需要获得完整状态而不是事件流。这解释了很多人在多人项目里写的第一个同步bug他们试图把事件当作同步单位比如客户端发一个RPC说开了一枪希望所有人都看到开枪动画。但客户端后加入时它不知道之前开过几枪也不知道当前子弹余量——这些必须靠状态同步。正确架构是状态子弹数、血量、位置走属性复制事件音效、特效、命中瞬间的冲击表现走RPC或者本地预测。5.2 RPC三种形态的语义边界UE的RPC有三种Server、Client、Multicast每个都可以选择可靠Reliable或不可靠Unreliable。语义上Server RPC是客户端向服务器提交请求Client RPC是服务器只通知特定某个客户端Multicast是服务器广播给所有端。这个语义边界必须在架构文档里写死否则代码库会变成一团乱麻。可靠RPC要特别小心引擎会重发直到确认到达如果你在可靠RPC里塞了增加金币100这种非幂等操作客户端网络闪断重连后可能收到两次金币就凭空翻倍。所有可靠RPC的处理函数我都要求实现方写幂等性检查——要么在服务端记录处理过的请求ID要么让操作本身满足重复执行结果相同。带宽管理则是另一个大课题。一个直观的数字位置同步频率设在15-20Hz玩家体验还在可接受范围血量、Buff这类低频状态放属性复制2-5Hz够了真正需要瞬间感知的比如开火、被击中、丢手雷才走不可靠RPC。我在项目里会建一张同步频率表每个可复制字段都标出频率和优先级用引擎的NetPriority和NetUpdateFrequency配合调优而不是所有Actor用默认参数堆上去。5.3 一个实际同步方案的设计案例用一个FPS项目举例。玩家的基础移动服务器跑16Hz的移动复制客户端做本地的运动预测和插值只在关键碰撞事件时强制对齐一次。武器弹道服务器做权威命中检测客户端做表现层的命中反馈和受击特效命中判定结果通过不可靠Multicast广播给附近玩家。血量、护甲、子弹余量这类资源数值用属性复制RepNotify服务器变更后客户端自动更新HUD。这个方案最关键的设计决策是谁拥有权威。移动和命中的权威在服务器因为要防作弊但客户端不能每帧都等服务器回包才动否则输入延迟会让人感觉像在划船。所以客户端预测必须存在服务器则定期把权威位置发回来客户端把偏差平滑修正。UE的框架天然支持这套东西但你要主动规划而不是让引擎默认的同步参数替你决定体验。6. 高级主题性能剖析与团队工具链6.1 Stat命令和UnrealInsights先复现再优化说到UE实战性能剖析是绕不开的高级主题。我认为基本原则是先能复现再谈优化。用stat unit看整体帧时间分布用stat game看游戏线程用stat rhi看渲染线程用ProfileGPU或者UnrealInsights抓一帧的详细GPU耗时。如果某个模块的耗时稳定出现在同一个区域才值得动手。UnrealInsights是UE架构里我认为价值最高的工具之一。它的帧视图能同时展示游戏线程、渲染线程、RHI线程的时间轴还能追踪GC、加载、网络各环节。我通常这样用跑一段代表性游玩录像导出Insights数据重点看各线程之间的等待关系。很多感觉卡的问题最后定位出来不是某个函数慢而是两个线程在边界上互相等待架构问题比单点性能问题多得多。6.2 自建性能计数不靠猜靠埋点光靠引擎自带工具还不够。我强烈建议项目从第一天就建立自己的性能埋点体系。UE提供了SCOPE_CYCLE_COUNTER宏和CSV_PROFILER支持你可以在关键逻辑段插入计数器然后把它们聚合到自动化性能测试里。埋点的位置有讲究不要埋太细否则数据噪声大也不要只埋高层否则定位不到根因。我通常会在架构分层边界上埋点——例如技能系统总耗时网络复制包大小UI每帧耗时这几个维度这样每次性能报告出来你可以快速看到是哪个层的预算超了。这里分享一个实际心得性能问题的修复往往不在你埋点的那一层。比如我埋了技能系统总耗时发现超标进一步用stat和代码走查才定位到是某个技能每帧在蓝图上查询大量Actor。埋点负责缩小范围profiling负责精确定位两个工具要配合着用。6.3 从架构层做不写代码的优化最后聊一个容易被忽略但收益巨大的角度很多优化根本不在于你写了什么代码而在于你架构上减少了多少浪费。举例来说关卡里塞了几千个独立Actor每个都在做Tick哪怕每个Tick只花0.1毫秒总量也是灾难——此时最优解不是优化某个Actor的Tick函数而是把静态元素合并成InstancedStaticMesh把不需要每帧更新的Actor关掉Tick同理蓝图节点网络在数据量大的时候会拖慢加载和运行改成C聚合数据后大量运行时开销直接消失。我负责过的项目里有一次帧率问题特别顽固玩家进入主城后掉帧严重。逐一排查发现城市装饰用了几千个独立静态网格Actor每帧都在渲染裁剪和更新变换。解决方式是合并网格、启用实例化绘制、关掉不必要的碰撞响应改完帧率直接翻倍一行功能代码都没写全是架构层的取舍。这类优化对团队的意义是你的架构决策决定了系统上限在哪。工具链上我也会建议团队把日志、崩溃报告和自动化测试纳入架构考量。UE的CrashReportClient能自动收集崩溃日志配合你自己埋的UE_LOG上下文线上问题的定位速度会极大提升。日常开发里一个随手写的UE_LOG(LogGame, Warning, TEXT(...))可能就是日后你从上千份报告中捞到根因的唯一线索。回到实战本身。这个系列到这一篇算是把UE工程里最常被忽视的架构细节过了一遍。从模块划分到反射GC从五件套到多线程从网络同步到性能剖析每一块都是我在项目里真实踩过的路。UE的强大在于它什么都给你但正因为什么都有架构的主动权反而在你手里——很多项目不是被引擎局限的而是被自己模糊的边界拖垮的。如果你刚接手一个UE项目我建议第一周别写任何业务代码先把模块依赖图理清楚把UPROPERTY规范定下来把性能基线跑一遍。这三件事花的时间会在项目中期十倍奉还。