持续创造,加速生长!这是我参加「日新方案 6 月更文应战」的第1天,点击查看活动详情
最近在 WKWebView 中展示三维图画烘托的功能时,经常遇到 WKWebView 不可思议的 reload 的现象。
WKWebView 会在 APP 的进程外履行其所有的作业,并且 WKWebView 的内存运用量与 APP 的内存运用量分开核算。这样当 WKWebView 进程超出其内存约束时,就不会导致 APP 程序停止,最多也便是导致空白视图。
为了定位详细的原因,先查看一下 WKWebView 的署理提供的回调办法。在 WKNavigationDelegate
中界说了回调办法 webViewWebContentProcessDidTerminate(_)
:
/** @abstract Invoked when the web view's web content process is terminated.
@param webView The web view whose underlying web content process was terminated.
*/
@available(iOS 9.0, *)
optional func webViewWebContentProcessDidTerminate(_ webView: WKWebView)
当 web 视图的内容进程停止时,将经过此回调通知 APP,但是并没有提供更多的错误信息。
能够经过 github.com/WebKit/WebK… 看到 WKWebView 的源码。经过搜索 webViewWebContentProcessDidTerminate
的办法,能够一步步知道 WKWebView 的反常流程。
WKWebView 进程反常的流程
WKWebView 的停止署理流程
在 WebKit 的 NavigationState.mm 文件中,调用了 webViewWebContentProcessDidTerminate
办法:
bool NavigationState::NavigationClient::processDidTerminate(WebPageProxy& page, ProcessTerminationReason reason)
{
if (!m_navigationState)
return false;
if (!m_navigationState->m_navigationDelegateMethods.webViewWebContentProcessDidTerminate
&& !m_navigationState->m_navigationDelegateMethods.webViewWebContentProcessDidTerminateWithReason
&& !m_navigationState->m_navigationDelegateMethods.webViewWebProcessDidCrash)
return false;
auto navigationDelegate = m_navigationState->m_navigationDelegate.get();
if (!navigationDelegate)
return false;
if (m_navigationState->m_navigationDelegateMethods.webViewWebContentProcessDidTerminateWithReason) {
[static_cast<id <WKNavigationDelegatePrivate>>(navigationDelegate.get()) _webView:m_navigationState->m_webView webContentProcessDidTerminateWithReason:wkProcessTerminationReason(reason)];
return true;
}
// We prefer webViewWebContentProcessDidTerminate: over _webViewWebProcessDidCrash:.
if (m_navigationState->m_navigationDelegateMethods.webViewWebContentProcessDidTerminate) {
[navigationDelegate webViewWebContentProcessDidTerminate:m_navigationState->m_webView];
return true;
}
ASSERT(m_navigationState->m_navigationDelegateMethods.webViewWebProcessDidCrash);
[static_cast<id <WKNavigationDelegatePrivate>>(navigationDelegate.get()) _webViewWebProcessDidCrash:m_navigationState->m_webView];
return true;
}
在 processDidTerminate()
办法中,当线程停止时的处理流程为:
- 若未设置署理办法,则回来 false;
- 假如署理完成了
_webView:webViewWebContentProcessDidTerminateWithReason:
,则回调,并回来 true; - 假如署理完成了
webViewWebContentProcessDidTerminate:
,则回调,并回来 true; - 调用回调办法:
_webViewWebProcessDidCrash:
,并回来 true。
署理办法的设置办法如下:
void NavigationState::setNavigationDelegate(id <WKNavigationDelegate> delegate)
{
// ....
m_navigationDelegateMethods.webViewWebContentProcessDidTerminate = [delegate respondsToSelector:@selector(webViewWebContentProcessDidTerminate:)];
m_navigationDelegateMethods.webViewWebContentProcessDidTerminateWithReason = [delegate respondsToSelector:@selector(_webView:webContentProcessDidTerminateWithReason:)];
// ....
}
在 processDidTerminate()
办法中,参数 reason
说明晰反常原因,类型为 ProcessTerminationReason
,界说如下:
enum class ProcessTerminationReason {
ExceededMemoryLimit, // 超出内存约束
ExceededCPULimit, // 超出CPU约束
RequestedByClient, // 自动触发的terminate
IdleExit,
Unresponsive, // 无法呼应
Crash, // web进程自己发生了crash
// Those below only relevant for the WebContent process.
ExceededProcessCountLimit,
NavigationSwap,
RequestedByNetworkProcess,
RequestedByGPUProcess
};
经过署理办法获取反常原因
能够看到回调办法有两个:webViewWebContentProcessDidTerminate:
和_webView:webContentProcessDidTerminateWithReason:
,一个不带 reason 参数,一个带有 reason 参数,并且带有 reason 参数的回调办法优先级更高。
在 WKWebView 的 WKNavigationDelegate
署理中,咱们只看到了不带 reason 的回调办法,那 _webView:webContentProcessDidTerminateWithReason:
是怎么回事呢?
经过检索发现,它界说在 WKNavigationDelegatePrivate
在署理中:
@protocol WKNavigationDelegatePrivate <WKNavigationDelegate>
@optional
// ...
- (void)_webView:(WKWebView *)webView webContentProcessDidTerminateWithReason:(_WKProcessTerminationReason)reason WK_API_AVAILABLE(macos(10.14), ios(12.0));
// ...
@end
WKNavigationDelegatePrivate
并没有公开让 App 运用。不过,咱们依然能够经过完成上面的署理办法,获取到 reason 信息。
不过需求留意:WebKit 内部的反常类型为:ProcessTerminationReason
,而此处 reason 参数的类型为:_WKProcessTerminationReason
:
typedef NS_ENUM(NSInteger, _WKProcessTerminationReason) {
_WKProcessTerminationReasonExceededMemoryLimit,
_WKProcessTerminationReasonExceededCPULimit,
_WKProcessTerminationReasonRequestedByClient,
_WKProcessTerminationReasonCrash,
} WK_API_AVAILABLE(macos(10.14), ios(12.0));
_WKProcessTerminationReason
和 ProcessTerminationReason
的转化联系如下:
static _WKProcessTerminationReason wkProcessTerminationReason(ProcessTerminationReason reason)
{
switch (reason) {
case ProcessTerminationReason::ExceededMemoryLimit:
return _WKProcessTerminationReasonExceededMemoryLimit;
case ProcessTerminationReason::ExceededCPULimit:
return _WKProcessTerminationReasonExceededCPULimit;
case ProcessTerminationReason::NavigationSwap:
case ProcessTerminationReason::IdleExit:
// We probably shouldn't bother coming up with a new API type for process-swapping.
// "Requested by client" seems like the best match for existing types.
FALLTHROUGH;
case ProcessTerminationReason::RequestedByClient:
return _WKProcessTerminationReasonRequestedByClient;
case ProcessTerminationReason::ExceededProcessCountLimit:
case ProcessTerminationReason::Unresponsive:
case ProcessTerminationReason::RequestedByNetworkProcess:
case ProcessTerminationReason::RequestedByGPUProcess:
case ProcessTerminationReason::Crash:
return _WKProcessTerminationReasonCrash;
}
ASSERT_NOT_REACHED();
return _WKProcessTerminationReasonCrash;
}
能够看出,在转化过程中,并不是一一对应的,会损失掉详细的 crash 类型。也便是,当咱们完成_webView:webContentProcessDidTerminateWithReason:
署理时,能够获取到一个相对抽象的 reason。
内存超限的逻辑(ExceededMemoryLimit)
下面先剖析内存超限的逻辑。
初始化 web 线程的办法:initializeWebProcess()
,完成如下:
void WebProcess::initializeWebProcess(WebProcessCreationParameters&& parameters)
{
// ...
if (!m_suppressMemoryPressureHandler) {
// ...
#if ENABLE(PERIODIC_MEMORY_MONITOR)
memoryPressureHandler.setShouldUsePeriodicMemoryMonitor(true);
memoryPressureHandler.setMemoryKillCallback([this] () {
WebCore::logMemoryStatistics(LogMemoryStatisticsReason::OutOfMemoryDeath);
if (MemoryPressureHandler::singleton().processState() == WebsamProcessState::Active)
parentProcessConnection()->send(Messages::WebProcessProxy::DidExceedActiveMemoryLimit(), 0);
else
parentProcessConnection()->send(Messages::WebProcessProxy::DidExceedInactiveMemoryLimit(), 0);
});
// ...
#endif
// ...
}
// ...
}
其间:
-
setShouldUsePeriodicMemoryMonitor()
设置是否需求定时检测内存; -
setMemoryKillCallback()
设置内存超限后,被停止后的回调。
定时内存检测
设置定时内存检测的办法setShouldUsePeriodicMemoryMonitor
的完成如下:
void MemoryPressureHandler::setShouldUsePeriodicMemoryMonitor(bool use)
{
if (!isFastMallocEnabled()) {
// If we're running with FastMalloc disabled, some kind of testing or debugging is probably happening.
// Let's be nice and not enable the memory kill mechanism.
return;
}
if (use) {
m_measurementTimer = makeUnique<RunLoop::Timer<MemoryPressureHandler>>(RunLoop::main(), this, &MemoryPressureHandler::measurementTimerFired);
m_measurementTimer->startRepeating(m_configuration.pollInterval);
} else
m_measurementTimer = nullptr;
}
其间,初始化了一个 Timer,时刻距离为 m_configuration.pollInterval(pollInterval 的值为 30s),履行办法为 measurementTimerFired()
。也便是每隔 30s 调用一次 measurementTimerFired()
对内存运用量进行一次查看。
内存查看的办法 measurementTimerFired()
界说如下:
void MemoryPressureHandler::measurementTimerFired()
{
size_t footprint = memoryFootprint();
#if PLATFORM(COCOA)
RELEASE_LOG(MemoryPressure, "Current memory footprint: %zu MB", footprint / MB);
#endif
auto killThreshold = thresholdForMemoryKill();
if (killThreshold && footprint >= *killThreshold) {
shrinkOrDie(*killThreshold);
return;
}
setMemoryUsagePolicyBasedOnFootprint(footprint);
switch (m_memoryUsagePolicy) {
case MemoryUsagePolicy::Unrestricted:
break;
case MemoryUsagePolicy::Conservative:
releaseMemory(Critical::No, Synchronous::No);
break;
case MemoryUsagePolicy::Strict:
releaseMemory(Critical::Yes, Synchronous::No);
break;
}
if (processState() == WebsamProcessState::Active && footprint > thresholdForMemoryKillOfInactiveProcess(m_pageCount))
doesExceedInactiveLimitWhileActive();
else
doesNotExceedInactiveLimitWhileActive();
}
其间,footprint
来为当时运用的内存量,killThreshold
为内存的最大约束。假如 killThreshold
大于等于 footprint
,则调用 shrinkOrDie()
。
当时运用的内存量
当时运用的内存量是经过 memoryFootprint()
来获取的。界说如下:
namespace WTF {
size_t memoryFootprint()
{
task_vm_info_data_t vmInfo;
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
kern_return_t result = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count);
if (result != KERN_SUCCESS)
return 0;
return static_cast<size_t>(vmInfo.phys_footprint);
}
}
其间,运用到了 task_info
来获取线程的信息,传递的参数有:
- mach_task_self() :获取当时进程
- TASK_VM_INFO:取虚拟内存信息
- vmInfo、count:两个参数传递的为引用地址,用于接收回来值。
- vmInfo:task_vm_info_data_t 里的 phys_footprint 便是进程的内存占用,以 byte 为单位。
内存的最大约束
内存最大的约束由 thresholdForMemoryKill() 办法完成,界说如下:
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;
}
static size_t thresholdForMemoryKillOfActiveProcess(unsigned tabCount)
{
size_t baseThreshold = ramSize() > 16 * GB ? 15 * GB : 7 * GB;
return baseThreshold + tabCount * GB;
}
static size_t 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));
}
能够看出,最大的可用内存由:当时的 webview 的页数(m_pageCount),线程的状况(Inactive 和 Active)和 ramSize() 核算得来。
当时的页数和线程的状况比较易容了解,下面来看 ramSize() 的核算办法:
namespace WTF {
size_t ramSize()
{
static size_t ramSize;
static std::once_flag onceFlag;
std::call_once(onceFlag, [] {
ramSize = computeRAMSize();
});
return ramSize;
}
} // namespace WTF
ramSize 只会核算一次,由 computeRAMSize()
核算得来,界说如下:
#if OS(WINDOWS)
static constexpr size_t ramSizeGuess = 512 * MB;
#endif
static size_t computeRAMSize()
{
#if OS(WINDOWS)
MEMORYSTATUSEX status;
status.dwLength = sizeof(status);
bool result = GlobalMemoryStatusEx(&status);
if (!result)
return ramSizeGuess;
return status.ullTotalPhys;
#elif USE(SYSTEM_MALLOC)
#if OS(LINUX) || OS(FREEBSD)
struct sysinfo si;
sysinfo(&si);
return si.totalram * si.mem_unit;
#elif OS(UNIX)
long pages = sysconf(_SC_PHYS_PAGES);
long pageSize = sysconf(_SC_PAGE_SIZE);
return pages * pageSize;
#else
#error "Missing a platform specific way of determining the available RAM"
#endif // OS(LINUX) || OS(FREEBSD) || OS(UNIX)
#else
return bmalloc::api::availableMemory();
#endif
}
computeRAMSize()
中,依据不同的操作体系(Windows,LINUX、Unix)和一个默许方式来核算。需求留意的是:虽然iOS是基于 Unix 的,但是这儿的 Unix 不包括 iOS 体系。所以,在 iOS 体系中,会履行 return bmalloc::api::availableMemory();
。界说如下:
inline size_t availableMemory()
{
return bmalloc::availableMemory();
}
它仅仅简略的调用了 bmalloc::availableMemory()
。再来看 bmalloc::availableMemory()
的完成:
size_t availableMemory()
{
static size_t availableMemory;
static std::once_flag onceFlag;
std::call_once(onceFlag, [] {
availableMemory = computeAvailableMemory();
});
return availableMemory;
}
availableMemory()
办法中的 availableMemory
只会核算一次,由 computeAvailableMemory()
核算而来。
static constexpr size_t availableMemoryGuess = 512 * bmalloc::MB;
static size_t computeAvailableMemory()
{
#if BOS(DARWIN)
size_t sizeAccordingToKernel = memorySizeAccordingToKernel();
#if BPLATFORM(IOS_FAMILY)
sizeAccordingToKernel = std::min(sizeAccordingToKernel, jetsamLimit());
#endif
size_t multiple = 128 * bmalloc::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.
return ((sizeAccordingToKernel + multiple - 1) / multiple) * multiple;
#elif BOS(FREEBSD) || BOS(LINUX)
//...
#elif BOS(UNIX)
//...
#else
return availableMemoryGuess;
#endif
}
在 computeAvailableMemory()
办法中,
- 先经过
memorySizeAccordingToKernel()
获取内核的内存大小; - 假如是 iOS 体系,再获取 jetsam 的约束:
jetsamLimit()
,在内存大小和 jetsamLimit() 中取较小的值; - 将成果向上取整为 128M 的倍数。
所以,此处的成果依赖于 memorySizeAccordingToKernel()
和 jetsamLimit()
。
先看 memorySizeAccordingToKernel()
的完成:
#if BOS(DARWIN)
static size_t memorySizeAccordingToKernel()
{
#if BPLATFORM(IOS_FAMILY_SIMULATOR)
BUNUSED_PARAM(availableMemoryGuess);
// Pretend we have 1024MB of memory to make cache sizes behave like on device.
return 1024 * bmalloc::MB;
#else
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);
#endif
}
#endif
逻辑为:
- 假如是模拟器,则内存设定为 1024M。
- 假如是实在设备,则经过
host_info
获取结构体为host_basic_info_data_t
的信息,读取max_mem
的数值,然后与std::numeric_limits<size_t>::max()
进行比较,取其间较小的值。其间std::numeric_limits<size_t>::max()
为当时设备能够表明的最大值。 - 核算失利时,回来 availableMemoryGuess,即 512 M。
再来看jetsamLimit()
的完成:
#if BPLATFORM(IOS_FAMILY)
static size_t 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 * bmalloc::MB;
if (properties.memlimit_active < 0)
return std::numeric_limits<size_t>::max();
return static_cast<size_t>(properties.memlimit_active) * bmalloc::MB;
}
#endif
在 jetsamLimit()
中,
- 经过
memorystatus_control()
获取结构体为memorystatus_memlimit_properties_t
的信息,回来值不为 0,则回来 840M; - 假如获取的 memoryStatus 的约束属性 memlimit_active 小于 0 时,则回来当时设备能够表明的最大值;
- 假如运转正常,则回来体系回来的数值。
至此,就看到了 ramSize()
的整个核算过程。
总结一下内存最大约束的核算办法:
- 判别线程当时的状况:
- 激活状况
- 核算 ramSize();
- 核算内核的内存大小和 jetsam 的约束,取较小值,
- 向上取整为 128M 的倍数。
- 核算
baseThreshold = ramSize() > 16 * GB ? 15 * GB : 7 * GB;
- 最终成果为:baseThreshold + tabCount * GB;
- 核算 ramSize();
- 非激活状况:
- 在
CPU(X86_64) || CPU(ARM64)
下,baseThreshold = 3 * GB + tabCount * GB;
,不然baseThreshold = tabCount > 1 ? 3 * GB : 2 * GB;
; - 最终成果为:
smin(baseThreshold, (ramSize() * 0.9))
;
- 在
- 激活状况
内存超限的处理
内存超限之后,就会调用 shrinkOrDie()
,界说如下:
void MemoryPressureHandler::shrinkOrDie(size_t killThreshold)
{
RELEASE_LOG(MemoryPressure, "Process is above the memory kill threshold. Trying to shrink down.");
releaseMemory(Critical::Yes, Synchronous::Yes);
size_t footprint = memoryFootprint();
RELEASE_LOG(MemoryPressure, "New memory footprint: %zu MB", footprint / MB);
if (footprint < killThreshold) {
RELEASE_LOG(MemoryPressure, "Shrank below memory kill threshold. Process gets to live.");
setMemoryUsagePolicyBasedOnFootprint(footprint);
return;
}
WTFLogAlways("Unable to shrink memory footprint of process (%zu MB) below the kill thresold (%zu MB). Killed\n", footprint / MB, killThreshold / MB);
RELEASE_ASSERT(m_memoryKillCallback);
m_memoryKillCallback();
}
其间,m_memoryKillCallback
便是在初始化 web 线程时设置的回调。
因为 OOM 导致 reload/白屏,看起来并不是iOS的机制。从办法的调用联系进行全局检索,目前发现内存超出导致的白屏只要这么一条调用链。
OOM 之后的默许处理流程
苹果对 WebContentProcessDidTerminate 的处理逻辑如下:
void WebPageProxy::dispatchProcessDidTerminate(ProcessTerminationReason reason)
{
bool handledByClient = false;
if (m_loaderClient)
handledByClient = reason != ProcessTerminationReason::RequestedByClient && m_loaderClient->processDidCrash(***this**);
else
handledByClient = m_navigationClient->processDidTerminate(*this, reason);
if (!handledByClient && shouldReloadAfterProcessTermination(reason)) {
// We delay the view reload until it becomes visible.
if (isViewVisible())
tryReloadAfterProcessTermination();
else {
WEBPAGEPROXY_RELEASE_LOG_ERROR(Loading, "dispatchProcessDidTerminate: Not eagerly reloading the view because it is not currently visible");
m_shouldReloadDueToCrashWhenVisible = true;
}
}
}
其间 m_loaderClient 只在苹果的单元测试中有运用,所以,正式版本的 iOS 下应该会履行:
handledByClient = m_navigationClient->processDidTerminate(*this, reason);
假如开发者未完成 webViewWebContentProcessDidTerminate(_)
的署理办法,将回来 false,进入苹果的默许处理逻辑:经过 shouldReloadAfterProcessTermination()
判别是否需求进行从头加载,假如需求则在恰当时候进行从头加载。
shouldReloadAfterProcessTermination()
依据停止原因来判别是否需求进行从头加载:
static bool shouldReloadAfterProcessTermination(ProcessTerminationReason reason)
{
switch (reason) {
case ProcessTerminationReason::ExceededMemoryLimit:
case ProcessTerminationReason::ExceededCPULimit:
case ProcessTerminationReason::RequestedByNetworkProcess:
case ProcessTerminationReason::RequestedByGPUProcess:
case ProcessTerminationReason::Crash:
case ProcessTerminationReason::Unresponsive:
return true;
case ProcessTerminationReason::ExceededProcessCountLimit:
case ProcessTerminationReason::NavigationSwap:
case ProcessTerminationReason::IdleExit:
case ProcessTerminationReason::RequestedByClient:
break;
}
return false;
}
tryReloadAfterProcessTermination()
的改写逻辑如下:
static unsigned maximumWebProcessRelaunchAttempts = 1;
void WebPageProxy::tryReloadAfterProcessTermination()
{
m_resetRecentCrashCountTimer.stop();
if (++m_recentCrashCount > maximumWebProcessRelaunchAttempts) {
WEBPAGEPROXY_RELEASE_LOG_ERROR(Process, "tryReloadAfterProcessTermination: process crashed and the client did not handle it, not reloading the page because we reached the maximum number of attempts");
m_recentCrashCount = 0;
return;
}
WEBPAGEPROXY_RELEASE_LOG(Process, "tryReloadAfterProcessTermination: process crashed and the client did not handle it, reloading the page");
reload(ReloadOption::ExpiredOnly);
}
每次 crash 后,苹果会给 crash 标识(m_recentCrashCount)进行 +1,在不超越最大约束(maximumWebProcessRelaunchAttempts = 1)时,体系会进行改写,当最近 crash 的次数超越约束时,它便不会改写,仅仅将标明归位为0,下次就能够改写。
总结一下:假如开发者未完成 webViewWebContentProcessDidTerminate(_)
的署理办法:
- 则依据 crash 的原因判别是否要从头改写;
- 从头改写有最大次数约束(一次),超越则不会进行改写。
后记:咱们在iOS的Safari上测试了safari的白屏处理逻辑,当第一次发生白屏时Safari会默许重刷,第2次时safari会展示错误加载页,提示当时页面屡次发生了错误。这个逻辑和上面webkit的默许处理逻辑时类似的。
- 摘自:www.twblogs.net/a/5cfe4bdfb…
至此,就总结了 WKWebView 检测内存的办法,核算最大内存约束的办法和默许的处理办法。
参考
- developer.apple.com/forums/thre…
- www.twblogs.net/a/5cfe4bdfb…
- justinyan.me/post/3982
- www.jianshu.com/p/22a077fd5…