当运行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,但它还没有修复。