在活动(fevents/102205.html)中我收到了很多有意思的问题,比如:
打开 crashsafari.com 可以看到该网页的代码如下:
实际有用的内容也就是红框一段了。代码逻辑很简单,不断地 pushState,这个操作会不断向object History 中添加信息并立刻改变地址栏(但不导航)。在树莓派中添加下列页面:
使用 Safari 访问,发现 Safari 疯狂吃内存和 CPU,但是只是因为过于密集的代码执行使页面不响应用户输入,但是整体还是在运行的(我可以清晰地看到脚本计时器在连续读秒)。
但是我显然等不到让他执行完了,时间实在是太长了。让我们简单地计算一下,在执行完后,history 对象的 length 应该是多少?答案是大于等于 100002(命令行打开/直接打开等的情况下也有可能是 100001)。
就这么简单吗?当然不是,length 只是元素个数。每个 history 条目中还保存了 url 信息,因此,这是一个简单的等差数列的累计。假设 1 个字符占用 2 字节(wchar_t),我们的 base url 是 crashsafari.htm?一共 37 字节。
不考虑 Base url,从 i=0 起, 到 i=9 时,数字长度是 1,2,3,4……,10 到 i=99 时,数字长度是 12,14,16……,190 到 i=999 时,数字长度是 193,196,199,……,2890 到 i=9999 时,数字长度是 2894,2898,……,6494 到 i=99999 时,数字长度是 6499,6504,……,51499 到 i=100000 时,数字长度是 51505。
看起来不大吗?现在把它们累加起来。同时,把 37 字节也加进每个条目中,最终我们的到的数字是:
说了这么多,不如直接试一试 Windows 上的浏览器——IE 和 Edge。
在 IE11 上执行,一段时间后浏览器就出现了未响应的提示。但是内存占用却很少。这是为什么呢?从代码看,IE 对历史条目做了限制。数量是 500-1209(不同版本的 IE/Edge 有区别),但是对比 Chrome(100)和 Safari(50),IE 的数量明显要多,但是内存占用却最少,这是为什么呢?看来得再仔细看下它的机制了。
IE 中历史操作的接口为 IOmHistory(对应类 COmHistory),对应 F12 里面的 history 对象。
可以在 MSDN 查到它的相关信息:/en-us/library/hh774261(v=vs.85).aspx。
因为今天机器修好了,所以,我就以 Edge 为样本开始分析吧。jscript9.dll 中调用 pushState后,传给 CFastDOM::CHistory::Trampoline_pushState(PVOID, CallInfo*,PPVOID)去处理。trampoline 的代码十分简单,我简单人肉反编译一下,原始代码大致如下(为了避免各种 vtguard 代码,以下使用的是 64 位 EdgeHTML.dll):
反观这段代码,唯一“有效”(我们能看得见)的操作就是 SetStateHelper 了。这个函数里面,如果一切正常,Edge 会更新窗口数据,为文档添加浏览记录(CDoc::AddTravelEntery(pWindow, TRUE);)、更新后退状态(CDoc::UpdateBackForwardState(TRUE);)并更新了历史记录管理器中的 URL 信息(HRESULT hr = COmHistory::UpdateUrl(pWindow, (OLECHAR*)&pszNewURL);)。
但是事实确实如此吗?显然不是,当传入的 URL 大于 INTERNET_MAX_PATH_LENGTH (2048)字节时,COmHistory::PrepareUrlForUpdate 调用的MSHTML!CMarkup::ExpandUrl 中拼接 URL 的 API CoInternetCombineUrl 会返回0x80004003。
0x80004003 的含义是:E_POINTER | pwzResult is NULL, or the buffer is too small. 但这是为什么呢,这个 API 只是拼接 URL 而已,为什么会管 URL 长度?回头看MSHTML!CMarkup::ExpandUrl 调用该 API 时的代码,传入的 BUFFER 只有 2048 字节呀。
hr = CoInternetCombineUrl(pwzBaseUrl, pwzRelativeUrl, dwCombineFlags, pszResult,INTERNET_MAX_PATH_LENGTH, &pcchResult, 0);
所以后续插入、更新操作都没有了,顺便还调用了 ThrowDOMError,导致后面的代码都无法执行了(在 IE/Edge 中的体现是 SCRIPT16387:无效指针,就是 0x80004003 的解释)。这也就能解释为什么 Edge 为啥如此“高效”了。所以 Edge 实际上做的是:
占用 702425 字。
Chrome 和 Safari 的我会在接下来几期中发布。