UE4垃圾回收机制深度解析:从GUObjectArray到异步Purge的性能优化实践
1. 项目概述为什么需要深入理解UE4的垃圾回收在UE4项目开发的中后期尤其是当你的游戏世界变得庞大、角色和特效满天飞的时候你可能会突然遭遇一些“幽灵”问题游戏运行一段时间后帧率莫名骤降内存占用像坐了火箭一样往上窜甚至直接崩溃闪退。很多时候这些问题的罪魁祸首并非显眼的内存泄漏而是垃圾回收机制运作不畅。对于很多开发者尤其是从Unity或其他托管语言环境转过来的朋友UE4基于C的这套GC机制像是一个黑盒——我们知道它存在知道它能自动清理不再使用的UObject但对其内部何时触发、如何工作、为何卡顿却知之甚少。我自己就曾踩过一个深坑。在一个大型开放世界项目中我们实现了动态的植被破坏系统每一片被击碎的树叶、每一块崩落的岩石都是一个独立的Actor。在激烈的战斗场景中成百上千的这类对象被创建和标记为待销毁。起初一切正常直到某次测试在连续战斗十分钟后游戏出现了长达数秒的完全卡顿Profiler显示一个叫CollectGarbage的函数占用了绝大部分时间。那一刻我才明白不理解GC就谈不上性能优化。本文将以UE4 4.26版本为蓝本彻底拆解这套垃圾回收系统。我们不会停留在“调用MarkPendingKill”这样的表面操作而是要从最核心的全局对象容器GUObjectArray开始穿过标记、清除、异步Purge的完整链条看看一个UObject是如何被判定为“垃圾”又是如何被安全、高效地清理出内存的。理解这些不仅能帮你解决令人头疼的卡顿问题更能让你在设计游戏架构时做出更明智的决策比如对象池该用在哪儿、如何避免不必要的对象引用等。2. 核心数据结构GUObjectArray的架构与职责一切的故事都始于GUObjectArray。你可以把它想象成UE4运行时所有UObject的“中央花名册”或“全局户籍管理系统”。它不是简单的TArrayUObject*而是一个为高效管理和垃圾回收量身定制的复杂数据结构。2.1 两层索引结构ObjectIndex与InternalIndex当你创建一个新的UObject时引擎并不会直接给你一个内存指针了事。它会向GUObjectArray申请一个唯一的“身份证号”这个号由两部分组成ObjectIndex和InternalIndex。// 简化概念非直接源码 int32 ObjectIndex GUObjectArray.AllocateObjectIndex(MyNewObject);ObjectIndex是给外部游戏逻辑、蓝图使用的逻辑索引。它是一个稳定的、对外暴露的标识。当你通过FindObject或序列化系统查找对象时使用的就是这个索引。它的特点是即使对象被垃圾回收这个索引值在一段时间内也可能不会被立即复用这有助于防止出现悬空指针误访问到新对象的情况。InternalIndex则是GUObjectArray内部使用的物理索引直接对应内部数组Objects的下标。这个数组里存放的不是UObject本身而是一个叫FUObjectItem的结构体。InternalIndex的分配和复用更为激进是内部高效管理的基础。这种分离的设计非常巧妙。外部逻辑通过稳定的ObjectIndex来引用对象而内部GC过程则通过高效的InternalIndex数组进行遍历和操作两者通过一个映射表进行转换兼顾了安全与性能。2.2 FUObjectItem对象的元信息容器每个在GUObjectArray中注册的对象都对应一个FUObjectItem。这个结构体是GC机制的“情报中心”包含了判定对象生死的关键状态位struct FUObjectItem { UObject* Object; // 指向实际UObject实例的指针 int32 Flags; // 状态标志位核心中的核心 int32 ClusterRootIndex; // 集群索引用于优化引用遍历 // ... 其他内部管理字段 };其中Flags字段的以下几个比特位至关重要EInternalObjectFlags::ReachableInCluster: 标记该对象是否被其所在的Cluster集群引用。集群是UE4将常一起加载/卸载的对象如一个Blueprint生成的所有组件打包的技术用于优化迭代速度。EInternalObjectFlags::GarbageCollectionKeepFlags: 这不是一个标志而是一组标志的集合用于定义在GC标记阶段哪些对象必须被保留即使没有外部引用。例如正在异步加载的对象、属于游戏根集Root Set的对象如GameInstance、World都会设置相应的KeepFlag。EInternalObjectFlags::Async: 表示该对象正在被异步加载。EInternalObjectFlags::AsyncLoading: 表示该对象是异步加载过程本身的一部分。GC标记阶段的核心任务就是遍历所有FUObjectItem根据对象间的引用关系和这些标志位来设置或清除另一个关键标志——可达性标记。一个对象如果最终没有被标记为“可达”它就会被判定为垃圾。实操心得在控制台使用obj list命令可以列出所有UObject及其内存地址。结合obj gc命令手动触发GC然后再次列表观察对象数量的变化是直观感受GC效果的好方法。但要注意在编辑器模式下很多编辑器对象也会被统计在内。2.3 根集垃圾回收的起点任何垃圾回收算法都需要一个起点即确定哪些对象是“绝对存活”的从它们开始追踪引用链。这个起点集合称为“根集”。在UE4中根集主要包括持久化根集通过AddToRoot函数显式添加到根的对象。这些对象永远不会被GC回收除非手动调用RemoveFromRoot。常用于单例管理器、游戏实例等需要全程存活的对象。MyGameManager-AddToRoot(); // 现在GC动不了它了引擎固有根集例如GWorld当前世界、GEngine等全局引擎对象。所有UObject类本身的CDO类默认对象是蓝图和类的元信息载体必须常驻内存。所有正在被异步加载的对象它们处于加载流程中不能中途被清理。GC标记阶段的第一件事就是把所有这些根集对象标记为“可达”。3. 垃圾回收的完整工作流标记、清除与删除理解了基础数据结构我们来看GC一次完整的“清扫”过程。它主要分为四个阶段标记、清除、集群化整理和异步删除。3.1 标记阶段可达性分析这是最核心、最耗时的阶段。目标是找出所有“活着的”对象。4.26版本采用了并行标记来加速这一过程。初始标记首先将所有根集对象见2.3节放入一个待处理队列并将其在FUObjectItem中标记为可达。并行引用追踪引擎会创建多个工作线程从待处理队列中并行取出对象分析它的属性、数组、容器如TArray、TMap中引用的所有其他UObject。对于每一个被引用的对象检查其FUObjectItem如果它尚未被标记为可达则将其标记并加入到待处理队列中供其他工作线程继续分析。如果它已经被标记则跳过避免重复工作。处理弱引用与屏障UE4中有TWeakObjectPtr这样的弱引用。在标记阶段弱引用不会阻止其指向的对象被回收。但GC需要知道弱引用的存在以便在对象被删除后能将其置空。此外跨线程引用等“屏障”也需要特殊处理确保线程安全。集群优化遍历对于已经打包成集群的对象组如果集群根被标记为可达则整个集群的所有对象都会被快速批量标记为可达无需逐个遍历内部引用这大大提升了大型关联对象组如一个复杂Actor及其所有组件的标记速度。这个过程会一直持续直到待处理队列为空。此时所有能从根集通过引用链访问到的对象都被标记了剩下的未标记对象就是“不可达”的垃圾。注意事项标记阶段的性能瓶颈通常在于对象的引用复杂度。一个UObject如果包含大量UPROPERTY引用了其他对象或者持有庞大的TArray遍历它的时间就会变长。在设计数据密集型对象时需要考虑将引用结构扁平化或分块。3.2 清除阶段分离垃圾对象标记完成后就进入了清除阶段。这个阶段相对较快主要工作是遍历整个GUObjectArray。将所有未被标记为可达的FUObjectItem收集起来放入一个专门的“待删除对象”列表。将这些垃圾对象的Object指针从FUObjectItem中移除置为nullptr但FUObjectItem槽位本身可能暂时保留。此时垃圾对象在逻辑上已经被“判定死亡”并从主对象数组中隔离但它们在内存中占据的空间还没有被释放。3.3 集群化整理在清除之后引擎可能会执行一次集群化整理。它会分析当前存活对象之间的引用关系将那些关系紧密、生命周期一致的对象比如一个Actor和它的所有组件、子Actor重新打包成一个新的集群。这样在下一次GC的标记阶段就可以继续利用集群的批量标记优化提升效率。这是一个“边打扫边整理”的优化步骤。3.4 删除阶段内存的最终释放这是将垃圾对象实际从内存中抹去的阶段。在4.26之前的版本这个阶段尤其是对象的析构函数执行通常在GC主线程同步进行如果待删除对象很多且析构复杂就会导致明显的游戏卡顿。4. 4.26的核心优化异步Purge机制详解“Purge”指的就是删除阶段中真正调用对象析构函数并释放内存的过程。4.26版本最大的进步就是将这个可能阻塞游戏线程的Purge过程异步化了。4.1 异步Purge的工作流程主线程准备在清除阶段完成后主线程游戏线程将“待删除对象”列表准备好。这个列表包含了所有需要被销毁的对象实例。移交异步线程主线程不会立即处理这个列表而是将其交给一个或多个专用的后台GC线程通常是任务图系统中的一个低优先级任务。并行析构与释放后台GC线程开始遍历待删除列表对每个对象执行调用其UObject的析构函数~UObject()。执行其UClass中定义的Destroy相关逻辑如果有。最终调用FMemory::Free释放该对象所占用的内存块。 这个过程是并行进行的可以充分利用多核CPU。主线程无阻塞与此同时游戏主线程、渲染线程、游戏逻辑等可以继续流畅运行不受内存释放操作的影响。玩家完全感知不到卡顿。资源清理对象内存释放后其对应的FUObjectItem槽位会被标记为空闲。GUObjectArray内部会维护一个空闲列表供后续创建新对象时复用。4.2 异步Purge带来的优势与挑战优势是显而易见的消除卡顿这是最直接的收益。即使一帧内有成千上万个对象需要销毁游戏帧率也能保持平滑。提升CPU利用率将内存释放工作分摊到多个核心和后台线程平衡了系统负载。但也引入了新的复杂性和需要注意的点延迟销毁对象从被标记为垃圾到实际内存释放存在一个时间差。在这段“窗口期”内虽然逻辑上它已死亡但它的内存内容可能还未被覆盖。绝对不能再通过任何方式访问这个对象否则会导致未定义行为通常是崩溃。析构函数中的线程安全对象的析构函数现在是在后台线程调用的。这意味着如果你的UObject子类的析构函数中包含了访问共享资源如某个全局管理器、静态变量或者执行非线程安全的操作就可能引发数据竞争和崩溃。// 不安全的析构函数示例 MyClass::~MyClass() { // 危险GMyGlobalArray可能被其他线程同时访问 GMyGlobalArray.Remove(this-SomeData); }对性能分析工具的影响由于释放是异步的在性能分析器如Unreal Insights中原来集中在CollectGarbage函数下的释放成本现在可能被分散到多个后台任务中需要你调整分析视角来定位问题。4.3 如何控制GC行为关键CVars参数即使有了异步PurgeGC的触发时机和强度依然需要精细控制。UE4提供了一系列控制台变量gc.TimeBetweenPurgingPendingKillObjects这是最重要的参数之一。它定义了两次完整的垃圾回收包括Purge之间的最小时间间隔秒。默认值在编辑器模式下可能是几秒在打包游戏中可能更长。调低这个值会让GC更频繁内存使用更紧凑但可能增加CPU开销调高则相反内存占用可能更高但GC开销更少。你需要根据游戏的内存和性能表现来权衡。gc.NumRetriesBeforeForcingGC当引擎尝试分配内存但失败时它会先尝试触发一次GC来释放内存。这个参数定义了在内存分配失败后强制进行完整GC前的重试次数。在内存紧张的游戏里适当调低此值可能有助于更快地释放内存。gc.ClusteringEnabled启用或禁用对象集群化。对于动态生成和销毁对象非常频繁的项目有时禁用集群化反而能获得更稳定的性能因为避免了集群的频繁打包/解包开销。这是一个需要实测的优化选项。gc.MaxObjectsInGame/gc.MaxObjectsInEditor当UObject总数超过此阈值时引擎会触发一次强制GC。可以作为一个安全网防止对象数量无限增长。你可以在游戏运行时通过控制台输入这些命令来动态调整观察效果然后将合适的值配置在项目的DefaultEngine.ini中。5. 开发者最佳实践与避坑指南理解了原理最终要落实到编码和设计上。以下是我从多个项目实践中总结出的经验。5.1 避免GC卡顿的编码习惯慎用AddToRoot/RemoveFromRoot这是最强的对象生命周期控制。滥用AddToRoot会导致对象永远无法被回收是内存泄漏的常见原因。确保每个AddToRoot都有配对的RemoveFromRoot。管理好UObject引用及时置空当一个对象不再需要时将其引用UPROPERTY指针或TStrongObjectPtr设为nullptr。这能帮助GC更快地识别出垃圾。注意容器清理清空TArrayUObject*或TMap中不再需要的引用。容器本身持有引用会阻止其中对象被回收。区分强引用与弱引用如果只是需要判断对象是否存在或者对象生命周期由其他系统管理使用TWeakObjectPtr。它不会阻止GC回收对象。TWeakObjectPtrAActor MyWeakActorRef; // ... 赋值 if (MyWeakActorRef.IsValid()) { // 使用前必须检查 // 对象仍然存在可以安全使用 AActor* Actor MyWeakActorRef.Get(); }对象池技术对于高频创建和销毁的对象如子弹、特效粒子、UI控件不要依赖GC。实现一个对象池在游戏初始化时创建一批对象并存入池中需要时从池中取出并激活用完后回池并重置状态。这完全避免了运行时动态内存分配和GC开销。分批销毁如果确实需要在一帧内销毁大量对象考虑将其分散到多帧中进行。例如每帧只销毁50个对象而不是5000个。这可以平滑GC压力。5.2 异步Purge下的线程安全陷阱这是4.26之后需要特别关注的新问题。确保析构函数是纯的理想情况下UObject的析构函数只负责清理该对象自己的成员变量非UObject的原始数据、TArray等。避免在析构函数中访问全局变量或单例。调用其他可能不安全的UObject的方法。执行文件I/O或网络操作。对共享资源的访问加锁如果析构函数中必须访问共享资源必须使用线程安全的机制如FCriticalSection或FScopeLock。// 改进后的线程安全析构示例 static FCriticalSection GlobalArrayCriticalSection; MyClass::~MyClass() { FScopeLock Lock(GlobalArrayCriticalSection); GMyGlobalArray.Remove(this-SomeData); // 现在安全了 }注意BeginDestroy()与FinishDestroy()UObject的销毁分为两个阶段BeginDestroy()在主线程调用适合执行与游戏线程相关的清理如解除事件绑定。FinishDestroy()在异步Purge线程调用适合执行纯内存释放。确保将代码放在正确的阶段。5.3 性能分析与调试技巧使用OBJ LIST和MEMORY命令在编辑器或开发版游戏的控制台中这些命令是了解当前对象数量和内存状况的第一手工具。依赖Unreal Insights这是最强大的性能分析工具。捕获游戏运行数据重点关注CollectGarbage事件的总时长和调用间隔。MarkObjectsAsUnreachable标记和PurgeGarbage清除/删除各个阶段的耗时。观察异步Purge任务如FAsyncPurge的分布和耗时。关注“Pending Kill”对象数量在控制台使用obj gc后观察日志中“Pending Kill”对象的数量。如果这个数字持续增长且不下降说明有对象虽然被标记为待销毁但由于异步Purge的延迟或某些引用未清除内存没有及时释放。可能需要检查是否有非UObject系统如物理引擎、音频引擎持有了对这些对象的引用。编写自动化测试对于大型项目可以编写简单的关卡测试在特定场景如大量单位死亡、场景切换前后触发GC并记录内存和帧时间建立性能基线防止后续更新引入GC性能回归。垃圾回收机制是现代游戏引擎自动内存管理的基石。在UE4中它从简单的同步清理演进到4.26的异步Purge体现了Epic对运行时体验的极致追求。作为开发者我们不应将其视为一个完全自动化的“魔法黑箱”而应深入理解其脉络。当你清楚了GUObjectArray如何记录对象、标记阶段如何追踪引用、异步Purge如何化解卡顿你就能写出对GC更友好的代码能更精准地定位内存和性能问题最终让你的游戏世界在复杂和动态中依然保持流畅与稳定。记住最有效的优化往往源于对底层机制的那份透彻理解。

