29、WebKit 的内存管理:引用计数与智能指针、内存池与分配器、图片与缓存的内存控制、低内存处理与回收
聊到浏览器内核,内存管理是个绕不开的话题。你想想看,一个浏览器要同时打开几十个标签页,每个页面里可能还有图片、视频、各种脚本,内存要是管不好,分分钟卡死或者闪退。我在做WebKit移植的时候,就遇到过不少因为内存泄漏导致OOM的惨案。今天咱们就好好掰扯一下WebKit的内存管理机制。
引用计数与智能指针
WebKit里最基础的内存管理手段,就是引用计数。说白了,就是给每个对象记个账——有多少人在用我。没人用了,我就把自己销毁。
核心思想:每个对象维护一个引用计数器,当引用计数降为0时,自动释放对象。
WebKit里有两个核心的智能指针类:RefPtr和PassRefPtr。我刚开始接触的时候,觉得这俩玩意儿挺绕的。后来踩过坑才明白,它们的设计其实很巧妙。
// RefPtr 示例
RefPtr<Node> node = document->createElement("div");
// 这里node的引用计数为1
RefPtr<Node> anotherNode = node;
// 现在引用计数变成2了
node = nullptr;
// 引用计数减1,还剩1
anotherNode = nullptr;
// 引用计数归0,对象被销毁
嗯,这里要注意一个坑。我曾经在项目里遇到过循环引用的问题——两个对象互相持有对方的RefPtr,结果谁也释放不了。后来怎么解决的?用WeakPtr打破循环。WeakPtr不增加引用计数,就像个旁观者,只观察不干涉。
避坑指南:我曾经在渲染树节点之间不小心引入了循环引用,导致整个页面的DOM树无法释放。排查了整整两天才发现问题。记住:父子关系用RefPtr,兄弟关系或者回引用用WeakPtr。
内存池与分配器
频繁地new和delete对象,性能开销很大。WebKit用了内存池来优化这个问题。我个人的习惯是,在需要大量创建小对象的场景下,内存池几乎是必选项。
WebKit里有个叫FastMalloc的东西,它其实是对系统malloc的一层封装。但别小看这层封装,它做了很多优化:
- 线程本地缓存:每个线程有自己的小对象缓存,避免锁竞争
- 批量分配:一次申请一大块内存,然后切分成小份
- 内存对齐:保证分配的内存地址对齐,提高CPU访问效率
还有一个叫PartitionAlloc的分配器,专门用来管理特定类型的内存。比如,DOM节点和渲染对象的内存是分开管理的。这样做的好处是,当你释放一个页面时,可以整块回收,不会产生碎片。
// WebKit中内存池的使用示例
void* buffer = FastMalloc::allocate(1024);
// 使用buffer...
FastMalloc::deallocate(buffer);
注意:不要直接调用系统的malloc/free。WebKit里所有内存分配都应该通过FastMalloc或者PartitionAlloc。否则,内存统计和回收机制会出问题。
图片与缓存的内存控制
图片是浏览器里最吃内存的资源之一。一张1920x1080的图片,解码成位图后,光像素数据就要占8MB(如果是RGBA格式)。你想想看,一个页面里放个十几张图片,内存就上百兆了。
WebKit的图片缓存机制,我总结下来有这几个关键点:
- 解码延迟:图片下载完成后,不会立即解码。只有真正要显示的时候才解码。这个策略能省不少内存。
- 缓存淘汰:使用LRU(最近最少使用)算法。我见过一个场景,用户快速滑动页面时,图片不断进出缓存,淘汰策略没调好,导致频繁解码,反而更卡了。
- 内存压缩:对于不活跃的图片,可以压缩成更小的格式。比如把32位色深降到16位。
我记得有一次,一个电商页面在低端机上总是闪退。排查下来,是商品列表页的缩略图缓存没控制好,一次性解码了太多图片。后来我们限制了图片缓存的最大数量,并且对不可见的图片优先回收,问题就解决了。
经验之谈:图片缓存的大小,建议根据设备内存来动态调整。2GB内存的手机和8GB内存的手机,缓存策略应该不一样。WebKit里有个MemoryCache类,可以设置容量上限。
低内存处理与回收
低内存处理,说白了就是系统快撑不住了,浏览器得赶紧腾出空间。Android系统会发onTrimMemory回调,WebKit收到后要立刻响应。
WebKit的低内存处理策略,我把它分成三个等级:
| 内存压力等级 | 回收策略 | 影响 |
|---|---|---|
| 轻度 | 清理图片解码缓存、清除未使用的JS堆 | 用户几乎无感知 |
| 中度 | 回收后台标签页的渲染资源、压缩字体缓存 | 切换到后台标签页时可能重新渲染 |
| 重度 | 释放所有非活跃页面的内存、甚至丢弃整个页面 | 用户可能看到页面重新加载 |
我曾经在低端机上做过测试,当内存只剩100MB时,如果不做任何处理,浏览器会在10秒内被系统杀掉。但如果我们主动回收图片缓存和后台标签页,可以把这个时间延长到30秒以上,给用户留出保存数据的时间。
WebKit里有个MemoryPressureHandler,专门负责监听和处理内存压力事件。它会定期检查各个模块的内存使用情况,然后决定回收哪些资源。
// 低内存处理的伪代码
void MemoryPressureHandler::handlePressure(int level) {
if (level >= CRITICAL) {
// 释放所有非活跃页面的内存
for (auto& page : m_pages) {
if (!page->isActive()) {
page->discard();
}
}
}
// 清理图片缓存
m_imageCache->prune();
// 通知JS引擎进行垃圾回收
m_jsEngine->performGC();
}
小技巧:在调试内存问题时,可以打开WebKit的--show-composited-layer-borders标志,它能显示每个渲染层的内存占用情况。我经常用这个来定位哪些层占用了大量内存。
最后,我想说一句。内存管理没有银弹,每个场景都需要具体分析。但WebKit这套机制,经过这么多年的打磨,已经相当成熟了。你只要理解它的设计思路,遇到问题就知道从哪里入手。