29、WebKit 的内存管理:引用计数与智能指针、内存池与分配器、图片与缓存的内存控制、低内存处理与回收

聊到浏览器内核,内存管理是个绕不开的话题。你想想看,一个浏览器要同时打开几十个标签页,每个页面里可能还有图片、视频、各种脚本,内存要是管不好,分分钟卡死或者闪退。我在做WebKit移植的时候,就遇到过不少因为内存泄漏导致OOM的惨案。今天咱们就好好掰扯一下WebKit的内存管理机制。

引用计数与智能指针

WebKit里最基础的内存管理手段,就是引用计数。说白了,就是给每个对象记个账——有多少人在用我。没人用了,我就把自己销毁。

核心思想:每个对象维护一个引用计数器,当引用计数降为0时,自动释放对象。

WebKit里有两个核心的智能指针类:RefPtrPassRefPtr。我刚开始接触的时候,觉得这俩玩意儿挺绕的。后来踩过坑才明白,它们的设计其实很巧妙。

// 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这套机制,经过这么多年的打磨,已经相当成熟了。你只要理解它的设计思路,遇到问题就知道从哪里入手。

WebKit 内存管理体系 WebKit 内存管理 引用计数与智能指针 RefPtr / PassRefPtr / WeakPtr 内存池与分配器 FastMalloc / PartitionAlloc 图片与缓存控制 解码延迟 / LRU淘汰 / 压缩 低内存处理与回收 onTrimMemory / 分级回收 自动生命周期管理 循环引用检测 线程本地缓存 批量分配 / 对齐 解码延迟策略 LRU缓存淘汰 三级压力响应
公众号:蓝海资料掘金营,微信deep3321