web-dev-qa-db-fra.com

Le navigateur iPad / iPhone se bloque lors du chargement d'images en Javascript

J'essaie de créer une galerie d'images dans Safari qui imite l'application photo iPad. Cela fonctionne parfaitement, sauf qu'une fois que je charge plus de 6 Mo ou plus d'images, soit en les ajoutant au DOM ou en créant de nouveaux objets Image, de nouvelles images cessent de se charger ou le navigateur se bloque. Ce problème est suffisamment répandu (avec tout le monde atteignant la même limite) que j'ai exclu mon code Javascript comme coupable.

Étant donné que vous pouvez diffuser beaucoup plus que quelques Mo dans un élément ou via le lecteur multimédia intégré au navigateur, cette limite semble inutile, et il devrait y avoir une sorte de solution de contournement disponible. Peut-être en libérant de la mémoire ou autre chose.

Je suis également tombé sur cette référence pour UIWebView .

"Les allocations JavaScript sont également limitées à 10 Mo. Safari déclenche une exception si vous dépassez cette limite sur l'allocation totale de mémoire pour JavaScript."

Ce qui correspond assez bien à ce que je vois. Est-il possible de désallouer des objets en Javascript, ou Safari/UIWebView conserve-t-il un total cumulé et ne lâche jamais? Sinon, existe-t-il une solution de contournement pour charger les données d'une autre manière qui ne consomme pas ces 10 Mo?

53
Steve Simitzis

