白屏是webview进程终止之后的表现,在webview因异常使用内存、CPU等资源时,webkit会终止当前m页展示的进程,在用户端表现为白页。

第一、webview的进程被终止的原因有哪些?

ProcessTerminationReason {ExceededMemoryLimit,//超出内存限制ExceededCPULimit,//超出CPU限制RequestedByClient,//主动触发的terminateCrash,//web进程自己发生了crashNavigationSwap,//m页的加载环境出现了变化
};

第二、内存限制是如何计算的?

1、苹果是如何计算当前内存的使用量的?

主要是通过计算当前进程的phys_footprint来度量内存的使用量

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);
}}

2、内存使用过量的阈值是如何计算的?

内存超出的计算依赖了两个值:基于CPU框架的基本阈值baseThreshold和手机内存大小ramSize来计算阈值

static size_t thresholdForMemoryKillWithProcessState(WebsamProcessState processState, unsigned tabCount)
{size_t baseThreshold = 2 * GB;
#if CPU(X86_64) || CPU(ARM64)if (processState == WebsamProcessState::Active)baseThreshold = 4 * GB;if (tabCount > 1)baseThreshold += std::min(tabCount - 1, 4u) * 1 * GB;
#elseif ((tabCount > 1) || (processState == WebsamProcessState::Active))baseThreshold = 3 * GB;
#endifreturn std::min(baseThreshold, static_cast<size_t>(ramSize() * 0.9));
}

这里可以看出,baseThreshold初始化为2GB,64位CPU初始化位4GB,多tab时每个tab可以多加1GB(总计不超过4GB)。非64位CPU在多tab的情况下最多为3GB。

再来看ramSize大小的计算方法,因为webkit是一个多平台使用的框架,这里计算ramsize,我们只分析iOS系统的计算方法:

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

这是个单例,只运算一次。而computeRAMSize()的实现如下:

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 defined(USE_SYSTEM_MALLOC) && USE_SYSTEM_MALLOC
#if OS(UNIX)struct sysinfo si;sysinfo(&si);return si.totalram * si.mem_unit;
#else
#error "Missing a platform specific way of determining the available RAM"
#endif // OS(UNIX)
#elsereturn bmalloc::api::availableMemory();
#endif
}

这里有windows系统和Unix系统以及其他三种分类,但是这里的Unix系统不包括iOS系统(虽然iOS是基于Unix的),我们的iOS系统在最后一个分支bmalloc::api::availableMemory()中。来看看该方法的实现:

//调用第一步
inline size_t availableMemory()
{return bmalloc::availableMemory();
}//调用第二步
size_t availableMemory()
{static size_t availableMemory;static std::once_flag onceFlag;std::call_once(onceFlag, [] {availableMemory = computeAvailableMemory();});return availableMemory;
}//调用第三步
static size_t computeAvailableMemory()
{
#if BOS(DARWIN)size_t sizeAccordingToKernel = memorySizeAccordingToKernel();
#if BPLATFORM(IOS_FAMILY)sizeAccordingToKernel = std::min(sizeAccordingToKernel, jetsamLimit());
#endifsize_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(UNIX)long pages = sysconf(_SC_PHYS_PAGES);long pageSize = sysconf(_SC_PAGE_SIZE);if (pages == -1 || pageSize == -1)return availableMemoryGuess;return pages * pageSize;
#elsereturn availableMemoryGuess;
#endif
}

第三步是计算设备ramsize的核心代码,这里sizeAccordingToKernel的计算与两个很重要的方法memorySizeAccordingToKernel()、jetsamLimit()相关联,前者是与当前设备内核相关的内存size,后者jetsamLimit是苹果对各个进程使用内存的限制,我们来分析一下。

static const size_t availableMemoryGuess = 512 * bmalloc::MB;#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;
#elsehost_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

这里有三个要点:

一是模拟器的内存size为1G;

