一些有趣的问题(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异常,提前结束代码执行。

标签:none

添加新评论

captcha
请输入验证码