一些有趣的问题(3)——Safari(WebKit)是如何处理history.pushState的?
Safari是闭源的,但是内核WebKit是开元的,因此还是有机会能看到Safari处理history.pushState的原始风貌。接下来看看。
与Chromium的Blink处理方式不同,WebKit对pushState的处理函数是JSHistory::pushState。(webkit\Source\WebCore\bindings\js\JSHistoryCustom.cpp)
JSValue JSHistory::pushState(ExecState& state)
{
RefPtr<SerializedScriptValue> historyState = SerializedScriptValue::create(&state, state.argument(0), 0, 0);
if (state.hadException())
return jsUndefined();
String title = valueToStringWithUndefinedOrNullCheck(&state, state.argument(1));
if (state.hadException())
return jsUndefined();
String url;
if (state.argumentCount() > 2) {
url = valueToStringWithUndefinedOrNullCheck(&state, state.argument(2)); //取出了Url部分
if (state.hadException())
return jsUndefined();
}
ExceptionCodeWithMessage ec;
wrapped().stateObjectAdded(historyState.release(), title, url, History::StateObjectType::Push, ec);
setDOMException(&state, ec);
m_state.clear();
return jsUndefined();
}
对每个有效的pushState请求,函数都会调用stateObjectAdded来将状态添加到HistroyList中。实现代码(webkit\Source\WebCore\page\History.cpp)如下:
void History::stateObjectAdded(PassRefPtr<SerializedScriptValue> data, const String& title, const String& urlString, StateObjectType stateObjectType, ExceptionCodeWithMessage& ec)
{
// Each unique main-frame document is only allowed to send 64mb of state object payload to the UI client/process.
static uint32_t totalStateObjectPayloadLimit = 0x4000000;
static double stateObjectTimeSpan = 30.0;
static unsigned perStateObjectTimeSpanLimit = 100;
if (!m_frame || !m_frame->page())
return;
URL fullURL = urlForState(urlString);
if (!fullURL.isValid() || !m_frame->document()->securityOrigin()->canRequest(fullURL)) {
ec.code = SECURITY_ERR;
return;
}
Document* mainDocument = m_frame->page()->mainFrame().document();
History* mainHistory = nullptr;
if (mainDocument) {
if (auto* mainDOMWindow = mainDocument->domWindow())
mainHistory = mainDOMWindow->history();
}
if (!mainHistory)
return;
double currentTimestamp = currentTime();
if (currentTimestamp - mainHistory->m_currentStateObjectTimeSpanStart > stateObjectTimeSpan) {
mainHistory->m_currentStateObjectTimeSpanStart = currentTimestamp;
mainHistory->m_currentStateObjectTimeSpanObjectsAdded = 0;
}
if (mainHistory->m_currentStateObjectTimeSpanObjectsAdded >= perStateObjectTimeSpanLimit) {
ec.code = SECURITY_ERR;
if (stateObjectType == StateObjectType::Replace)
ec.message = String::format("Attempt to use history.replaceState() more than %u times per %f seconds", perStateObjectTimeSpanLimit, stateObjectTimeSpan);
else
ec.message = String::format("Attempt to use history.pushState() more than %u times per %f seconds", perStateObjectTimeSpanLimit, stateObjectTimeSpan);
return;
}
Checked<unsigned> titleSize = title.length();
titleSize *= 2;
Checked<unsigned> urlSize = fullURL.string().length();
urlSize *= 2;
Checked<uint64_t> payloadSize = titleSize;
payloadSize += urlSize;
payloadSize += data ? data->data().size() : 0;
Checked<uint64_t> newTotalUsage = mainHistory->m_totalStateObjectUsage;
if (stateObjectType == StateObjectType::Replace)
newTotalUsage -= m_mostRecentStateObjectUsage;
newTotalUsage += payloadSize;
if (newTotalUsage > totalStateObjectPayloadLimit) {
ec.code = QUOTA_EXCEEDED_ERR;
if (stateObjectType == StateObjectType::Replace)
ec.message = ASCIILiteral("Attempt to store more data than allowed using history.replaceState()");
else
ec.message = ASCIILiteral("Attempt to store more data than allowed using history.pushState()");
return;
}
m_mostRecentStateObjectUsage = payloadSize.unsafeGet();
mainHistory->m_totalStateObjectUsage = newTotalUsage.unsafeGet();
++mainHistory->m_currentStateObjectTimeSpanObjectsAdded;
if (!urlString.isEmpty())
m_frame->document()->updateURLForPushOrReplaceState(fullURL);
if (stateObjectType == StateObjectType::Push) {
m_frame->loader().history().pushState(data, title, fullURL.string());
m_frame->loader().client().dispatchDidPushStateWithinPage();
} else if (stateObjectType == StateObjectType::Replace) {
m_frame->loader().history().replaceState(data, title, fullURL.string());
m_frame->loader().client().dispatchDidReplaceStateWithinPage();
}
}
代码很长,一行行阅读(不得不说,WebKit代码比Blink的好读多啦……),这个函数的逻辑如下:
1、对请求中URL部分进行拼接,结果不正确或跨域时,不予处理并抛出异常。
2、为了防止出现大量的请求,Webkit限制了30ms中最多有100条pushState记录,出现过多的记录将抛出异常
3、为了防止大量的内存占用,Webkit限制了最大状态数据为64MB,出现过多内存占用时,会抛出异常
4、增加计数,更新URL,更新history状态(通过HistoryItem)
更新状态的处理函数如下:
void HistoryController::pushState(PassRefPtr<SerializedScriptValue> stateObject, const String& title, const String& urlString)
{
if (!m_currentItem)
return;
Page* page = m_frame.page();
ASSERT(page);
// Get a HistoryItem tree for the current frame tree.
Ref<HistoryItem> topItem = m_frame.mainFrame().loader().history().createItemTree(m_frame, false);
// Override data in the current item (created by createItemTree) to reflect
// the pushState() arguments.
m_currentItem->setTitle(title);
m_currentItem->setStateObject(stateObject);
m_currentItem->setURLString(urlString);
LOG(History, "HistoryController %p pushState: Adding top item %p, setting url of current item %p to %s", this, topItem.ptr(), m_currentItem.get(), urlString.ascii().data());
page->backForward().addItem(WTFMove(topItem));
if (m_frame.page()->usesEphemeralSession())
return;
addVisitedLink(*page, URL(ParsedURLString, urlString));
m_frame.loader().client().updateGlobalHistory();
}
那么,问题来了,既然限制每个Tab History State Object最多64MB内存占用,30ms 100条添加,那么为什么还有系列《1》中那么蛋疼的内存占用呢?
答案就是——内存占用全部是消耗在了字符串上面。
如果我们修改一下payload如下:
var junkStr = "A";
for(var i = 0; i < 30000000; i++)
junkStr += "A"; //28M word
for(var i = 0; i < 30000000; i++)
history.pushState(0, 0, junkStr); //at least ~82GB if all pushed into history list
在Safari中就不会出现几十G内存占用的情况了。这会触发上述(3,4)保护,并抛出DOM异常,提前结束代码执行。