二是对于真实设备,它主要计算结构体host_basic_info_data_t中max_mem的大小,然后取MIN(max_mem,std::numeric_limits<size_t>::max()),这里std::numeric_limits<size_t>::max()取当前设备可以表示的最大值;

三是上述计算失败时默认为512M。

再来看jatsamLimit的实现

#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

这个方法实现有三种可能

1、如果memorystatus_control返回值不为0时,其返回值为840M

2、如果memoryStatus的限制属性memlimit_active<0时,返回当前设备可以表示的最大值。

3、如果运算正常,则返回系统的取值。

这里jetsam的相关运算对我们开发者来说是黑盒无文档的,有兴趣的同学可以通过下面这篇文章了解一下

(译)Handling low memory conditions in iOS and Mavericks - 掘金

至此,我们可以获取iOS设备的ramsize了

#if BPLATFORM(IOS_FAMILY)sizeAccordingToKernel = std::min(sizeAccordingToKernel, jetsamLimit());
#endifsize_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;

那么进程被kill的内存上限为:

return std::min(baseThreshold, static_cast<size_t>(ramSize() * 0.9));

第三、内存使用过度时白屏发生的具体流程是什么?

发生白屏的基本流程如下图所示

当我们给webview设置了navigationDelegate时,这里的delegate方法将由NavigationState这个类来触发。

MemoryPressureHandler是一个单例,当它判断当前内存使用过量时会触发WebProcess的memoryKillCallBack,此回调是在initializeWebProcess进行注册的,WebProcess通过IPC机制将消息转发给WebProcessProxy,WebProcessProxy调用requestTermination方法,然后调用了processDidTerminate--->dispatchProcessDidTerminate,这里才会将ProcessDidTerminate的处理交给用户。

这里需要说明一下,memoryKillCallback以及设置每隔30s检查内存,Apple的实现中都用宏包裹了,

#if (PLATFORM(MAC) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 101200) || PLATFORM(GTK) || PLATFORM(WPE)memoryPressureHandler.setShouldUsePeriodicMemoryMonitor(true);memoryPressureHandler.setMemoryKillCallback([this] () {WebCore::logMemoryStatisticsAtTimeOfDeath();if (MemoryPressureHandler::singleton().processState() == WebsamProcessState::Active)parentProcessConnection()->send(Messages::WebProcessProxy::DidExceedActiveMemoryLimit(), 0);elseparentProcessConnection()->send(Messages::WebProcessProxy::DidExceedInactiveMemoryLimit(), 0);});memoryPressureHandler.setDidExceedInactiveLimitWhileActiveCallback([this] () {parentProcessConnection()->send(Messages::WebProcessProxy::DidExceedInactiveMemoryLimitWhileActive(), 0);});
#endif

看起来并不是iOS的机制,但是我们从方法的调用关系进行了全局检索,目前发现内存超出导致的白屏只有这么一条调用链。

第四、如果开发者不实现webViewWebContentProcessDidTerminate:(WKWebView *)webView的代理方法,默认的处理是什么?

苹果对WebContentProcessDidTerminate的处理逻辑如下:

void WebPageProxy::dispatchProcessDidTerminate(ProcessTerminationReason reason)
{bool handledByClient = false;if (m_loaderClient)handledByClient = reason != ProcessTerminationReason::RequestedByClient && m_loaderClient->processDidCrash(*this);elsehandledByClient = m_navigationClient->processDidTerminate(*this, reason);if (!handledByClient && shouldReloadAfterProcessTermination(reason))tryReloadAfterProcessTermination();
}

这里的m_loaderClient只在苹果的单元测试中有使用,所以,正式版本的iOS下应该会执行

handledByClient = m_navigationClient->processDidTerminate(*this, reason);

这里如果开发者未实现webViewWebContentProcessDidTerminate的代理方法,它将返回false,进入苹果的默认逻辑:

