Dart - 内存管理与垃圾回收(GC)深度解析
Flutter 性能底层(一):内存世界的版图 —— Dart 分代模型
引言:透过现象看本质
作为 Flutter 开发者,我们每天都在与 Widget 打交道。无论是简单的 Text 还是复杂的 CustomPaint,我们习惯了通过不断地 new 对象来构建界面。
但在这些代码背后,你是否曾有过一丝隐忧:Dart 虚拟机(VM)到底把这些对象存在了哪里?它是怎么管理这一大堆数据的?
要回答这个问题,解决我们对“频繁创建对象”的恐惧,我们不能一上来就谈枯燥的 GC 算法。我们首先需要拿出一张地图,看懂 Dart 内存世界的版图结构。
因为 Dart 所有的内存策略,都建立在两个核心设计之上:Isolate 的独立性 与 分代假说。
一、 宏观格局:Isolate 的分户式管理
在 Ja va 或 C++ 等传统语言中,多线程通常共享同一块堆内存(Shared Heap)。这就像一群人住在一个大通铺里:
- 优点:我想给你个东西,直接放在桌子上就行。
- 缺点:打扫卫生(GC)时非常麻烦。为了防止“我刚扫完地,你又扔了纸团”,GC 往往需要暂停所有人的活动(Stop The World),给整个屋子上锁。
Dart 选择了完全不同的路线:Isolate(隔离区)。
在 Dart 中,每个线程(Isolate)都有自己独立的堆内存(Heap)。Isolate A 的内存,Isolate B 根本看不见,更摸不着。
这种“分户式管理”带来了两个巨大的优势:
- 无锁分配:因为只有我自己用这块内存,创建对象时不需要加锁,速度极快。
- 局部 GC:当主 UI 线程需要扫垃圾时,完全不影响后台做繁重计算的 Isolate。App 不会因为后台任务的 GC 而卡顿。
二、 核心理论:分代假说 (Generational Hypothesis)
搞清楚了“谁管谁家”的问题,接下来我们看单各 Isolate 内部,这块内存(Heap)是如何规划的。
Dart 的内存规划并非拍脑门决定的,而是基于计算机科学中一个著名的观察结论 —— 分代假说 (The Generational Hypothesis)。
研究人员发现,在几乎所有的面向对象程序(特别是像 Flutter 这样构建 UI 的程序)中,对象的生命周期呈现出极端的两极分化:
- 绝大多数对象是“朝生夕死”的:
比如我们在
build()方法里创建的Container,Padding,EdgeInsets,它们存在的意义就是为了渲染这一帧画面。下一帧来了,它们就没用了。这些对象占了总量的 90% 以上。 - 极少数对象是“长命百岁”的:
比如全局的
UserSession,HttpClient, 图片缓存。它们一旦创建,可能会伴随 App 运行很久。
基于这个事实,Dart 决定不再“一视同仁”地管理所有内存,而是将堆内存物理划分为两个世界:新生代 与 老年代。
三、 版图划分:两室一厅的智慧
Dart VM 将堆内存(Heap)切分成了两块截然不同的区域,每一块都有自己的“性格”和“规则”。
1. 新生代 (Young Generation / New Space)
- 定位:对象的“生产室”与“育儿所”。
- 住户:所有刚被
new出来的对象,必须先在这里出生。没有例外。 - 特点:
- 空间较小:因为大部分对象马上就会死,不需要太大空间。
- 内存连续:为了追求极致的分配速度(下一章会讲的指针碰撞)。
- 流转极快:这里的 GC 频率非常高,策略是“快进快出”。
- 双空间:方便快速复制整理,涉及Cheney 算法内容(下一章节会讲)。
2. 老年代 (Old Generation / Old Space)
- 定位:对象的“退休所”或“档案馆”。
- 住户:只有在新生代经历过“大风大浪”(GC)还没死的对象,才有资格拿到这里的“绿卡”。
- 特点:
- 空间巨大:用于存放长期数据,甚至可以动态扩容。
- 管理沉稳:这里的 GC 频率很低,因为大家都很稳定,但这并不意味着它不重要(涉及内存碎片整理)。
四、 为什么这么设计?
试想一下,如果没有分代,所有对象都在一个大池子里: 每次 GC,我们都要遍历几万个 Widget 和几个全局单例。为了清理那 90% 的垃圾 Widget,我们不得不把 10% 的长寿对象也扫描一遍。这就像为了扔掉门口的垃圾袋,却把家里的保险柜也翻了一遍,效率极低。
有了分代模型,Dart 就可以因材施教:
- 对 新生代(满地垃圾):采用一种“只捡宝贝,剩下全扔”的激进算法。
- 对 老年代(满地宝贝):采用一种“小心核对,整理归档”的保守算法。
小结
这一章我们建立了两条核心认知:
- Isolate 让我们拥有了独立的内存空间,互不干扰。
- 分代模型 将内存划分为“快”与“慢”两个区域,完美契合了 Flutter UI “高频刷新”的特性。
这就解释了那个经典问题:“为什么 Flutter 官方建议我们将大 Widget 拆分成小 Widget?”
因为拆分出来的小 Widget 都在 新生代。只要我们能证明新生代的创建和销毁是“免费”的,那么拆分 Widget 就只有好处(代码清晰、局部刷新),没有坏处(性能损耗)。
那么,新生代到底凭什么做到“创建极速”且“销毁免费”呢?
下一章,我们将拿上放大镜,深入 新生代 的内部,揭秘那个反直觉的 Sca venge 算法 与 指针碰撞 技术。
Flutter 性能底层(二):新生代的极速魔法 —— 为什么创建对象不需要勇气?
引言:被误解的 new
在上一章的地图中,我们要聚焦在左边那个繁忙的区域——新生代 (New Space)。
大家回想一下,在build()方法里,我们是不是随手写了new Container()和new Padding()呀?从直觉上来说,“申请内存”这事儿,应该挺耗费资源的。就好比去图书馆占座,你得先去找管理员,查查哪里有空位,登记名字,然后才能坐下。要是频繁这么做,那可不得累坏了嘛!
但在 Dart 的新生代里,**“占座”**这件事被简化到了极致。它不需要查找,甚至不需要管理员介入。
今天,我们将揭秘两项核心技术:让分配快如闪电的 “指针碰撞”,以及让销毁成本归零的 “Sca venge 算法”。
一、 分配的艺术:指针碰撞 (Bump Pointer)
还记得我们在第一章图中看到的新生代吗?它的内存空间不仅小,还有一个关键特性:连续。
正因为内存是连续的,Dart VM 不需要像老式内存管理那样,去维护一个复杂的“空闲列表(Free List)”来记录哪里有缝隙。
Dart 只需要维护一个简单的指针:Top Pointer(顶部指针)。这个指针永远指向当前内存使用量的末尾。
当你执行 new Widget() 时,VM 内部的对话是这样的:
- Widget: “我需要 32 字节的空间。”
- VM: “Top 指针现在的地址是 1000。1000 + 32 = 1032。没超出边界。给,你拿去用吧。”
- VM: (默默把 Top 指针移到 1032)。
结束。
没有查找,没有遍历,没有碎片整理。整个过程仅仅是几条 CPU 指令(一次加法运算)。
这就像是老式的磁带录音。 你想录一段新歌,根本不需要去整盘磁带里寻找哪里有空白片段。你只需要在当前磁头的位置,按下录音键,顺着往下录就行了。
所以,在 Dart 新生代里分配内存,在时间复杂度上几乎等同于赋值操作。 这就是你敢在 Flutter 每一帧里疯狂 new 对象的底气。
二、 布局的秘密:两个半区 (Semi-Space)
但是,“指针碰撞”有一个致命问题:只能往后录,不能回头。 如果内存填满了(Top 指针撞墙了)怎么办?这时候,GC (垃圾回收) 就必须登场了。
为了解决这个问题,Dart 采用了 Cheney 算法 的变种。它把新生代物理切分成了两个大小完全一样的半区:
- 活跃半区 (Active / To Space):当前的“工作车间”,所有新对象都在这里分配。
- 空闲半区 (Inactive / From Space):完全是空的,留作备用。
这就解释了第一章那张图中,为什么新生代里总有一半是“灰白色”的空闲状态。这是为了接下来的“乾坤大挪移”做准备。
三、 清理的艺术:Sca venge (搬家算法)
当活跃半区被填满时,GC 哨声吹响。Dart VM 开始执行 Sca venge 操作。
这里的核心逻辑极其反直觉: 传统的清洁工是“找垃圾 -> 扔掉”。 Dart 的 Sca venge 是“找活人 -> 救走”。
过程如下:
- 标记存活:GC 从根节点(栈变量)出发,瞬间找出那些还活着的对象(比如还在用的 State,还没销毁的 Widget)。
- 复制 (Copying):把这些活着的“幸存者”,从 活跃半区 复制到 空闲半区。
- 注意:复制过去的时候,它们是紧紧挨着排队的。这意味着,原来可能存在的内存碎片,在复制的一瞬间被自动整理好了。
- 无视垃圾:那些没人引用的垃圾对象呢?GC 看都不看它们一眼。
四、 最大的反直觉:O(0) 的销毁成本
现在,活对象都搬到了另一边。 原来的活跃半区里,剩下了成吨的垃圾。Dart 需要一个个去销毁它们吗?
不需要。
Dart 只需要做一个动作:身份互换 (Swap)。
- 原来的空闲半区(现在装满了整齐的活对象) 变成新的 活跃半区。
- 原来的活跃半区(满地垃圾) 变成新的 空闲半区。
变成空闲半区意味着什么?意味着 指针归零 (Reset Top Pointer)。 那块内存里的数据(垃圾)还在吗?在。 但在逻辑上,它们已经被视为“空白”了。下次分配新对象时,直接覆盖在它们尸体上。
这就是 O(0) 销毁的奥义。
我们可以得出一个震撼的结论: Dart 清理新生代的耗时,只取决于“活对象”的数量,而与“垃圾”的数量完全无关。
- 如果你产生了 10 个垃圾,GC 耗时为 T。
- 如果你产生了 10,000 个垃圾,只要活下来的还是那几个,GC 耗时依然是 T。
这就是为什么 Flutter 官方敢建议我们将大 Widget 拆分成小 Widget。因为那些中间产生的临时小 Widget,对 GC 来说,清理它们的成本是零。
小结
这一章我们见证了新生代的两大魔法:
- 进得快:指针碰撞,无需查表。
- 死得快:半区复制,无视垃圾。
Dart 的新生代就像一个高效的**“一次性用品流水线”**,完美契合了 Flutter 声明式 UI “用完即扔”的特性。
但是,如果一个对象命很大,在这一轮“大清洗”中活下来了,甚至活了好几轮,该怎么办? 它不能永远在两个半区之间搬来搬去,那样太累了。
下一章,我们将讲述这些幸存者的故事——晋升 (Promotion),以及它们在 老年代 (Old Generation) 将面临的残酷生存法则。
Flutter 性能底层(三):老年代的沉稳智慧 —— 晋升与并发的艺术
引言:幸存者的烦恼
在上一章,我们见识了新生代 Sca venge 算法的“神速”。通过把活对象在两个半区之间搬来搬去,Dart 实现了 O(0) 的垃圾清理。
不过,咱们来想象这样一种情况:
你有一个全局的 UserSession 对象,或者一张缓存的大图。它们在第1帧被创建,并且会一直存在,直到App关闭。
要是它们一直待在新生代,就会出现一个很有意思的现象:
每进行一次GC,这些长寿对象都得被“搬”一次。 它们就像钉子户一样,在两个半区之间来回折腾,不仅浪费了宝贵的CPU搬运时间,还长期霸占着新生代那本就不大的空间。
为了解决这个问题,Dart 引入了 晋升机制 (Promotion)。
一、 晋升:从幼儿园到社会
Dart 的规则很简单:“新生代是留给短命鬼的,只有经历过风浪的对象才配去老年代。”
当一次 Sca venge (新生代 GC) 发生时,Dart 会检查那些幸存的对象:
- 规则:如果一个对象在上一轮 GC 中已经活下来过(或者其生命周期显式地被标记为长),它就不再会被复制到新生代的另一个半区,而是直接被打包,晋升(Promote) 到 老年代 (Old Generation)。
一旦进入老年代,它就安全了。它不会再被频繁地搬来搬去,而是安稳地定居下来。
但是,随着越来越多的对象晋升进来,老年代终究也会满。这时候,老年代的 GC 就必须出手了。
二、 策略突变:标记-清除 (Mark-Sweep)
在老年代,Dart 放弃了新生代那种高效的“半区复制”算法。为什么?
- 空间浪费:复制算法需要把内存一分为二。老年代通常很大(几百 MB),如果为了 GC 浪费一半内存,手机内存根本不够吃。
- 复制太慢:老年代里 90% 可能都是活对象。要把几百 MB 的数据搬一遍,CPU 绝对会发烧,App 也会卡死。
所以,老年代采用了更成熟、更节省空间的 标记-清除 (Mark-Sweep) 策略。
- Mark (标记):找出所有活的对象。
- Sweep (清除):回收死掉对象的内存地址,记录在“空闲列表”里,供下次使用。
其中最核心的难点在于:如何在几百兆内存中快速找出活对象,而且不让 UI 卡顿?
三、 并发标记:三色魔法 (Tri-Color Marking)
如果 Dart 像传统 GC 那样,在扫描老年代时暂停整个 App(Stop The World),用户就会感觉到明显的 掉帧(Jank)。
为了解决这个问题,Dart 采用了源自 Dijkstra 的经典理论 —— 三色标记算法 (Tri-Color Marking)。这允许 GC 线程在后台默默工作,而 UI 线程可以同时继续运行。
Dart 将内存中的对象逻辑上分为三种颜色:
- 白色 (White):未访问。初始状态下所有对象都是白色。如果扫描结束还是白色,说明是垃圾。
- 灰色 (Grey):进行中。对象本身被访问了(活着),但它引用的子对象还没扫描完。这是扫描的“波浪前沿”。
- 黑色 (Black):已完成。对象本身和它引用的所有子对象都已扫描完毕。黑色对象是绝对安全的。
魔法过程(边跑边标):
- 根节点扫描:GC 把从栈上直接能访问到的对象瞬间染成 灰色。
- 并发推进:GC 线程从灰色对象出发,把它的子对象染灰,把自己染黑。灰色像一道波浪,推着白色(未知区域)向黑色(安全区域)转化。
如果 UI 捣乱怎么办?(写屏障 Write Barrier) 既然是并发,就可能出事。比如 GC 刚把一个对象标记为黑色(扫完了),UI 线程突然把一个白色的新对象塞给了这个黑色对象。GC 会以为黑色对象已经没事了,结果漏掉了那个白色对象。
Dart 的对策是 写屏障:
UI 线程: “大哥(GC),我对这个黑色对象做了修改,给它指了个新引用。” GC: “收到。把你修改的这个黑色对象重新标灰,我稍后会重新扫描它。”
正是这套机制,让 Flutter 在进行大规模内存扫描时,依然能保持流畅。
四、 整理:治愈“瑞士奶酪” (Compaction)
当标记完成后,所有白色的对象就是垃圾。Dart 会把它们的地址回收。 但是,随着时间推移,老年代会变成一块 “瑞士奶酪” —— 到处都是小的空洞(内存碎片)。
如果你突然想存一张高清大图(需要连续的大块内存),虽然空洞加起来总和够大,但没有一块连续区域能放得下。这就叫 OOM (Out Of Memory)。
这时候,Dart 会祭出最后的大招:整理 (Compaction)。
动作: GC 会暂停世界,把所有活着的(黑色)对象,强行往内存的一端推移,把它们挤在一起,把所有的空隙都挤到另一端去。
代价: 这是一个非常昂贵的操作。所以,Dart 并不是每次 GC 都做整理,只有在碎片化非常严重,或者内存即将耗尽时才会触发。
小结
老年代的世界没有“唯快不破”的热血,只有“精打细算”的权衡。
- 晋升:避免了长寿对象的无意义搬运。
- 并发标记:利用三色模型,让 GC 和 UI 线程和谐共处。
- 整理:解决内存碎片,防止 OOM。
至此,我们已经看懂了 Dart 内存管理的空间维度。
但还有一个时间维度的谜题没有解开: Flutter 每一帧只有 16ms,Dart 到底是在这 16ms 的哪个时间缝隙里偷偷执行这些 GC 操作的?
下一章(终章),我们将揭秘 Flutter Engine 的调度机制 —— 看它如何利用 Idle Time(空闲时间) 做到“在用户眨眼的瞬间打扫完房间”。
Flutter 性能底层(四):终极调度 —— 时间缝隙里的生存游戏
引言:16 毫秒的生死线
做过动画或游戏开发的都知道,16.6ms 是一条不可逾越的生死线。 为了保持 60fps 的流畅体验,系统每隔 16.6ms 就会发出一个 VSync(垂直同步)信号。
Flutter 的渲染管线(Build -> Layout -> Paint)必须在这段时间内跑完。
- 跑完了,屏幕刷新,画面丝般顺滑。
- 跑不完,新的一帧无法生成,屏幕重复显示上一帧。这就是用户眼中的 “掉帧” (Jank)。
这时候,一个终极矛盾出现了: GC(垃圾回收)也是要消耗 CPU 的。如果我的业务代码已经很重了,GC 还要横插一杠,岂不是必然导致卡顿?
Dart 的回答是:我会看脸色,也会在绝境中求生。
一、 顺风局:引擎与 VM 的“空闲密谋” (Idle-Time GC)
在大多数正常的场景下,Flutter 的每一帧并不会填满 16ms。也许你的代码很高效,只用了 10ms 就完成了所有渲染工作。
那么,剩下的 6.6ms 去哪了? 这段时间,CPU 处于空闲状态,等待下一个 VSync 信号。这就是 “空闲时间 (Idle Time)”。
Flutter Engine(渲染管家)极其聪明,它利用这段时间与 Dart VM(内存管家)进行了一次完美的配合:
- 计算余额:Engine 发现:“当前帧已提交,距离下一帧还有 6ms。”
- 发送信号:Engine 通知 VM:“嘿,我有 6ms 空档,你要不要打扫卫生?”
- 见缝插针:VM 评估发现新生代 GC 只需要 2ms,于是果断执行 Sca venge。
结果:GC 在用户感知的“死角”里完成了。用户觉得 App 极其流畅,完全感觉不到内存被清理了。这是 Dart 最理想的工作状态。
二、 逆风局:当没有空闲时间时 (Forced GC & Jank)
但是(关键的转折来了),现实并不总是完美的。
假设你写了一个极其复杂的列表,或者正在进行大量的 3D 变换计算,导致 Build 和 Layout 阶段就已经消耗了 15ms。
这时候,Engine 根本没有多余的时间留给 VM。
更糟糕的是,在这个高强度的计算过程中,你的代码还在疯狂地 new 对象(比如在循环里创建临时变量)。
突然,新生代(New Space)满了。 指针指到了尽头,物理上无法再分配下一个对象的内存了。
这时候,Dart VM 别无选择,必须启动 “应急预案”。这个过程不再是协商,而是强制执行。
强制 GC (Stop The World)
- 立即挂起:VM 必须强制暂停当前的 UI 线程。哪怕现在正画到一半,也得停下来,因为没有内存可用了。
- 原地清理:VM 立即执行 Sca venge 算法,清理新生代。
- 惨痛代价:
- 业务代码耗时:15ms。
- 强制 GC 耗时:3ms。
- 总耗时:18ms。
结局:总时间超过了 16.6ms 的红线。VSync 信号来了,但新画面没准备好。用户看到了明显的 卡顿。
三、 绝境局:老年代的“内存换时间”策略
如果不是新生代满了,而是 老年代(Old Gen) 满了呢? 老年代的 GC(标记-整理)非常慢,可能耗时 100ms 以上。如果在 UI 运行过程中触发这个,App 会直接卡死一瞬间。
为了避免这种灾难性的体验,Dart 采用了一种 “能拖就拖” 的策略:
- 优先扩容:如果老年代满了,但当前没有空闲时间,Dart 通常不会立即触发老年代 GC。相反,它会向操作系统申请更多的物理内存,临时扩大老年代的容量(Hard Limit)。
- 扛过高峰:先把新晋升的对象塞进去,硬扛过这一波高负载的动画或计算。
- 秋后算账:等到后面终于有空闲时间了,或者内存占用实在太大触碰到了 OOM(内存溢出)的红线,才会强制执行大规模 GC。
这也解释了为什么 Flutter App 有时候内存占用会飙升: 因为它在用 空间(内存) 换取 时间(流畅度)。它宁愿多吃点内存,也不想让用户感到卡顿。
四、 总结:开发者的责任
看完这整个系列,我们可以得出一个最终结论:
Dart 的内存管理机制是“为 UI 而生”的,它构筑了三道防线:
- 极速分配(微观):指针碰撞,快如闪电。
- 并发标记(宏观):三色算法,减少阻塞。
- 空闲调度(终极):见缝插针,隐藏成本。
但是,Dart 救不了“作死”的代码。 如果你的代码在每一帧里都塞满了繁重的计算,并且毫无节制地分配内存,导致“空闲时间”消失,那么 强制 GC 和 掉帧 就是必然的物理结果。
所以,作为 Flutter 开发者,我们在享受便利的同时,依然要遵守基本法:
- **多用
const**:const对象不占新生代,不给 GC 添堵。 - **避免循环内
new**:这会瞬间填满新生代,触发强制 GC。 - 拆分耗时任务:别让 UI 线程过载,给 Engine 留一点喘息(和 GC)的时间。
Dart - 认识Pattern Matching(模式匹配)
- 上一篇
- Dart - 认识Pattern Matching(模式匹配)
- 下一篇
- Dart - 完全解剖await/async原理
-
- 文章 · 软件教程 | 3小时前 |
- Chrome DevTools Performance 怎么录制长任务:Main 轨道、红色三角与回放核对
- 415浏览 收藏
-
- 文章 · 软件教程 | 4小时前 | future
- 从一个带并发数限制的请求深入 Dart 的 Future
- 269浏览 收藏
-
- 文章 · 软件教程 | 4小时前 | 强迫症
- Dart 3.7格式化工具“乱改代码”?强迫症必看
- 309浏览 收藏
-
- 文章 · 软件教程 | 4小时前 | Android
- Dart 速通攻略(面向 Android 工程师)
- 444浏览 收藏
-
- 文章 · 软件教程 | 4小时前 | Rust
- 假如Flutter用Rust,你也写不出更快的App
- 306浏览 收藏
-
- 文章 · 软件教程 | 4小时前 | 其他
- 最新的 Dart sdk 安装教程
- 311浏览 收藏
-
- 文章 · 软件教程 | 4小时前 | 其他
- Flutter路由3分钟学会
- 297浏览 收藏
-
- 文章 · 软件教程 | 4小时前 | scroll
- flutter_easy_refresh在3.38.3配合NestedScrollView的注意要点。
- 445浏览 收藏
-
- 文章 · 软件教程 | 4小时前 | 其他
- Kotlin vs Dart:当“优雅”变成心智负担,我选择了更简单的 Dart
- 445浏览 收藏
-
- 文章 · 软件教程 | 4小时前 | 其他
- Flutter自定义组件: 为横向列表自定义“进度条”式滚动指示器
- 452浏览 收藏
-
- 文章 · 软件教程 | 4小时前 | 其他
- Dart - 完全解剖await/async原理
- 129浏览 收藏
-
- 文章 · 软件教程 | 4小时前 | MATCH
- Dart - 认识Pattern Matching(模式匹配)
- 337浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- ljg-skills
- ljg-skills 是李继刚开源的 AI 技能与提示词集合,面向大模型使用者整理了一批可复用的 prompt、角色设定和任务技能模板,适合用于学习提示词设计、搭建个人 AI 工作流和沉淀团队常用智能体能力。
- 5037次使用
-
- MELO音乐
- MELO音乐是一站式AI视频与音乐制作助手,对标suno, udio的高品质体验。提供伴奏生成、原创写词、无损导出、哼唱识曲、混音变声等全套音频与短视频编辑工具。无论是流行Kpop、电音说唱、民谣古风、摇滚儿歌还是商用轻音乐,MELO为你免费谱曲,轻松做同款!
- 4573次使用
-
- UniScribe
- UniScribe 是一款 AI 音视频转文字与内容整理工具,支持上传音频、视频文件或粘贴 YouTube 链接,自动生成转写文本、摘要、思维导图和关键问题,并支持多格式导出,适合会议记录、课程学习、访谈整理和内容创作复盘。
- 4518次使用
-
- 剧云
- 剧云是专业中文剧本创作平台,安全稳定运行十余年,集成AI编剧、剧本医生审核、人物小传、剧情关系图、大纲编写、多人协作、Word导入导出、版权管控功能,数据安全防护,轻松高效创作剧本。
- 4779次使用
-
- 万象有声
- 万象有声,一个专为有声创作者打造的新一代智能有声内容创作平台。平台提供专业的智能拆章、智能画本编辑、AI配音、AI生成音效、后期制作、智能对轨、智能审听等有声创作全流程工具,可以帮助创作者高效、低成本创作出引人入胜的有声作品。立即体验,让有声书制作更简单!
- 4730次使用
-
- VS Code 怎么给 Go 项目配置测试任务:tasks.json 运行与结果验收
- 2026-07-09 501浏览
-
- Windows 11 如何开启 HEIF 图片支持
- 2026-05-31 501浏览
-
- TikTok用户画像与付费订阅变现方法
- 2026-05-27 501浏览
-
- 学信网学历翻译件申请方法
- 2026-05-27 501浏览
-
- Windows 11 24H2 更新失败0x80070005解决方法
- 2026-05-26 501浏览

