WKWebView 线程终止的原因——之 OOM 的数值

博客探讨了iOS应用中WKWebView内存管理的细节,特别是针对OOM(Out of Memory)的控制逻辑。通过抽离WebKit的计算方法,展示了如何获取设备的最大可用内存和jetsamLimit,并提供了Objective-C封装以供Swift调用。在iPhone XS上运行示例代码得出的结果显示,即使设备总内存不足4GB,最大可用内存仍可达8G,这可能是因为WKWebView内部的阈值计算涉及更多因素。文章最后提及要理解iOS系统中WKWebView的实际内存使用,可能需要深入研究其源码。

上篇文章介绍了WKWebView 线程终止的原因——之 OOM 的控制逻辑,那 iOS 的最大可用内存到底是多少呢?我们可不可以将 WebKit 中的计算逻辑拿出来运行一下呢?

最终的实现过程可以查看 GitHub 上的 WKWebViewMemory

抽离 WebKit 的计算方法

我们可以尝试将WKWebView 线程终止的原因——之 OOM 的控制逻辑中找的的对应方法,放到一个 app 程序当中,来获取对应的数值。

整体的方法整理在如下:

#include "MemoryPressure.hpp"
#include <iostream>
#include <mutex>
#include <array>
#include <mutex>
#import <algorithm>
#import <dispatch/dispatch.h>
#import <mach/host_info.h>
#import <mach/mach.h>
#import <mach/mach_error.h>
#import <math.h>

#define MEMORYSTATUS_CMD_GET_MEMLIMIT_PROPERTIES 8
static constexpr size_t kB = 1024;
static constexpr size_t MB = kB * kB;
static constexpr size_t GB = kB * kB * kB;
static constexpr size_t availableMemoryGuess = 512 * MB;


#if __has_include(<System/sys/kern_memorystatus.h>)
extern "C" {
#include <System/sys/kern_memorystatus.h>
}
#else
extern "C" {
using namespace std;

typedef struct memorystatus_memlimit_properties {
    int32_t memlimit_active;                /* jetsam memory limit (in MB) when process is active */
    uint32_t memlimit_active_attr;
    int32_t memlimit_inactive;              /* jetsam memory limit (in MB) when process is inactive */
    uint32_t memlimit_inactive_attr;
} memorystatus_memlimit_properties_t;

#define MEMORYSTATUS_CMD_GET_MEMLIMIT_PROPERTIES 8
#define MEMORYSTATUS_CMD_SET_PROCESS_IS_FREEZABLE 18
#define MEMORYSTATUS_CMD_GET_PROCESS_IS_FREEZABLE 19

}
#endif // __has_include(<System/sys/kern_memorystatus.h>)

extern "C" {
int memorystatus_control(uint32_t command, int32_t pid, uint32_t flags, void *buffer, size_t buffersize);
}

size_t MemoryPressure::jetsamLimit()
{
    memorystatus_memlimit_properties_t properties;
    pid_t pid = getpid();
    if (memorystatus_control(MEMORYSTATUS_CMD_GET_MEMLIMIT_PROPERTIES, pid, 0, &properties, sizeof(properties)))
        return 840 * MB;
    if (properties.memlimit_active < 0)
        return std::numeric_limits<size_t>::max();
    return static_cast<size_t>(properties.memlimit_active) * MB;
}

size_t MemoryPressure::memorySizeAccordingToKernel()
{
    host_basic_info_data_t hostInfo;

    mach_port_t host = mach_host_self();
    mach_msg_type_number_t count = HOST_BASIC_INFO_COUNT;
    kern_return_t r = host_info(host, HOST_BASIC_INFO, (host_info_t)&hostInfo, &count);
    mach_port_deallocate(mach_task_self(), host);
    if (r != KERN_SUCCESS)
        return availableMemoryGuess;

    if (hostInfo.max_mem > std::numeric_limits<size_t>::max())
        return std::numeric_limits<size_t>::max();

    return static_cast<size_t>(hostInfo.max_mem);
}

size_t MemoryPressure::computeAvailableMemory()
{
    size_t memorySize = memorySizeAccordingToKernel();
    size_t sizeJetsamLimit = jetsamLimit();
    cout << "jetsamLimit:" << sizeJetsamLimit / 1024 / 1024 << "MB\n";
    cout << "memorySize:" << memorySize / 1024 / 1024 << "MB\n";
    size_t sizeAccordingToKernel = std::min(memorySize, sizeJetsamLimit);
    size_t multiple = 128 * MB;

    // Round up the memory size to a multiple of 128MB because max_mem may not be exactly 512MB
    // (for example) and we have code that depends on those boundaries.
    sizeAccordingToKernel = ((sizeAccordingToKernel + multiple - 1) / multiple) * multiple;
    cout << "sizeAccordingToKernel:" << sizeAccordingToKernel / 1024 / 1024 << "MB\n";
    return sizeAccordingToKernel;
}