static bool shouldReloadAfterProcessTermination(ProcessTerminationReason reason)
{switch (reason) {case ProcessTerminationReason::ExceededMemoryLimit:case ProcessTerminationReason::ExceededCPULimit:case ProcessTerminationReason::Crash:return true;case ProcessTerminationReason::NavigationSwap:case ProcessTerminationReason::RequestedByClient:break;}return false;
}

从这个方法可以看出,苹果认为在内存超出、CPU超出、以及发生了Crash的场景下需要重新刷新。这里有两点需要注意:

第一、重新刷新是苹果官方的默认处理webViewWebContentProcessDidTerminate的方法;

第二、重新刷新是有条件的。

再来看具体的刷新逻辑:

static unsigned maximumWebProcessRelaunchAttempts = 1;void WebPageProxy::tryReloadAfterProcessTermination()
{m_resetRecentCrashCountTimer.stop();if (++m_recentCrashCount > maximumWebProcessRelaunchAttempts) {RELEASE_LOG_IF_ALLOWED(Process, "%p - WebPageProxy's process crashed and the client did not handle it, not reloading the page because we reached the maximum number of attempts", this);m_recentCrashCount = 0;return;}RELEASE_LOG_IF_ALLOWED(Process, "%p - WebPageProxy's process crashed and the client did not handle it, reloading the page", this);reload(ReloadOption::ExpiredOnly);
}

这里每次crash时苹果会给crash标示+1,在标示不超过1时(其实只有m_recentCrashCount为0时),系统会进行刷新,当最近crash的次数超过一次时它便不会刷新,只是将标示归位为0,下次就可以刷新。

后记:我们在iOS的Safari上测试了safari的白屏处理逻辑,当第一次发生白屏时Safari会默认重刷,第二次时safari会展示错误加载页,提示当前页面多次发生了错误。这个逻辑和上面webkit的默认处理逻辑时相似的。

第五、CPU使用过量的白屏是如何触发的?

很遗憾,截止目前,苹果在iOS下没有监控CPU,只有mac下会监控。我们可以从mac的逻辑中窥探一二。

#if PLATFORM(MAC)std::unique_ptr<WebCore::CPUMonitor> m_cpuMonitor;std::optional<double> m_cpuLimit;
#endifvoid WebProcess::updateCPUMonitorState(CPUMonitorUpdateReason reason)
{
#if PLATFORM(MAC)if (!m_cpuLimit) {if (m_cpuMonitor)m_cpuMonitor->setCPULimit(std::nullopt);return;}if (!m_cpuMonitor) {m_cpuMonitor = std::make_unique<CPUMonitor>(cpuMonitoringInterval, [this](double cpuUsage) {RELEASE_LOG(PerformanceLogging, "%p - WebProcess exceeded CPU limit of %.1f%% (was using %.1f%%) hasVisiblePages? %d", this, m_cpuLimit.value() * 100, cpuUsage * 100, hasVisibleWebPage());parentProcessConnection()->send(Messages::WebProcessProxy::DidExceedCPULimit(), 0);});} else if (reason == CPUMonitorUpdateReason::VisibilityHasChanged) {// If the visibility has changed, stop the CPU monitor before setting its limit. This is needed because the CPU usage can vary wildly based on visibility and we would// not want to report that a process has exceeded its background CPU limit even though most of the CPU time was used while the process was visible.m_cpuMonitor->setCPULimit(std::nullopt);}m_cpuMonitor->setCPULimit(m_cpuLimit.value());
#elseUNUSED_PARAM(reason);
#endif
}

这里有两个成员变量:m_cpuMonitor和m_cpuLimit,这两个成员变量只在mac下定义。m_cpuMonitor是一个unique的智能指针,它初始化时需要添加一个闭包来响应超过cpu使用量时的事件,这里的响应行为不难发现时向WebProcessProxy发送消息,WebProcessProxy处理逻辑如下:

void WebProcessProxy::didExceedCPULimit()
{for (auto& page : pages()) {if (page->isPlayingAudio()) {RELEASE_LOG(PerformanceLogging, "%p - WebProcessProxy::didExceedCPULimit() WebProcess with pid %d has exceeded the background CPU limit but we are not terminating it because there is audio playing", this, processIdentifier());return;}if (page->hasActiveAudioStream() || page->hasActiveVideoStream()) {RELEASE_LOG(PerformanceLogging, "%p - WebProcessProxy::didExceedCPULimit() WebProcess with pid %d has exceeded the background CPU limit but we are not terminating it because it is capturing audio / video", this, processIdentifier());return;}}bool hasVisiblePage = false;for (auto& page : pages()) {if (page->isViewVisible()) {page->didExceedBackgroundCPULimitWhileInForeground();hasVisiblePage = true;}}// We only notify the client that the process exceeded the CPU limit when it is visible, we do not terminate it.if (hasVisiblePage)return;RELEASE_LOG_ERROR(PerformanceLogging, "%p - WebProcessProxy::didExceedCPULimit() Terminating background WebProcess with pid %d that has exceeded the background CPU limit", this, processIdentifier());logDiagnosticMessageForResourceLimitTermination(DiagnosticLoggingKeys::exceededBackgroundCPULimitKey());requestTermination(ProcessTerminationReason::ExceededCPULimit);
}

可以看出:

第一、当前的web如果有窗口在播放音视频,则不做响应;

第二、当前web如果存在可见页面时不做响应;

第三、调用了requestTermination方法,和超过内存限制情形下进行同样的处理。

另:我们在WebKit的源码中没有看到Mac下m_cpuLimit的值的大小。

第六、ProcessTerminationReason为Crash发生在哪些场景下?

整个WebKit源码中只有一处调用了Crash的情况:

void WebProcessProxy::processDidTerminateOrFailedToLaunch()
{// Protect ourselves, as the call to disconnect() below may otherwise cause us// to be deleted before we can finish our work.Ref<WebProcessProxy> protect(*this);if (auto* webConnection = this->webConnection())webConnection->didClose();auto pages = copyToVectorOf<RefPtr<WebPageProxy>>(m_pageMap.values());shutDown();#if ENABLE(PUBLIC_SUFFIX_LIST)if (pages.size() == 1) {auto& page = *pages[0];String domain = topPrivatelyControlledDomain(WebCore::URL({ }, page.currentURL()).host().toString());if (!domain.isEmpty())page.logDiagnosticMessageWithEnhancedPrivacy(WebCore::DiagnosticLoggingKeys::domainCausingCrashKey(), domain, WebCore::ShouldSample::No);}
#endiffor (auto& page : pages)page->processDidTerminate(ProcessTerminationReason::Crash);
}

这里,当WebProcessProxy的processDidTerminateOrFailedToLaunch方法被调用时,它会发送消息给WebPageProxy,后续的处理和内存过度使用是一致的。现在我们来看processDidTerminateOrFailedToLaunch被调用的情况:

第一种调用情况:当前的IPC连接标示不合法时会触发

void WebProcessProxy::didFinishLaunching(ProcessLauncher* launcher, IPC::Connection::Identifier connectionIdentifier)
{RELEASE_ASSERT(isMainThreadOrCheckDisabled());ChildProcessProxy::didFinishLaunching(launcher, connectionIdentifier);if (!IPC::Connection::identifierIsValid(connectionIdentifier)) {RELEASE_LOG_IF(m_websiteDataStore->sessionID().isAlwaysOnLoggingAllowed(), Process, "%p - WebProcessProxy didFinishLaunching - invalid connection identifier (web process failed to launch)", this);processDidTerminateOrFailedToLaunch();return;}...
}

这里连接标示的合法性由mach_port来完成

#define MACH_PORT_VALID(name)                \(((name) != MACH_PORT_NULL) &&        \((name) != MACH_PORT_DEAD))static bool identifierIsValid(Identifier identifier) { return MACH_PORT_VALID(identifier.port); }