Mise à jour: je pense qu'il existe un moyen encore plus simple de le faire, selon votre application. Au lieu d'avoir plusieurs images, si vous avez simplement un élément <img> Ou Image (ou peut-être deux, comme une image 'this' et une image 'next' si vous avez besoin d'animations ou de transitions) et mettez simplement à jour les .src, .width, .height et ainsi de suite, vous ne devriez jamais vous approcher de la limite de 10 Mo. Si vous souhaitez faire une application de carrousel, vous devez d'abord utiliser des espaces réservés plus petits. Vous pourriez trouver cette technique plus facile à mettre en œuvre.


Je pense que j'ai peut-être trouvé une solution à ce problème.

Fondamentalement, vous devrez faire une gestion d'image plus approfondie et réduire explicitement toute image dont vous n'avez pas besoin. Vous le feriez normalement en utilisant document.removeChild(divMyImageContainer) ou $("myimagecontainer").empty() ou quoi d'autre, mais sur Mobile Safari cela ne fait absolument rien; le navigateur ne désalloue simplement jamais la mémoire.

Au lieu de cela, vous devez mettre à jour l'image elle-même afin qu'elle occupe très peu de mémoire; et vous pouvez le faire en modifiant l'attribut src de l'image. La façon la plus rapide que je connaisse de le faire est d'utiliser un RL de données . Donc au lieu de dire ceci:

myImage.src="/path/to/image.png"

... dis plutôt ceci:

myImage.src="data:image/gif;base64,AN_ENCODED_IMAGE_DATA_STRING"

Voici un test pour démontrer qu'il fonctionne. Dans mes tests, ma grande image de 750 Ko finirait par tuer le navigateur et arrêterait toute exécution JS. Mais après avoir réinitialisé src, j'ai pu charger plus de 170 fois les instances de l'image. Une explication du fonctionnement du code est également présentée ci-dessous.

var strImagePath = "http://path/to/your/gigantic/image.jpg";
var arrImages = [];
var imgActiveImage = null
var strNullImage = "data:image/gif;base64,R0lGODlhEAAOALMAAOazToeHh0tLS/7LZv/0jvb29t/f3//Ub//ge8WSLf/rhf/3kdbW1mxsbP//mf///yH5BAAAAAAALAAAAAAQAA4AAARe8L1Ekyky67QZ1hLnjM5UUde0ECwLJoExKcppV0aCcGCmTIHEIUEqjgaORCMxIC6e0CcguWw6aFjsVMkkIr7g77ZKPJjPZqIyd7sJAgVGoEGv2xsBxqNgYPj/gAwXEQA7";
var intTimesViewed = 1;
var divCounter = document.createElement('h1');
document.body.appendChild(divCounter);

var shrinkImages = function() {
    var imgStoredImage;
    for (var i = arrImages.length - 1; i >= 0; i--) {
        imgStoredImage = arrImages[i];
        if (imgStoredImage !== imgActiveImage) {
            imgStoredImage.src = strNullImage;
        }
    }
};
var waitAndReload = function() {
    this.onload = null;
    setTimeout(loadNextImage,2500);
};
var loadNextImage = function() {
    var imgImage = new Image();
    imgImage.onload = waitAndReload;
    document.body.appendChild(imgImage);
    imgImage.src = strImagePath + "?" + (Math.random() * 9007199254740992);
    imgActiveImage = imgImage;
    shrinkImages()
    arrImages.Push(imgImage);
    divCounter.innerHTML = intTimesViewed++;
};
loadNextImage()

Ce code a été écrit pour tester ma solution, vous devrez donc trouver comment l'appliquer à votre propre code. Le code est divisé en trois parties, que j'expliquerai ci-dessous, mais la seule partie vraiment importante est imgStoredImage.src = strNullImage;

loadNextImage() charge simplement une nouvelle image et appelle shrinkImages(). Il assigne également un événement onload qui est utilisé pour commencer le processus de chargement d'une autre image (bug: je devrais effacer cet événement plus tard, mais je ne le suis pas).

waitAndReload() n'est là que pour permettre à l'image de s'afficher à l'écran. Mobile Safari est assez lent et affiche de grandes images, il faut donc du temps après le chargement de l'image pour peindre l'écran.

shrinkImages() parcourt toutes les images précédemment chargées (sauf celle active) et change le .src en adresse dataurl.

J'utilise une image de dossier-fichier pour le dataurl ici (c'était la première image dataurl que j'ai pu trouver). Je l'utilise simplement pour que vous puissiez voir le script fonctionner. Vous voudrez probablement utiliser un gif transparent à la place, alors utilisez plutôt cette chaîne d'url de données: data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==

14
Andrew

Les limites de téléchargement de 6,5 Mo (iPad)/10 Mo (iPhone) sont calculées en fonction du nombre d'éléments d'image utilisés pour définir une image via sa propriété src. Le safari mobile ne semble pas différencier les images chargées depuis le cache ou via le réseau. Peu importe que l'image soit injectée dans le dom ou non.

La deuxième partie de la solution est que le safari mobile semble pouvoir charger un nombre illimité d'images via la propriété css "background-image".

Cette preuve de concept utilise un pool de précacheurs qui définissent les propriétés de l'image d'arrière-plan une fois téléchargé avec succès. Je sais que ce n'est pas optimal et ne renvoie pas le téléchargeur d'images utilisé à la piscine, mais je suis sûr que vous avez l'idée :)

L'idée est adaptée de la solution de contournement originale de Rob Laplaca http://roblaplaca.com/blog/2010/05/05/ipad-safari-image-limit-workaround/

<!DOCTYPE html>
<head> 
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 
<title>iPad maximum number of images test</title> 
<script type="text/javascript">
    var precache = [
        new Image(),
        new Image(),
        new Image(),
        new Image()
    ];

    function setImage(precache, item, waiting) {
        precache.onload = function () {
            item.img.style.backgroundImage = 'url(' + item.url + ')';
            if (waiting.length > 0) {
                setImage(precache, waiting.shift(), waiting);
            }
        };
        precache.src = item.url;
    }

    window.onload = function () {
        var total = 50,
            url = 'http://www.roblaplaca.com/examples/ipadImageLoading/1500.jpg',
            queue = [],
            versionUrl,
            imageSize = 0.5,
            mb,
            img;

        for (var i = 0; i < total; i++) {
            mb = document.createElement('div');
            mb.innerHTML = ((i + 1) * imageSize) + 'mb';
            mb.style.fontSize = '2em';
            mb.style.fontWeight = 'bold';

            img = new Image();
            img.width = 1000;
            img.height = 730;
            img.style.width = '1000px';
            img.style.height = '730px';
            img.style.display = 'block';

            document.body.appendChild(mb);
            document.body.appendChild(img);


            queue.Push({
                img: img,
                url: url + '?ver=' + (i + +new Date())
            });
        }

        //
        for (var p = 0; p < precache.length; p++) {
            if (queue.length > 0) {
                setImage(precache[p], queue.shift(), queue);
            }
        }
    };
</script>
</head> 
<body> 
<p>Loading (roughly half MB) images with the <strong>img tag</strong></p> 
</body> 
</html> 
12
Alex

J'ai eu de la chance en commençant par la suggestion de Steve Simitzis et Andrew.

Mon projet:

Application basée sur PhoneGap avec 6 sections principales et environ 45 sous-sections qui ont une galerie de cycle jquery entre 2 et 7 images, chacune 640 x 440 (215+ images au total). Au début, j'utilisais ajax pour charger des fragments de page, mais depuis, je suis passé à un site d'une page, avec toutes les sections cachées jusqu'à ce qu'elles soient nécessaires.

Au départ, après avoir parcouru une vingtaine de galeries, je recevais un avertissement de mémoire 1, puis 2, puis le crash.

Après avoir transformé toutes les images en div avec l'image appliquée en arrière-plan, j'ai pu parcourir plus de galeries (environ 35) dans l'application avant un crash, mais après être allé dans des galeries précédemment visitées, cela finirait par échouer.

La solution qui semble fonctionner pour moi, est de stocker l'URL de l'image d'arrière-plan dans l'attribut title de div, et de définir toutes les images d'arrière-plan comme un gif vide. Avec plus de 215 images, je voulais garder l'URL quelque part dans le html par souci de facilité et de référence rapide.

Lorsqu'un bouton de sous-navigation est enfoncé, je réécris l'image d'arrière-plan CSS dans la source correcte qui est contenue dans la balise de titre de la div, pour SEULEMENT la galerie qui s'affiche. Cela m'a évité d'avoir à faire du javascript de fantaisie pour stocker l'image source correcte.

var newUrl = $(this).attr('title');
$(this).css('background-image', 'url('+newUrl+')'); 

Lorsqu'un nouveau bouton de sous-navigation est enfoncé, je réécris l'image d'arrière-plan des derniers divs de la galerie en gifs vierges. Donc, à part l'interface gfx, je n'ai que 2 à 7 images "actives" à tout moment. Avec tout ce que j'ajoute qui contient des images, j'utilise simplement cette technique "ondemand" pour échanger le titre avec l'image d'arrière-plan.

Il semble maintenant que je puisse utiliser l'application indéfiniment sans plantages. Je ne sais pas si cela aidera quelqu'un d'autre, et ce n'est peut-être pas la solution la plus élégante, mais cela m'a fourni une solution.

6
Transoptic

Jusqu'à présent, j'ai eu de la chance en utilisant <div> balises au lieu de <img> balise et définit l'image comme image d'arrière-plan de la div.

Dans l'ensemble, c'est fou. Si l'utilisateur fait une demande affirmative pour plus de contenu d'image, alors il n'y a aucune raison pour que Safari ne vous autorise pas à le charger.

5
Steve Simitzis

Je n'ai pas pu trouver de solution à cela. Voici quelques méthodes que j'ai essayées, et toutes ont échoué:

  • Il suffit de changer l'arrière-plan d'un DIV à l'aide de div.style.backgroundImage = "url("+base64+")"

  • Modification du .src D'une image à l'aide de img.src = base64

  • Suppression de l'ancienne et ajout de la nouvelle image à l'aide de removeChild( document.getElementById("img") ); document.body.appendChild( newImg )

  • Le même que ci-dessus mais avec une hauteur aléatoire sur la nouvelle image

  • Suppression et ajout de l'image en tant qu'objet canevas HTML5. Ne fonctionne pas non plus, car une nouvelle Image(); doit être créée, voir *

  • Au lancement, créé un nouvel objet Image(), appelons-le conteneur. Affiche l'image sous la forme <canvas>, Chaque fois que l'image change, je change le .src Du conteneur et redessine la toile à l'aide de ctx.drawImage( container, 0,0 ).

  • Les mêmes que les précédents, mais sans réellement redessiner la toile. Changer simplement la Image() l'objet src de l'objet utilise de la mémoire.

Une chose étrange que j'ai remarquée: le bug se produit même si l'image n'est pas affichée! Par exemple, lors de cette opération:

var newImg = new Image( 1024, 750 );
newImg.src = newString; // A long base64 string

Toutes les 5 secondes, et rien d'autre, pas de chargement ou d'affichage de l'image, bien sûr enveloppée dans un objet, plante également la mémoire après un certain temps!

3
Louis B.

Sur une application Rails, j'étais paresseux en train de charger des centaines de photos de taille moyenne (défilement infini) et j'ai inévitablement atteint la limite de 10 Mo sur l'iphone. J'ai essayé de charger les graphiques dans une toile (nouvelle image, src =, puis Image.onload) mais j'ai quand même atteint la même limite. J'ai également essayé de remplacer l'img src et de le supprimer (quand il sortait de la zone visible) mais toujours pas de cigare. En fin de compte, en désactivant toutes les balises img w/div's w/la photo en arrière-plan a fait l'affaire.

      $.ajax({
        url:"/listings/"+id+"/big",
        async:true,
        cache:true,
        success:function(data, textStatus, XMLHttpRequest) {
          // detect iOS
          if (navigator.userAgent.match(/iPhone/i) || navigator.userAgent.match(/iPod/i) || navigator.userAgent.match(/iPad/i)) {
            // load html into data
            data = $(data);
            // replace img w/ div w/ css bg
            data.find(".images img").each(function() { 
              var src = $(this).attr("src").replace(/\s/g,"%20");
              var div = $("<div>"); 
              div.css({width:"432px",height:"288px",background:"transparent url("+src+") no-repeat"}); 
              $(this).parent().append(div); 
              $(this).remove(); 
            }); 
            // remove graphic w/ dynamic dimensions
            data.find(".logo").remove();
          }
          // append element to the page
          page.append(data);
        }
      });

Je peux maintenant charger bien plus de 40 Mo de photos sur une seule page sans toucher le mur. J'ai rencontré un problème étrange, cependant, avec certains graphiques d'arrière-plan CSS qui ne s'affichent pas. Un rapide fil js a corrigé cela. Définissez la propriété css bg de div toutes les 3 secondes.

  setInterval(function() {
    $(".big_box .images div.img").each(function() {
      $(this).css({background:$(this).css("background")});
    });
  }, 3000);

Vous pouvez le voir en action sur http://fotodeck.com . Vérifiez-le sur votre iPhone/iPad.

3
danlee

J'ai rencontré une mémoire insuffisante avec Javascript sur l'iPad lorsque nous essayions de rafraîchir une image très souvent, comme toutes les deux secondes. C'était un bug pour rafraîchir cela souvent, mais Safari s'est écrasé sur l'écran d'accueil. Une fois que j'ai maîtrisé le calendrier de rafraîchissement, l'application Web a bien fonctionné. Il semblait que le moteur Javascript ne pouvait pas suivre la collecte des ordures assez rapidement pour supprimer toutes les anciennes images.

2
David Bakkom

Il y a des problèmes de mémoire et la façon de résoudre ce problème est très simple. 1) Mettez toutes vos vignettes en toile. Vous allez créer beaucoup de nouveaux objets Image et les dessiner dans le canevas, mais si votre miniature est très petite, ça devrait aller. Pour le conteneur dans lequel vous allez afficher l'image en taille réelle, créez un seul objet Image et réutilisez cet objet et assurez-vous de le dessiner également dans un canevas. Ainsi, chaque fois qu'un utilisateur clique sur la miniature, vous mettez à jour votre objet Image principal. N'insérez pas de balises IMG dans la page. Insérez plutôt des balises CANVAS avec la largeur et la hauteur correctes des vignettes et du conteneur d'affichage principal. L'iPad pleurera si vous insérez trop de balises IMG. Alors, évitez-les !!! Insérez uniquement la toile. Vous pouvez ensuite trouver l'objet canevas à partir de la page et obtenir le contexte. Ainsi, chaque fois que l'utilisateur clique sur une miniature, vous obtiendrez le src de l'image principale (image en taille réelle) et la dessinerez sur le canevas principal, en réutilisant l'objet Image principal et en déclenchant les événements. Effacer les événements à chaque fois au début.