相关新闻

AI创意的本质:模式重组与人类引导的技术实现

AI创意的本质:模式重组与人类引导的技术实现

在技术领域讨论 AI 的创造力时,一个常见的误解是认为 AI 能够独立产生全新的创意。实际上,当前主流 AI 系统(无论是生成式模型还是决策模型)的本质,是建立在人类预设的目标函数、训练数据和算法架构之上的指令执行程序…

2026/7/11 2:03:50阅读更多 →
蛋白质结构可视化分析:PyMOL 2.5 操作指南与 5 类作用力3D标注

蛋白质结构可视化分析:PyMOL 2.5 操作指南与 5 类作用力3D标注

蛋白质结构可视化分析:PyMOL 2.5 操作指南与 5 类作用力3D标注在结构生物学研究中,蛋白质的三维构象解析只是起点,真正的挑战在于如何从原子层面理解这些复杂分子如何通过微妙的相互作用维持其功能形态。传统文献中关于氢键、疏水作用等概念的…

2026/7/11 2:03:50阅读更多 →
Huawei VRP PPPoE 服务器与客户端双向配置:CHAP认证 + 地址池 + 3条静态路由

Huawei VRP PPPoE 服务器与客户端双向配置:CHAP认证 + 地址池 + 3条静态路由

Huawei VRP PPPoE全栈配置实战:从认证到路由的完整解决方案1. PPPoE技术架构与华为实现原理在企业级网络环境中,PPPoE(Point-to-Point Protocol over Ethernet)技术已经成为宽带接入的主流方案。华为VRP系统通过虚拟模板接口和DCC…

