53 votes

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 de l'iPad. Cela fonctionne parfaitement, sauf que dès que je charge plus de 6 Mo d'images, soit en les ajoutant au DOM, soit en créant de nouveaux objets Image, les nouvelles images cessent de se charger ou le navigateur se bloque. Ce problème est suffisamment répandu (tout le monde se heurte à la même limite) pour que j'aie exclu mon code Javascript comme coupable.

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

Je suis aussi tombé sur ceci référence pour UIWebView .

" Les allocations JavaScript sont également limitées à 10 Mo. Safari lève 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 est-ce que Safari/UIWebView garde un total en cours et ne le lâche jamais ? Sinon, existe-t-il une solution pour charger des données d'une autre manière qui ne consomme pas ces 10 Mo ?

14voto

Andrew Points 5374

Mise à jour : Je pense qu'il y a un moyen encore plus facile de faire cela, en fonction de votre application. Au lieu d'avoir de multiples images, si vous avez simplement une <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 l'objet .src , .width , .height et ainsi de suite, vous ne devriez jamais vous approcher de la limite des 10 Mo. Si vous vouliez faire une application carrousel, vous devriez d'abord utiliser des espaces réservés plus petits. Vous trouverez peut-être cette technique plus facile à mettre en œuvre.


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

En gros, vous devrez gérer les images de manière plus approfondie et réduire explicitement toutes les images dont vous n'avez pas besoin. Pour ce faire, vous utilisez normalement document.removeChild(divMyImageContainer) o $("myimagecontainer").empty() ou autre, mais sur Mobile Safari, cela ne fait absolument rien ; le navigateur ne désalloue tout simplement jamais la mémoire.

Au lieu de cela, vous devez mettre à jour l'image elle-même de manière à ce qu'elle occupe très peu de mémoire. src attribut. Le moyen le plus rapide que je connaisse pour le faire est d'utiliser un attribut URL des données . Donc au lieu de dire ça :

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

...dites plutôt ceci :

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

Vous trouverez ci-dessous un test pour démontrer son fonctionnement. Dans mes tests, ma grande image de 750KB a fini par tuer le navigateur et arrêter toute exécution de JS. Mais après avoir réinitialisé src J'ai été capable de charger des instances de l'image plus de 170 fois. Vous trouverez également ci-dessous une explication du fonctionnement du code.

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 composé de trois parties, que j'expliquerai ci-dessous, mais la seule partie vraiment importante est la suivante imgStoredImage.src = strNullImage;

loadNextImage() charge simplement une nouvelle image et appelle shrinkImages() . Il attribue également un 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 fais pas).

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

shrinkImages() passe en revue toutes les images précédemment chargées (à l'exception de l'image active) et modifie l'attribut .src à l'adresse dataurl.

J'utilise ici une image de fichier-dossier pour le dataurl (c'était la première image de 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 dataurl : data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==

12voto

Alex Points 6968

Les limites de téléchargement de 6,5 Mo (iPad) / 10 Mo (iPhone) sont calculées sur la base du nombre d'éléments d'image utilisés pour définir une image via sa propriété src. Mobile safari ne semble pas faire la différence entre les images chargées depuis le cache ou via le réseau. Le fait que l'image soit injectée ou non dans le dom n'a pas non plus d'importance.

La deuxième partie de la solution est que Safari mobile semble être capable de 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 le téléchargement réussi. Je sais que ce n'est pas optimal et que le téléchargeur d'image utilisé n'est pas renvoyé au pool, mais je suis sûr que vous comprenez l'idée :)

L'idée est adaptée de la solution originale de Rob Laplaca pour le canevas 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>

6voto

Transoptic Points 160

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 cyclique jquery de 2 à 7 images, chacune 640 x 440 (215+ images au total). Au début, j'utilisais ajax pour charger les fragments de page, mais j'ai depuis changé pour un site à une seule page, avec toutes les sections cachées jusqu'à ce qu'elles soient nécessaires.

Au début, après avoir parcouru une vingtaine de galeries, j'ai eu un premier avertissement de mémoire, puis un deuxième, puis le crash.

Après avoir transformé toutes les images en divs avec l'image appliquée en arrière-plan, j'ai pu parcourir un plus grand nombre de galeries (environ 35) dans l'application avant un plantage, mais après avoir visité des galeries précédentes, l'application finissait 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 la division, et de définir toutes les images d'arrière-plan comme étant un gif vide. Avec plus de 215 images, je voulais garder l'URL quelque part dans le html pour des raisons de facilité et de référence rapide.

Lorsqu'on appuie sur un bouton de sous-navigation, je réécris l'image d'arrière-plan en css vers la source correcte qui est contenue dans la balise title de la division, pour SEULEMENT la galerie qui est affichée. Cela m'a évité d'avoir à faire un javascript sophistiqué pour stocker l'image source correcte.

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

Lorsqu'on appuie sur un nouveau bouton de sous-navigation, je réécris l'image d'arrière-plan des derniers divs de la galerie pour qu'ils soient des gifs vides. Ainsi, en dehors des images de l'interface, je n'ai que 2 à 7 images "actives" à tout moment. Pour tout ce que j'ajoute qui contient des images, j'utilise simplement cette technique "à la demande" pour remplacer le titre par l'image d'arrière-plan.

Maintenant, il semble que je puisse utiliser l'application indéfiniment sans plantage. Je ne sais pas si cela va aider quelqu'un d'autre, et ce n'est peut-être pas la solution la plus élégante, mais cela m'a apporté une solution.

5voto

Steve Simitzis Points 522

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

Dans l'ensemble, c'est fou. Si l'utilisateur fait une demande positive pour plus d'images, il n'y a aucune raison pour que Safari ne vous permette pas de les charger.

3voto

Louis Bataillard Points 930

Je n'ai pas réussi à trouver une solution à ce problème. Voici quelques méthodes que j'ai essayées, et toutes ont échoué :

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

  • Modifié le .src d'une image en utilisant img.src = base64

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

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

  • Suppression et ajout de l'image en tant qu'objet HTML5 canvas. Cela ne fonctionne pas non plus, car un nouvel objet Image(); doit être créé, voir *

  • Au lancement, création d'une nouvelle Image() appelons-le conteneur. Affiche l'image comme <canvas> chaque fois que l'image changeait, je modifiais les paramètres du conteneur. .src et redessiner le canevas en utilisant ctx.drawImage( container, 0,0 ) .

  • La même chose que la précédente, mais sans redessiner la toile. Il suffit de changer le Image() de l'objet src consomme 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, en faisant ceci :

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, qui fait également planter la mémoire après un certain temps !

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X