iOS 8 Bug-当设备从睡眠状态返回时,OnUpdateReady再也不会调用

iOS 8 Bug - OnUpdateReady never called again when device returns from sleep

本文关键字:返回 OnUpdateReady 再也不 调用 状态 Bug- iOS      更新时间:2023-09-26

当运行Web应用程序的iOS 8设备(即从主屏幕上的快捷方式启动)从睡眠状态返回时,所有异步Web请求都无法触发OnUpdateReady回调。

这个问题很容易重现——只需将下面的两个代码文件放在任何web服务器上并尝试一下。

有其他人遇到过这个问题吗?如果是,有什么解决办法吗?

我发布这篇文章是为了引起人们对iOS 8中这个错误的关注,这个错误基本上破坏了我所有的网络应用程序——我们不得不建议不要升级到iOS 7之后。是的,我已经在Apple Bug Reporter上发布了这个问题,但我认为已经很久没有人关注这些了。

app.manifest

CACHE MANIFEST
# 2014-09-24 - Test
CACHE:
default.html

default.html

<!DOCTYPE html>
<html manifest="app.manifest">
<head>
  <title>Test Harness</title>
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
  <meta name="HandheldFriendly" content="true" />
  <meta name="apple-mobile-web-app-capable" content="yes" />
  <meta name="apple-mobile-web-app-status-bar-style" content="black" />
  <script language="javascript" type="text/javascript">
    var Test = new function () {
      var _GetEnd = function (oResult) {
        var sResult = ': ' +
          ((oResult.Value === true)
            ? 'Success'
            : 'Failure<br>' + oResult.Reason) +
          '<br>';
        var oLog = document.getElementById('idLog');
        oLog.innerHTML = (new Date()) + sResult + oLog.innerHTML
        setTimeout(_GetBegin, 1000);
      };
      var _GetBegin = function () {
        var sURL = 'app.manifest';
        var hAsyncCallback = _GetEnd;
        try {
          var oRequest = new XMLHttpRequest();
          oRequest.onreadystatechange =
            function () {
              if (oRequest.readyState != 4) return;
              if (oRequest.status != 200) {
                hAsyncCallback({ Value: false, Reason: oRequest.responseText });
              } else {
                hAsyncCallback({ Value: true, Reason: null });
              }
            };
          oRequest.open('GET', sURL, true);
          oRequest.send(null);
        } catch (e) {
          alert('Critical Error: ' + e.message );
        }
      };
      this.Start = function () { _GetBegin(); }
    };
  </script>
</head>
<body onload="Test.Start();">
  <ol>
    <li>Put both app.manifest and default.html on a web server.</li>
    <li>Make sure this page is being launched from the Home screen as a web application.</li>
    <li>Press the sleep button while it is running.</li>
    <li>Press the wake button and unlock the phone to get back to this screen.</li>
    <li>Under iOS7x the page continues, under iOS8 the onreadystatechange never gets called again.</li>
  </ol>
  <div id="idLog"></div>
</body>
</html>

安装iOS 8.1.1修复了此问题。

我也看到了同样的问题,尽管我的例子要简单得多。只需拥有一个带有的网络剪辑应用程序

<script> window.setInterval(function(){ console.log("Johnny Five Alive! : " + new Date()); },1000); </script>

在页面上。检查控制台,睡眠唤醒后,不再有控制台输出。这在iOS7上运行良好(我的实际应用程序是一个复杂的angularJS,我只是把问题归结为这个)。你对你的错误报告有任何回应吗?

我们的解决方法(针对AJAX)是:

  • 检测iOS8(事实上,8.0.2仍然有这个)(另请参阅其他解决方法:如何在屏幕解锁后恢复iOS8 web应用程序上的JavaScript计时器?)
  • 删除正常的eventListener,但保留onProgress