mainDisplayImage.onload = null;
mainDisplayImage.onerror = null;

...

mainDisplayImage.onload = function() { ... Draw it to main canvas }
mainDisplayImage.onerror = function() { ... Draw the error.gif to main canvas }
mainDisplayImage.src = imgsrc_string_url;

J'ai créé 200 miniatures et chacune est comme 15kb. Les vraies images sont comme 1 Mo chacune.

2
Sergio

J'ai également eu des problèmes similaires lors du rendu de grandes listes d'images sur les iPhones. Dans mon cas, afficher même 50 images dans la liste était suffisant pour planter le navigateur ou parfois l'ensemble du système d'exploitation. Pour une raison quelconque, les images restituées sur la page n'ont pas été récupérées, même lors de la mise en commun et du recyclage de quelques éléments DOM à l'écran ou de l'utilisation des images comme propriété d'image d'arrière-plan. Même l'affichage direct des images sous forme d'URI de données suffit pour compter dans la limite.

La solution s'est avérée plutôt simple - l'utilisation de position: absolute Sur les éléments de la liste leur permet d'être récupérés assez rapidement pour ne pas se heurter à une limite de mémoire. Cela impliquait toujours d'avoir seulement environ 20-30 images dans le DOM à tout moment, la création et la suppression des nœuds DOM de l'élément par position de défilement ont finalement fait l'affaire.

Il semble que cela dépend particulièrement de l'application de webkit-transform':'scale3d() à tout ancêtre des images dans le DOM. Faire couler relativement un DOM très haut et le rendre sur le GPU fait chier une fuite de mémoire dans le moteur de rendu webkit, je suppose?

1
haversine

J'exécute un problème similaire dans Chrome aussi, en développant une extension qui charge les images dans la même page (le popup, en fait) en remplaçant les anciennes images par de nouvelles. La mémoire utilisée par l'ancien les images (supprimées du DOM) ne sont jamais libérées, consommant toute la mémoire du PC en peu de temps. J'ai essayé diverses astuces avec CSS, sans succès. En utilisant du matériel avec moins de mémoire qu'un PC, comme l'iPad, ce problème se pose plus tôt, naturellement .

0
Omiod