size_t MemoryPressure::availableMemory()
{
    static size_t availableMemory;
    static std::once_flag onceFlag;
    std::call_once(onceFlag, [this] {
        availableMemory = computeAvailableMemory();
    });
    return availableMemory;
}

size_t MemoryPressure::computeRAMSize()
{
    return availableMemory();
}

size_t MemoryPressure::ramSize()
{
    static size_t ramSize;
    static std::once_flag onceFlag;
    std::call_once(onceFlag, [this] {
        ramSize = computeRAMSize();
    });
    return ramSize;
}

size_t MemoryPressure::thresholdForMemoryKillOfActiveProcess(unsigned tabCount)
{
    size_t ramSizeV = ramSize();
    cout << "ramSize:" << ramSizeV / 1024 / 1024 << "MB\n";
    
    size_t baseThreshold = ramSizeV > 16 * GB ? 15 * GB : 7 * GB;
    return baseThreshold + tabCount * GB;
}

size_t MemoryPressure::thresholdForMemoryKillOfInactiveProcess(unsigned tabCount)
{
//#if CPU(X86_64) || CPU(ARM64)
    size_t baseThreshold = 3 * GB + tabCount * GB;
//#else
//    size_t baseThreshold = tabCount > 1 ? 3 * GB : 2 * GB;
//#endif
    return std::min(baseThreshold, static_cast<size_t>(ramSize() * 0.9));
}

上面方法中的具体作用,可以查看WKWebView 线程终止的原因——之 OOM 的控制逻辑

Swift 并不能直接调用 C++ 的方法,所以,我们需要使用 Object-C 进行封装:

#import "MemoryPressureWrapper.h"
#import "MemoryPressure.hpp"

@implementation MemoryPressureWrapper

+ (size_t)thresholdForMemoryKillOfActiveProcess {
    MemoryPressure cpp;
    return cpp.thresholdForMemoryKillOfActiveProcess(1);
}

+ (size_t)thresholdForMemoryKillOfInactiveProcess {
    MemoryPressure cpp;
    return cpp.thresholdForMemoryKillOfInactiveProcess(1);
}

@end

另外:如果 Object-C 调用 C++ 代码时,要将创建的 .m 文件后缀改成 .mm ,告诉 XCode 编译该文件时要用到 C++ 代码。

直接引用 WebKit 的基础模块

在 WebKit 中,内存相关的方法在 WTFbmalloc 模块中。我们可以下载下来源码,然后创建一个 APP 来引用。步骤如下:

  1. 下载 WebKit 源码,找到 Source/WTFSource/bmalloc 模块,和 Tools/ccache 文件。
  2. 新建一个 WorkSpace:WKWebViewMemory,再新建、添加一个 iOS Project:WKWebViewMemoryApp。并将步骤 1 中的 Source/WTFSource/bmalloc 模块添加到 WorkSpace 中,
  3. 在 WKWebViewMemoryApp 的 TARGETS 的 Build Settings 中,找到 Header Search Paths,添加 $(BUILT_PRODUCTS_DIR)/usr/local/include$(DSTROOT)/usr/local/include$(inherited)
  4. 因为 WTF中的计算方法为 private 的,为了能在 app 中进行访问,需要修改为 public。

最终,就可以通过下面的方式进行获取了:

#import "WTFWrapper.h"
#import <wtf/MemoryPressureHandler.h>

// 这个文件必须名称为 .mm 类型,否自会编译错误
@implementation WTFWrapper

+ (size_t)thresholdForMemoryKillOfActiveProcess {
    return WTF::thresholdForMemoryKillOfActiveProcess(1);
}

+ (size_t)thresholdForMemoryKillOfInactiveProcess {    
    return WTF::thresholdForMemoryKillOfInactiveProcess(1);
}
@end

运行结果

在 iPhoneXS 上运行的结果为:

jetsamLimit 的值为:840MB
内存大小(memorySize)为:3778MB
ramSize为:896MB
激活状态下(ActiveProcess)的最大可用内存为:8G
非激活状态下(InactiveProcess)的最大可用内存为:806M

奇怪的结果:在最大内存为 3778MB(不到 4G)的手机上,最大可用内存居然为 8G。

为什么?难道计算错了?难道内存没有限制?

我们来重新看一下获取最大可用内存的方法:

std::optional<size_t> MemoryPressureHandler::thresholdForMemoryKill()
{
    if (m_configuration.killThresholdFraction)
        return m_configuration.baseThreshold * (*m_configuration.killThresholdFraction);

    switch (m_processState) {
    case WebsamProcessState::Inactive:
        return thresholdForMemoryKillOfInactiveProcess(m_pageCount);
    case WebsamProcessState::Active:
        return thresholdForMemoryKillOfActiveProcess(m_pageCount);
    }
    return std::nullopt;
}

如果配置当中设置了 killThresholdFraction,则会通过 m_configuration.baseThreshold * (*m_configuration.killThresholdFraction); 进行计算。

我怀疑是抽离出来的方法,没有设置 killThresholdFraction,而在 iOS 系统中,在初始化 WKWebView 时,会设置此值,来返回一个合理的数值。

那在 iOS 系统中,thresholdForMemoryKill() 究竟会返回多少呢?可能只能通过 WKWebView 的源码进行获取了。

如果想获取最终可以运行的项目,可以查看 GitHub 上的 WKWebViewMemory

参考

### 三级标题:排查和解决最大线程数设置过大导致的内存溢出问题 当线程数设置过大时,系统可能因无法分配足够的内存而导致内存溢出(OOM)。以下是排查和解决此类问题的方法。 #### 1. 确定当前系统限制 使用 `ulimit` 命令来查看当前系统的资源限制。特别是与线程和文件描述符相关的限制: ```bash ulimit -a ``` 重点关注以下参数: - `-n`:最大打开文件描述符数。 - `-u`:单个用户可创建的最大进程数。 - `-s`:最大堆栈大小。 调整这些参数可以增加系统支持的线程数。例如,增大堆栈大小可能会减少可用线程数,反之亦然。 #### 2. 检查系统内存和虚拟内存使用情况 线程数的增加会消耗更多的内存资源,包括物理内存和虚拟内存。通过以下命令监控系统内存使用情况: ```bash free -m ``` 此外,使用 `top` 或 `htop` 工具查看内存使用情况,确保系统有足够的空闲内存供新线程分配。 如果系统内存不足,可以通过调整虚拟内存设置来缓解问题: ```bash sysctl -w vm.swappiness=10 ``` 此命令将虚拟内存的交换倾向设置为较低值,从而减少系统对交换空间的依赖。 #### 3. 调整线程池配置 如果使用线程池管理线程,确保线程池的大小合理。线程池的最大线程数应根据系统资源进行调整。例如,在 Java 应用中,可以通过以下方式配置线程池: ```java ExecutorService executorService = Executors.newFixedThreadPool(10); ``` 根据系统资源和任务负载,适当减少线程池的大小,以避免内存溢出。 #### 4. 分析和优化 JVM 参数 如果问题是由于 JVM 内存不足引起的,可以通过调整 JVM 启动参数来优化内存使用。例如,增加堆内存: ```bash java -Xms512m -Xmx2g MyApplication ``` 此命令将 JVM 的初始堆内存设置为 512MB,最大堆内存设置为 2GB。根据系统资源,适当调整这些参数,以确保 JVM 有足够的内存供线程使用。 #### 5. 使用内存分析工具 使用内存分析工具(如 `Valgrind`、`Perf` 或 `VisualVM`)来检测内存泄漏和优化内存使用。这些工具可以帮助识别内存瓶颈,并提供优化建议。 #### 6. 优化代码逻辑 检查代码逻辑,确保线程的创建和销毁是高效的。避免不必要的线程创建,并确保线程在完成任务后能够正确释放资源。例如,在 Java 中,可以使用 `Thread.join()` 方法等待线程完成: ```java Thread thread = new Thread(() -> { // 执行任务 }); thread.start(); thread.join(); ``` 确保线程在完成后正确释放资源,以避免内存泄漏。 ### 三级标题:解决方案总结 1. **调整系统资源限制**:使用 `ulimit` 命令调整系统资源限制,确保系统支持所需的线程数。 2. **监控系统内存**:使用 `free`、`top` 或 `htop` 监控系统内存使用情况,确保有足够的内存供线程使用。 3. **优化线程池配置**:根据系统资源调整线程池的大小,避免内存溢出。 4. **调整 JVM 参数**:增加 JVM 的堆内存,确保 JVM 有足够的内存供线程使用。 5. **使用内存分析工具**:使用内存分析工具检测内存泄漏和优化内存使用。 6. **优化代码逻辑**:确保线程的创建和销毁是高效的,避免不必要的线程创建。 通过以上方法,可以有效排查和解决因最大线程数设置过大导致的内存溢出问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值