...
this.onProgress = function(e)
{
    var position = e.position || e.loaded;
    var total = e.totalSize || e.total;
    var percentage = 0.0;
    if(total != 0)
    {
        percentage = position / total;
    }
    if(percentage == 1) {
        if( this.isIOS8() ) {
            recovery_uuid.get(uuid, _.bind(this.ios8ScriptReturn, this));
        }
    }
}
    ...
   //this gets called when the script with this UUID is injected
   this.ios8ScriptReturn = function(uuid, value) {
//then we create a simpler non real one 
        this.xhr = {};
        this.xhr.readyState = 4;
        this.xhr.status = 200;
        this.xhr.responseText = value;
        this.xhr.onreadystatechange = null;
        this.xhr.isFake = true; 
        //fake stateChnage
        this.onReadyStateChange();
    }
  • 为每个请求添加一个UUID
if( this.isIOS8() ) {
    ajaxInfo.url += '&recoveryUUID='+ajaxInfo.uuid;
}
  • 然后仍然执行XHR发送(这实际上很好,服务器获取并发送回也很好)
  • 服务器端将"结果"保存在数据库/文件中,UUID作为索引/文件名的一部分
//detect the need for saving the result, and save it till requested
if(isset($_GET['recoveryUUID'])) {
    $uuid = $_GET['recoveryUUID'];
    RecoveryUUID::post($uuid, $result_json_string);
}
  • 在客户端上创建一个小的helper全局对象,用于侦听代码注入并将其重定向到onProgress处理程序
   var RecoveryUUID = (function (_root) {
    function RecoveryUUID() {
        this.callbacks = {};
    }
    var proto = RecoveryUUID.prototype;
    proto.onLoaded = null;
    proto.set = function(uuid, value) {
        console.log('RECOVERY UUID: Received DATA: '+uuid+' value: '+value);
        if(typeof this.callbacks[uuid] != 'undefined') {
            this.callbacks[uuid](uuid, value);
            delete this.callbacks[uuid]; //auto remove
        } 
        if(this.onLoaded != null) {
            this.onLoaded(uuid, value);
        }
        var script = document.getElementById("recoveryScript_"+uuid);
        script.parentElement.removeChild(script);
    }
    proto.getURL = function(uuid) {
        return "http://"+window.location.hostname+":8888/recoveryuuid/index.php?uuid="+uuid;
    }
    proto.get = function(uuid, callback) {
        var script = document.createElement("script");
        script.setAttribute("id", "recoveryScript_"+uuid);
        script.setAttribute("type", "text/javascript");
        script.setAttribute("src", this.getURL(uuid));
        if(typeof callback != 'undefined') {
            this.callbacks[uuid] = callback;
        }
        document.getElementsByTagName("head")[0].appendChild(script);
    }

    return RecoveryUUID;
})();
//global - just so the injected script knows what to call
recovery_uuid = new RecoveryUUID();
  • 加载的脚本会立即执行(推送,因为setInterval也是死的)
 // this is: http://"+window.location.hostname+":8888/recoveryuuid/index.php?uuid=...."
<?php
header('Cache-Control: no-cache, no-store, must-revalidate, post-check=0, pre-check=0 '); // HTTP 1.1. //iOS force this file to keep fresh
header('Pragma: no-cache'); // HTTP 1.0.
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header('Content-type: application/javascript; charset=UTF-8');
if(isset($_GET['uuid'])) {
    $uuid = $_GET['uuid'];
    $out = 'recovery_uuid.set('.json_encode($uuid).','.json_encode(RecoveryUUID::get($uuid)).');';
    echo $out;
}
?>
  • 下面是一个简单的基于文件的结果实现
