21

我们发现仅在 iOS 5/Safari 6(当时的 iPad 版本)上出现的 Javascript 代码解释存在严重问题,我们认为这是由于 Safari 中的 Just in Time JS 编译器中的严重错误造成的。(有关更多受影响的版本和现在似乎包含修复的版本,请参阅下面的更新)。

我们最初在我们库的在线演示中发现了这个问题:演示或多或少随机崩溃,但这只会在第二次(甚至更晚)执行相同的代码时发生。即,如果您运行该部分代码一次,一切正常,但随后的运行会使应用程序崩溃。

有趣的是,在 iOS 版 Chrome 中执行相同的代码时,问题并没有显示出来,我们认为这是由于 iOS 版 Chrome 中使用的 Webview 缺少 JIT 功能。

经过大量的摆弄,我们终于认为我们至少找到了一段有问题的代码:

  var a = 0; // counter for index
  for (var b = this.getStart(); b !== null; b = b.getNext()) // iterate over all cells
    b.$f = a++; // assign index to cell and then increment 

本质上,这是一个简单的 for 循环,它为链表数据结构中的每个单元分配其索引。这里的问题是循环体中的后增量操作。当前计数分配给字段并在计算表达式后更新,与首先分配 a 然后将其递增 1 基本相同。

这在我们测试的所有浏览器和前几次 Safari 中都可以正常工作,然后突然之间似乎计数器变量 a 先递增然后分配结果,就像预递增操作一样。

我创建了一个在这里显示问题的小提琴:http: //jsfiddle.net/yGuy/L6t5G/

在带有 iOS 6 的 iPad 2 上运行示例并且所有更新结果都可以,在我的情况下,前 2 次运行结果正常,在第三次相同的运行中,列表中的最后一个元素突然分配了一个值,该值被 1 关闭(输出当您单击“单击我”按钮时,从“从 0 到 500”变为“从 0 到 501”)

有趣的是,如果您切换选项卡或稍等片刻,可能会突然出现两次或更多运行的结果是正确的!似乎 Safari 有时会重置 JIT 缓存。

因此,由于我认为 Safari 团队可能需要很长时间才能修复此错误(我尚未报告),并且可能还有其他类似的错误潜伏在 JIT 中,同样难以找到,我想知道是否有办法在 Safari 中禁用 JIT 功能。当然,这会减慢我们的代码(这已经是 CPU 密集型的),但比崩溃要慢。

更新:毫不奇怪,受影响的不仅是后递增运算符,还有后减运算符。不那么令人惊讶和更令人担忧的是,如果值被赋值并没有什么区别,因此在现有代码中寻找赋值是不够的。例如,下面的代码b.$f = (a++ % 2 == 0) ? 1 : 2;未分配变量值但仅用于三元运算符条件也“失败”,因为有时选择了错误的分支。目前看来,只有在完全不使用 post 运算符的情况下才能避免该问题。

更新:同样的问题不仅存在于 iOS 设备中,而且在 Mac OSX的 Safari 6 和最新的 Safari 5 上也存在:这些已经测试并发现受到该错误的影响:Mac OS 10.7.4、Safari 5.1.7 Mac OS X 10.8.2、WebKit Nightly r132968:Safari 6.0.1(8536.26.14、537+)。有趣的是,这些似乎没有受到影响:iPad 2 (Mobile) Safari 5.1.7 和 iPad 1 Mobile Safari 5.1 我已向 Apple 报告了这些问题,但尚未收到任何回复。

更新:该错误已报告为 Webkit 错误109036。Apple 仍然没有回复我的错误报告,iOS 和 MacOS 上的所有当前(2013 年 2 月)Safari 版本仍然受到该问题的影响。

2013 年 2 月 27 日更新: Webkit 团队似乎已经修复这个错误!确实是 JIT 和 post-operators 的问题!评论表明可能有更多代码受到该错误的影响,因此可能是修复了更多神秘的 Heisenbugs,现在!

2013 年 10 月更新:该修复程序最终将其变为生产代码:至少在 iPad2 上的 iOS 7.0.2 似乎不再受此错误的影响。不过,我没有检查所有中间版本,因为我们很久以前就解决了这个问题。

4

3 回答 3

10

Try-catch 块似乎禁​​用了 Lion 上 Safari 6 上直接在try块内的部分的 JIT 编译器(此代码在 Safari 6.0.1 7536.26.14 和 OS X Lion 上对我有用)。

// test function
utility.test = function(){
    try {
        var a = 0; // counter for index
        for (var b = this.getStart(); b !== null; b = b.getNext()) // iterate over all cells
            b.$f = a++; // assign index to cell and then increment
    }
    catch (e) { throw e }
    this.$f5 = !1; // random code
};

这至少是当前版本的 Google V8 的记录行为(请参阅 V8 上的Google I/O 演示文稿),但我不知道 Safari。

如果您想为整个脚本禁用它,一种解决方案是编译您的 JS 以使用burrito之类的工具将每个函数的内容包装在 try-catch 中。

使这个可重现的工作做得很好!

于 2012-11-07T17:38:40.670 回答
1

IMO,正确的解决方案是将错误报告给 Apple,然后在您的代码中解决它(当然使用单独的a = a + 1;语句会起作用,除非 JIT 比您想象的更糟!)。不过确实很烂。下面列出了一些常见的东西,你也可以尝试将其放入函数中以使其去优化而不使用 JIT:

  • 例外
  • 'with' 语句
  • 使用 arguments 对象,例如 arguments.callee
  • 评估()

问题在于,如果 Javascript 引擎在修复该错误之前已针对 JIT 进行了优化,那么在这种情况下,您又会再次崩溃。所以,报告和解决方法!

于 2012-11-11T20:14:11.183 回答
1

实际上,在 iPhone 4 和 iPad 2 的 iOS 7.0.4 上的 Safari 中,FOR 循环错误仍然存​​在。循环失败可能比上图简单得多,并且它会通过多次遍历代码才能命中。更改为 WHILE 循环允许正确执行。

失败代码:

function zf(num,digs) 
{ 
var out = ""; 
var n = Math.abs(num); 
for (digs;  digs>0||n>0; digs--)
{ 
    out = n%10 + out; 
    n = Math.floor(n/10); 
}  
return num<0?"-"+out:out; 
} 

成功代码:

function zf(num,digs) 
{ 
var out = ""; 
var n = Math.abs(num); 
do 
{ 
    out = n%10 + out; 
    n = Math.floor(n/10); 
} 
while (--digs>0||n>0) 
return num<0?"-"+out:out; 
} 
于 2013-11-23T16:19:40.977 回答