2026/7/11 2:03:50阅读更多 →
Allan方差 MATLAB/Python 实战:5步代码实现IMU噪声参数标定

Allan方差 MATLAB/Python 实战:5步代码实现IMU噪声参数标定

Allan方差实战指南:5步代码实现IMU噪声参数标定在惯性测量单元(IMU)的性能评估中,噪声特性分析是核心环节。Allan方差作为时域分析的重要工具,能够有效识别和量化IMU中的各类噪声源。本文将带您从零开始,通…

2026/7/11 3:18:57阅读更多 →
芒果综艺能救活一座24亿的大庸古城吗?

芒果综艺能救活一座24亿的大庸古城吗?

作者 | 吴艳蕊编辑 | Sette2026年7月10日,投资24亿、沉寂四年的大庸古城以“奇幻大庸城”的新身份重新开门迎客。比开业更早一天引发关注的,是一份看似普通的关联交易公告。大庸古城与芒果系新运营方(张芒文旅)签署演唱会合作协议…

2026/7/11 3:18:57阅读更多 →
从源码编译AssetStudio:定制Unity游戏资源逆向解密环境

从源码编译AssetStudio:定制Unity游戏资源逆向解密环境

1. 项目概述与核心价值最近在游戏开发圈和逆向分析领域,一个话题的热度持续攀升:如何从那些经过加密、打包的商业Unity3D游戏中,提取出原始的模型、贴图、音频甚至脚本资源。无论是为了学习顶尖游戏的制作工艺,分析其性能优化方案…

