javascript ios web-applications sleep

javascript - Error de iOS 8: nunca se volvió a llamar a OnUpdateReady cuando el dispositivo regresa del modo de suspensión



web-applications sleep (6)

Cuando un dispositivo con iOS 8 ejecuta una aplicación web (es decir, se inicia desde un acceso directo en la pantalla de inicio) regresa de su estado de suspensión, todas las solicitudes web asincrónicas realizadas no activan la devolución de llamada OnUpdateReady.

El problema es bastante fácil de reproducir: simplemente coloque los dos archivos de código debajo en cualquier servidor web y pruébelo.

¿Alguien más se ha encontrado con este problema? Si es así, ¿hay alguna solución?

Estoy publicando esto para tratar de llamar la atención sobre este error en iOS 8 que esencialmente ha arruinado todas mis aplicaciones web: hemos tenido que recomendar NO actualizar más allá de iOS 7. Y sí, he publicado el problema en Apple Bug Reporter, pero creo que nadie está mirando esto, ya que ha pasado mucho tiempo.

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>


Extraño, Apple acaba de cerrar mi error y se refiere al mismo número de error. También una aplicación web pero encontré que las transiciones css3 dejan de funcionar después del bloqueo de pantalla, ver a continuación:

Engineering ha determinado que su informe de errores (18556061) es un duplicado de otro problema (18042389) y se cerrará

Mi reporte:

Si agrega una aplicación HTML a la pantalla de inicio y la abre, todas las transiciones CSS3 funcionan correctamente. Sin cerrar la aplicación y presionar el bloqueo de pantalla, las transiciones parecen detenerse y pueden hacer que la interfaz de usuario parezca congelarse. Por ejemplo, si se activa una superposición absoluta (opacidad: 0 a opacidad: 1) permanece invisible haciendo que la aplicación parezca no funcionar


También estoy viendo el mismo problema, aunque mi ejemplo es mucho más simple. Simplemente tiene una aplicación webclip con

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

en la pagina. Inspeccionando la consola, después de que el sueño se despierta, no hay más salida de consola. Esto funciona bien en iOS7 (mi aplicación real es una complicada cosa angularJS, simplemente reduje el problema a esto). ¿Has tenido alguna respuesta en tu informe de errores?


El entorno de la aplicación web está tan terriblemente roto al reanudar después del bloqueo de pantalla. No veo cómo (a) Apple podría ignorar esto indefinidamente y (b) cómo cualquier aplicación web puede funcionar con confianza en el entorno paralizado.

Mi solución es detectar reanudar después de dormir utilizando setInterval (que deja de funcionar después del currículum) y publicar una alerta () para el usuario que indica que la aplicación debe volver a iniciarse desde la pantalla de inicio porque iOS no puede reanudarla.

Sucede que la alerta () también se rompe después de la reanudación: muestra la alerta y luego la aplicación web sale a la pantalla de inicio cuando el usuario toca OK. Entonces esto obliga al usuario a relanzar.

El único problema cuando el usuario vuelve a iniciar es el manejo de apple-mobile-web-app-status-bar-style. Tengo este conjunto en negro translúcido, que normalmente establece el contenido de la barra de estado en negro (en fondos claros) o blanco (en fondos oscuros). En el primer relanzamiento después del currículum, el contenido de la barra de estado siempre es negro. En los siguientes relanzamientos (no interrumpidos por suspensión / reanudación) el comportamiento vuelve a ser normal.

Que desastre. Si fuera el responsable de esto en Apple, me avergonzaría, y aquí estamos con 8.1 y todavía no se ha solucionado.


La instalación de iOS 8.1.1 corrige esto.


Las solicitudes Ajax, las funciones del temporizador y WebkitAnimation se rompen después de una pantalla de bloqueo en iOS8.

Para las funciones Ajax y Timer, estamos usando esta solución en nuestro sistema: ¿Cómo reiniciar el temporizador de JavaScript en la aplicación web iOS8 después de desbloquear la pantalla? (enlace a gitHub en el comentario).

No es exactamente parte de la pregunta, pero me gustaría compartir nuestra solución con animaciones y eventos de CSS3, ya que puede ayudar a alguien en el futuro.

Para webkitAnimation, descubrimos que volver a dibujar el elemento con la animación activada o, de forma más drástica, el body reiniciaba las animaciones y los eventos aplicados a ellos (webkitAnimationEnd, por ejemplo, que usa mucho jquery-mobile).

entonces nuestro código da algo como:

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

Puede o no necesitar la función setTimeout en la segunda declaración. Lo más interesante es que, una vez que se ha vuelto a dibujar, nunca volverá a congelarse, sin importar cuántas pantallas de bloqueo surjan después de eso ...


Nuestra solución (para AJAX) es:

... 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(); }

  • agregar un UUID a cada solicitud

if( this.isIOS8() ) { ajaxInfo.url += ''&recoveryUUID=''+ajaxInfo.uuid; }

  • Luego, aún realice el envío XHR (que realmente funciona bien, el servidor recibe y lo envía bien).
  • Lado del servidor guarda el ''resultado'' en la base de datos / archivo con el UUID como índice / parte del nombre de archivo

//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); }

  • En el cliente, cree un pequeño objeto auxiliar global que escuche las inject del código y las redireccione al controlador 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();

  • La secuencia de comandos que se carga inmediatamente se ejecuta (empuja, ya que setInterval también está muerto).

// 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; } ?>

  • A continuación se muestra una implementación de resultados sencilla basada en archivos.

<?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; } } ?>

Como no utilizamos JQuery, todo lo que teníamos que hacer era agregar la lógica extra en nuestra clase Ajax y, por supuesto, guardar en la base de datos para todas las solicitudes.

Desventajas:

  • Asqueroso
  • Continuaremos agregando huella de memoria para cada llamada (para nosotros no es un problema ya que la memoria se borra entre las llamadas a window.location.href (no usamos SPA) para que eventualmente se caiga.
  • Lógica adicional del servidor.

Upsides:

  • Funciona hasta que se agota la memoria (eliminando las etiquetas de script, lo que hacemos no elimina la memoria asociada)

Comentarios:

  • Por supuesto, puede enviar todo con la ''llamada'', pero queríamos tener un impacto mínimo en el servidor (o no mucho trabajo para nosotros de todos modos) + suponemos que esto será reparado y significa que nuestro código de ''usuario'' tiene 0 impacto.