主要是验证标示的端口是否合法。

第二种情况:

当WebProcessProxy的didClose方法调用时会触发

void WebProcessProxy::didClose(IPC::Connection&)
{RELEASE_LOG_IF(m_websiteDataStore->sessionID().isAlwaysOnLoggingAllowed(), Process, "%p - WebProcessProxy didClose (web process crash)", this);processDidTerminateOrFailedToLaunch();
}void WebProcessProxy::didReceiveInvalidMessage(IPC::Connection& connection, IPC::StringReference messageReceiverName, IPC::StringReference messageName)
{WTFLogAlways("Received an invalid message \"%s.%s\" from the web process.\n", messageReceiverName.toString().data(), messageName.toString().data());WebProcessPool::didReceiveInvalidMessage(messageReceiverName, messageName);// Terminate the WebProcess.terminate();// Since we've invalidated the connection we'll never get a IPC::Connection::Client::didClose// callback so we'll explicitly call it here instead.didClose(connection);
}

didClose则是进程间通信时收到不合法的消息会触发。所以,综上:

Crash类型的进程终止一般都发生在进程通信机制中,当进程连接的端口不合法或者通信时发送的消息不合法时,则会被认为是发生了Crash,进程会终止。

第七、RequestedByClient类型的进程终止发生的场景有哪些?

这种类型进程终止在两个类中有体现:

WKWebView.mm:

- (void)_killWebContentProcessAndResetState
{Ref<WebKit::WebProcessProxy> protectedProcessProxy(_page->process());protectedProcessProxy->requestTermination(WebKit::ProcessTerminationReason::RequestedByClient);
}

WKPage.cpp

void WKPageTerminate(WKPageRef pageRef)
{Ref<WebProcessProxy> protectedProcessProxy(toImpl(pageRef)->process());protectedProcessProxy->requestTermination(ProcessTerminationReason::RequestedByClient);
}

这两个方法的调用全部在Webkit的单元测试中,因此开发者无法调用,属于WebKit的“特权”范畴,目前看它们的是提供给Apple内部来使用,而使用的时机和具体的业务场景没有强关联。

第八、NavigationSwap发生在什么场景下?

这里需要知道:WebKit有个进程池(ProcessPool),进程池中包含多个WebProcessProxy,页面每次加载时WebProcessProxy会去ProcessPool里面再次获取当前页面可能对应的WebProcessProxy,如果当前的WebProcessProxy与获取到的WebProcessProxy不是同一个时,则说明页面加载的进程发生了变化,此时需要交互页面加载的进程