2026/7/11 3:18:57阅读更多 →
风箱说紫微为什么强调命盘结构?不是看一个点就下结论

风箱说紫微为什么强调命盘结构?不是看一个点就下结论

很多人看紫微斗数命盘时,最容易犯的错误,就是只看一个点。看到命宫,就问自己“我是不是这种性格”;看到夫妻宫,就问“我的婚姻是不是这样”;看到财帛宫,就问“我有没有钱”;看到流年…

2026/7/11 3:18:57阅读更多 →
洛必达法则失效场景分析:3类典型反例与震荡间断点成因

洛必达法则失效场景分析:3类典型反例与震荡间断点成因

洛必达法则失效场景分析:3类典型反例与震荡间断点成因在微积分学习中,洛必达法则无疑是解决未定式极限问题的利器。但就像任何数学工具一样,它也有其适用范围和边界条件。许多学生在初次接触这个法则时,往往只记住了"分子分母…

2026/7/11 3:18:57阅读更多 →
ESP32-S3边缘AI视觉相机实战:低成本本地实时识别

ESP32-S3边缘AI视觉相机实战:低成本本地实时识别

1. 项目概述:为什么一个“边缘AI视觉相机”值得你花30分钟搭出来?“ESP32边缘AI视觉相机”听起来像实验室里接满线缆的开发板,配着散热风扇和外挂FPGA模块——但这次不是。我上周用一块不到25元的ESP32-S3-DevKitC-1(带USB-C口、2…

2026/7/11 3:13:57阅读更多 →
从GitHub安全案例解析常见漏洞与防护实践

从GitHub安全案例解析常见漏洞与防护实践

1. 项目概述:从GitHub Trending看安全实战 最近在GitHub Trending上看到一个项目,叫 skills4/skills ,它因为一些安全漏洞案例被大家讨论。这其实是一个挺典型的场景:一个旨在展示或教授某种技能的仓库,本身却成了安…

2026/7/10 12:10:00阅读更多 →
MLT 2026启示:因果推理与概率建模驱动下一代LLM应用

MLT 2026启示:因果推理与概率建模驱动下一代LLM应用

# MLT 2026启示:因果推理与概率建模驱动下一代LLM应用## 一、背景与挑战:从“黑箱预测”到“可信推理”2026年6月,第7届机器学习与趋势国际会议(MLT 2026)将在悉尼召开。会议议程中,“因果与可解释机器学习…

2026/7/10 12:29:21阅读更多 →
通达OA SQL注入漏洞深度剖析:从手工注入到自动化利用与防御

通达OA SQL注入漏洞深度剖析:从手工注入到自动化利用与防御

1. 项目概述与漏洞背景最近在梳理一些历史OA系统的安全风险时,通达OA v11.6版本中的一个老漏洞又进入了我的视线。这个漏洞位于/general/bi_design/appcenter/report_bi.func.php文件中,是一个典型的SQL注入点。虽然这个漏洞的利用方式看起来并不复杂&am…

2026/7/10 4:59:05阅读更多 →
Premiere Pro 2025安装失败原因与AGSIS验证绕过指南

Premiere Pro 2025安装失败原因与AGSIS验证绕过指南

1. 为什么2025版PR安装比以往更“磨人”?——从弹窗警告到路径陷阱的真实处境 Premiere Pro 2025版不是简单的一次版本迭代,它是一道分水岭。我从去年底开始帮影视工作室、高校剪辑实验室和自由职业者部署2025环境,累计处理了137台设备&#…

2026/7/11 0:03:43阅读更多 →
5款实用macOS系统优化工具:让你的Mac运行更流畅更高效

5款实用macOS系统优化工具:让你的Mac运行更流畅更高效

5款实用macOS系统优化工具:让你的Mac运行更流畅更高效 【免费下载链接】open-source-mac-os-apps 🚀 Awesome list of open source applications for macOS. https://t.me/s/opensourcemacosapps 项目地址: https://gitcode.com/gh_mirrors/op/open-so…

2026/7/11 0:03:43阅读更多 →
5分钟完全掌握:ComfyUI ControlNet预处理器终极使用指南

5分钟完全掌握:ComfyUI ControlNet预处理器终极使用指南

5分钟完全掌握:ComfyUI ControlNet预处理器终极使用指南 【免费下载链接】comfyui_controlnet_aux ComfyUIs ControlNet Auxiliary Preprocessors 项目地址: https://gitcode.com/gh_mirrors/co/comfyui_controlnet_aux 想要让AI图像生成真正听从你的指挥吗&…

2026/7/11 0:03:43阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/10 13:39:09阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/10 22:20:33阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/10 17:29:22阅读更多 →