<?php
class RecoveryUUID {
    static public function getFileName($uuid) {
        return SOMESTATIC LOCATION.$uuid.'.json';
    }
    static public function post($uuid, $stdClassOrString) {
        $data = '{ "data": '.json_encode($stdClassOrString).', "uuid": '.json_encode($uuid).' }';
        file_put_contents(self::getFileName($uuid), $data);
    }
    //might not work first time as the script tag might request this file before it was written.
    //so we wait just a bit.
    static public function getFull($uuid) {
        $tries = 10;
        $filename = self::getFileName($uuid);
        while ($tries > 0) {
            if(file_exists($filename)) {
                if (is_readable($filename)) {
                    $data = @file_get_contents($filename);
                    if($data !== FALSE) {
                        unlink($filename);
                        return $data;
                    }
                }
            }
            $tries = $tries -1;
            usleep(250000);//wait 0.25 secs ...
        }
        $data = new stdClass();
        $data->uuid = $uuid;
        $data->data = 'ERROR RECOVERYUUID: timeout on reading file';
        return $data;
    }
    static public function get($uuid) {
        $decoded = json_decode(self::getFull($uuid));
        if( $decoded->uuid == $uuid ) {
            return $decoded->data;
        }
        return null;
    }
}
?>

由于我们不使用JQuery,我们所需要做的就是在Ajax类中添加额外的逻辑,当然还有所有请求的Saving to Database。。

不利因素:

  • 讨厌
  • 将继续为每个调用添加内存占用(对我们来说不是问题,因为window.location.href调用之间的内存已被清除(我们不使用SPA),所以最终会失败
  • 额外的服务器端逻辑

优点:

  • 在内存耗尽之前一直有效(删除脚本标记,这样做不会删除关联的内存)

评论:

  • 当然,您可以通过"调用"发送所有内容,但我们希望将服务器端的影响降至最低(或者对我们来说没有太多工作)+我们认为这将得到解决,这意味着我们的"用户"代码没有影响

奇怪的是,苹果刚刚关闭了我的bug,并引用了相同的bug编号。也是一个网络应用程序,但我发现css3转换在屏幕锁定后停止工作,如下所示:

工程部已确定您的错误报告(18556061)与另一个问题(18042389)重复,将关闭

我的报告:

如果您将HTML应用程序添加到主屏幕并打开它,则所有CSS3转换都能正常工作。在不关闭应用程序并按下屏幕锁定的情况下,转换似乎会停止,并可能导致ui看起来冻结。例如,如果触发了一个绝对覆盖(不透明度:0到不透明度:1),它将保持不可见,使应用程序看起来不起作用。

Ajax请求、Timer函数和WebkitManimation在iOS8上的锁屏后被破坏。

对于Ajax和Timer函数,我们在系统中使用此解决方案:如何在屏幕解锁后恢复iOS8网络应用程序上的JavaScript计时器?(评论中的gitHub链接)。

这并不完全是问题的一部分,但我想与CSS3动画和事件分享我们的解决方法,因为它可能会在未来对某人有所帮助。

对于webkitManimation,我们发现在打开动画的情况下重新绘制元素,或者更严格地说,body会重新启动应用于它们的动画和事件(例如,jquery-mobile大量使用的webkitAnimationEnd)。

所以我们的代码给出了类似于:

    document.body.style.display='none';
    setTimeout( function() { document.body.style.display = 'block'; }, 1);

您可能需要也可能不需要第二条语句中的setTimeout函数。最有趣的是,一旦它被重新绘制,无论之后出现多少锁屏,它都不会再次冻结。。。

在屏幕锁定后恢复时,网络应用程序环境被严重破坏,我不明白(a)苹果如何无限期地忽略这一点,以及(b)任何网络应用程序如何能够自信地在受损的环境中工作。

我的解决方案是使用setInterval(在恢复后停止工作)检测睡眠后的恢复,然后向用户发布警报(),说明由于iOS无法恢复,必须从主屏幕重新启动应用程序。

恰好alert()在恢复后也会中断——它会显示警报,然后当用户点击OK时,Web应用程序会退出到主屏幕!因此,这迫使用户重新启动。

当用户重新启动时,唯一的问题是苹果移动网络应用程序状态栏样式的处理。我将此设置为黑色半透明,通常将状态栏内容设置为黑色(在浅色背景下)或白色(在深色背景下)。在恢复后的第一次重新启动时,状态栏内容始终为黑色。在随后的重新启动(未被睡眠/恢复中断)时,行为将恢复正常。

真是一团糟。如果我在苹果对此负责,我会很尴尬,现在我们有8.1,但它还没有修复。