void WebPageProxy::receivedNavigationPolicyDecision(PolicyAction policyAction, API::Navigation* navigation, ProcessSwapRequestedByClient processSwapRequestedByClient, WebFrameProxy& frame, API::WebsitePolicies* policies, Ref<PolicyDecisionSender>&& sender)
{........auto proposedProcess = process().processPool().processForNavigation(*this, *navigation, processSwapRequestedByClient, policyAction, reason);ASSERT(!reason.isNull());if (proposedProcess.ptr() != &process()) {RELEASE_LOG_IF_ALLOWED(ProcessSwapping, "%p - WebPageProxy::decidePolicyForNavigationAction, swapping process %i with process %i for navigation, reason: %{public}s", this, processIdentifier(), proposedProcess->processIdentifier(), reason.utf8().data());LOG(ProcessSwapping, "(ProcessSwapping) Switching from process %i to new process (%i) for navigation %" PRIu64 " '%s'", processIdentifier(), proposedProcess->processIdentifier(), navigation->navigationID(), navigation->loggingString());RunLoop::main().dispatch([this, protectedThis = makeRef(*this), navigation = makeRef(*navigation), proposedProcess = WTFMove(proposedProcess)]() mutable {continueNavigationInNewProcess(navigation, WTFMove(proposedProcess));});........}

而在continueNavigationInNewProcess处理中,WebKit先触发了processDidTerminate,然后做了“交换进程的操作”:

void WebPageProxy::continueNavigationInNewProcess(API::Navigation& navigation, Ref<WebProcessProxy>&& process)
{......processDidTerminate(ProcessTerminationReason::NavigationSwap);swapToWebProcess(WTFMove(process), navigation, mainFrameIDInPreviousProcess);......
}

这里的processDidTerminate就进入了与处理过度使用内存类似的逻辑了,But!!!!这种情况不会出现白屏,也不会回调给用户,我们再来看上述方法的实现:

void WebPageProxy::processDidTerminate(ProcessTerminationReason reason)
{if (reason != ProcessTerminationReason::NavigationSwap)RELEASE_LOG_IF_ALLOWED(Process, "%p - WebPageProxy::processDidTerminate (pid %d), reason %d", this, processIdentifier(), reason);ASSERT(m_isValid);#if PLATFORM(IOS_FAMILY)if (m_process->isUnderMemoryPressure()) {String domain = WebCore::topPrivatelyControlledDomain(WebCore::URL({ }, currentURL()).host().toString());if (!domain.isEmpty())logDiagnosticMessageWithEnhancedPrivacy(WebCore::DiagnosticLoggingKeys::domainCausingJetsamKey(), domain, WebCore::ShouldSample::No);}
#endif// There is a nested transaction in resetStateAfterProcessExited() that we don't want to commit before the client call.PageLoadState::Transaction transaction = m_pageLoadState.transaction();resetStateAfterProcessExited(reason);// For bringup of process swapping, NavigationSwap termination will not go out to clients.// If it does *during* process swapping, and the client triggers a reload, that causes bizarre WebKit re-entry.// FIXME: This might have to changeif (reason == ProcessTerminationReason::NavigationSwap)m_webProcessLifetimeTracker.webPageLeavingWebProcess();else {navigationState().clearAllNavigations();dispatchProcessDidTerminate(reason);}......

首先, WebPageProxy::processDidTerminate触发的log都避开了ProcessTerminationReason::NavigationSwap的情况;

其次,当reason为ProcessTerminationReason::NavigationSwap时,并不会执行dispatchProcessDidTerminate,而后者是回调真正处理白屏的逻辑。

所以,本质上这种情形的发生就是为了清空当前webProcessProxy的各种状态,为后续进程交换做好准备而已。

第九、小结

通过上述分析,我们可以知道,iOS设备上造成白屏的真正原因只有两个:

内存使用过度

进程通信机制出现错误

第十、目前主流app对白屏的处理方式

主流浏览器对白屏的处理方式
浏览器名称 白屏的处理方式
微信 每次发生时均会重新刷新
QQ 每次发生时均会重新刷新
Safari 第一次会刷新,并给出提示,第二次直接加载错误页,如下图1
京东 每次重刷或移除当前页面
微博 每次重新刷新
Chrome 直接展示错误页面,如下图2

safari对白屏处理的效果图:

图1 Safari发生白屏后的处理效果图

chrome的白屏处理效果:

图2 Chrome的白屏处理示意图

全篇完!

深入理解WKWebView白屏相关推荐

  1. WKWebView白屏问题

    WKWebView白屏问题 WKWebView自诩拥有更多的加载速度,更低的内存占用,但实际上WKWebView是一个多进程组件,Network Loading以及UI Rendering在其他进程中 ...

  2. WKWebView 白屏问题

    1.WKWebView 自诩拥有更快的加载速度,更低的内存占用,但实际上 WKWebView 是一个多进程组件,Network Loading 以及 UI Rendering 在其它进程中执行.初次适 ...

  3. webview加载页面有2秒白屏_iOS WKWebview 白屏检测实现

    前言 自ios8推出wkwebview以来,极大改善了网页加载速度及内存泄漏问题,逐渐全面取代笨重的UIWebview.尽管高性能.高刷新的WKWebview在混合开发中大放异彩表现优异,但加载网页过 ...

  4. iOS开发~WKWebView白屏适配

    WKWebView虽好,但白屏问题也很苦恼,下面分享一下自己解决问题的过程. 公司项目使用Cordova框架,做原生项目嵌入H5,业务复杂了以后,H5资源也越来越大,占用内存越来越多,加载也越来越慢, ...

  5. WKwebview 白屏问题——(WebApp/HybirdApp)

    我们app从ReactNative转H5app,在开发过程中发现一个顽固性问题.点击H5的tabbar或者页面之间点击跳转会偶现白屏问题.或者app退到后台一段时间唤醒app会出现白屏. 白屏原因: ...

  6. iOS WKWebView白屏检测演进方案记录

    网上查到的方案 typedef NS_ENUM(NSUInteger,webviewLoadingStatus) {WebViewNormalStatus = 0, //正常WebViewErrorS ...

  7. 前端白屏问题_小程序白屏问题和内存解决方法

    1 关于WKWebview白屏,网上罗列的常见原因大致有以下几种: 1.内存占用比较大时,WebContent Process 会 crash,从而出现白屏现象. 2.URL网址无效或者含有中文字符. ...

  8. uniapp 云打包后IOS白屏,真机调试也是白屏,没有报错!解决办法

    uniapp 云打包后IOS白屏,真机调试也是白屏,没有报错!解决办法! 原来用uniapp 写的小程序 然后要翻成app 一开始还是很顺利的,因为安卓端测试 没有什么大问题:但是IOS直接白屏,只能 ...

  9. wkwebview 在iOS10以下系统显示白屏问题

    打了测试环境的包,在iOS11系统上任何wkwebview界面显示都没有什么问题,但是到了iOS10的系统上,或者iOS9的系统上都显示白屏,遇到不配和你调试的H5,你想把他撕吧撕吧喂鹰的心都有了,但 ...

最新文章

  1. Backnbone的入门基础——Backbone的model
  2. rsync远程数据同步工具的使用
  3. android 显示进度的按钮
  4. 利用solr实现商品的搜索功能
  5. springmvc的相关配置文件
  6. python image 转成字节_就是这么牛!三行Python代码,让数据处理速度提高2到6倍
  7. 微信收款音响s3服务器断开,微信收款音响s2和s3有什么区别
  8. Kettle之数据抽取、转换、装载
  9. nginx 配置后网站图片加载出来一半或者不出来
  10. Java学习笔记(13)——Java注释
  11. 金三银四的面试黄金季节,Android面试题来了!
  12. execjs._exceptions.ProgramError: TypeError: ‘JSON‘ 未定义
  13. 动态下拉框中如何使用常量?
  14. 添加到当前最上层view
  15. wordpress更新主题时,显示无法连接到FTP服务器的问题解决
  16. cygwin--简单备忘
  17. 数据库DevOps:我们如何提供安全、稳定、高效的研发全自助数据库服务-iDB/DMS企业版
  18. Python 3 集合方法 copy( )
  19. JavaScript常用事件及其区别
  20. 答题小程序源码功能升级啦

热门文章

  1. 《关于TCP SYN包的超时与重传》——那些你应该知道的知识(四)
  2. C++ 封装 信息隐藏
  3. 如何零基础制作一款自己的游戏!(一)
  4. java算法优化_Java学习笔记---Java简单的代码算法优化(例)
  5. error pulling image configuration:XXX net/http: TLS handshake timeout
  6. Linux基础----文件管理、用户管理、用户权限
  7. 多目标应用:多目标蜣螂优化算法求解多旅行商问题(Multiple Traveling Salesman Problem, MTSP)
  8. 【FastGAN】★Towards Faster and Stabilized GAN Training for High-fidelity Few-shot Image Synthesis
  9. python中最小公倍数函数_python求最大公约数和最小公倍数的简单方法
  10. matlab取点坐标之前先将图片放大缩小