views:

1549

answers:

10

I'm trying to build an image gallery in Safari that mimics the iPad photo app. It works perfectly, except that once I load more than 6MB or so worth of images either by adding them to the DOM or creating new Image objects, new images either stop loading or the browser crashes. This problem is widespread enough (with everyone else hitting up against the same limit) that I've ruled out my Javascript code as the culprit.

Given that you can stream much more than a few MB in a element or through the in-browser media player, this limit seems unnecessary, and there should be some kind of workaround available. Perhaps by freeing up memory or something else.

I also came across this reference for UIWebView.

"JavaScript allocations are also limited to 10 MB. Safari raises an exception if you exceed this limit on the total memory allocation for JavaScript."

Which matches what I'm seeing fairly well. Is it possible to deallocate objects in Javascript, or does Safari/UIWebView keep a running total and never lets go? Alternately, is there any workaround to load in data another way that doesn't eat up this 10MB?

+4  A: 

Update: I think there's an even easier way to do this, depending on your application. Instead of having multiple images, if you simply have one <img> element or Image object (or maybe two, like a 'this' image and a 'next' image if you need animations or transitions) and simply update the .src, .width, .height and so on, you should never get near the 10MB limit. If you wanted to do a carousel application, you'd have to use smaller placeholders first. You might find this technique might be easier to implement.


I think I may actually have found a work-around to this.

Basically, you'll need to do some deeper image management and explicitly shrink any image you don't need. You'd normally do this by using document.removeChild(divMyImageContainer) or $("myimagecontainer").empty() or what have you, but on Mobile Safari this does absolutely nothing; the browser simply never deallocates the memory.

Instead, you need to update the image itself so it takes up very little memory; and you can do that by changing the image's src attribute. The quickest way I know of to do that is to use a data URL. So instead of saying this:

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

...say this instead:

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

Below is a test to demonstrate it working. In my tests, my large 750KB image would eventually kill the browser and halt all JS exectution. But after resetting src, I"ve been able to load in instances of the image over 170 times. An explanation of how the code works is below as well.

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()

This code was written to test my solution, so you'll have to figure out how to apply it to your own code. The code comes in three parts, which I will explain below, but the only really important part is imgStoredImage.src = strNullImage;

loadNextImage() simply loads a new image and calls shrinkImages(). It also assigns an onload event which is used to begin the process of loading another image (bug: I should be clearing this event later, but I'm not).

waitAndReload() is only here to allow the image time to show up on the screen. Mobile Safari is pretty slow and displaying big images, so it needs time after the image has loaded to paint the screen.

shrinkImages() goes through all previously loaded images (except the active one) and changes the .src to the dataurl address.

I'm using a file-folder image for the dataurl here (it was the first dataurl image I could find). I'm using it simply so you can see the script working. You'll probably want to use a transparent gif instead, so use this data url string instead: data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==

Andrew
This may work to reload the same image over and over but it does not work with a sequence of 70+ images. I still need to try Wolvrix's workaround with canvas but I'm pretty upset at Apple for gelding mobile Safari with these strange limitations. It would be nice if mobile Safari would just free old images instead of holding them until a page reload, essentially preventing new images from loading.
Jared Updike
A: 

There are issues with memory and the way to solve this problem is very simple. 1) Put all your thumbnails in canvas. You will be creating a lot of new Image objects and drawing them into canvas, but if your thumbnail are very small you should be fine. For the container where you will be displaying the real size image, create only one Image object and reuse this object and make sure to also draw it into a canvas. So, every time a user clicks the thumbnail, you will update your main Image object. Do not insert IMG tags in the page. Insert CANVAS tags instead with the correct width and height of the thumbnails and the main display container. iPad will cry foul if you insert too many IMG tags. So, avoid them!!! Insert only canvas. You can then find the canvas object from the page and get the context. So every time the user clicks a thumbnail, you will get the src of the main image (real size image) and draw it to the main canvas, reusing the main Image object and the firing the events. Clearing the events every time at the beginning.

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;

I have create 200 thumbnails and each is like 15kb. The real images are like 1 MB each.

Sergio
I've had no luck with this method, fyi.
Steve Simitzis
@Steve: I'll be posting a video on YouTube by the end of this week with some hands on. I'll do some explanation as well as to how I got it to work. I'll let you know once it is posted.
Sergio
A: 

So far I've had luck using <div> tags instead of <img> tags and setting the image as the div's background-image.

All in all, it's crazy. If the user is making an affirmative request for more image content, then there's no reason why Safari shouldn't allow you to load it.

Steve Simitzis
A: 

Steve, as promised. I have put a small sample how I got the problem solved on my end. Check it out on my Youtube Channel. http://www.youtube.com/wolvrix

I even have video of the slide show running in the iPad.

Sergio
A: 

I encountered an out of memory with Javascript on the iPad when we were trying to refresh an image very often, like every couple of seconds. It was a bug to refresh that often, but Safari crashed out to the home screen. Once I got the refresh timing under control, the web app functioned fine. It seemed as if the Javascript engine couldn't keep up with garbage collection quickly enough to discard all the old images.

David Bakkom
+1  A: 

I've had luck starting with the suggestion of Steve Simitzis, and Andrew.

My project:

PhoneGap-based app with 6 main sections, and about 45 subsections which have a jquery cycle gallery of between 2 and 7 images, each 640 x 440 (215+ images altogether). At first I was using ajax to load page fragments, but I've since switched to a one-page site, with all sections hidden until needed.

Initially, after going through about 20 galleries, I was getting memory warning 1, then 2, then the crash.

After making all the images into divs with the image applied as a background, I could get through more galleries (about 35) in the app before a crash, but after going to previously visited galleries, it would eventually fail.

The solution that seems to be working for me, is to store the background image URL in the div's title attribute, and setting all of the background images to be a blank gif. With 215+ images, I wanted to keep the url someplace in the html for sake of ease and quick reference.

When a subnavigation button is pressed, I rewrite the css background image to the correct source which is contained in the div's title tag, for ONLY the gallery that is showing. This saved me from having to do any fancy javascript to store the correct source image.

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

When a new subnavigation button is pressed, I rewrite the background image of the last gallery divs to be blank gifs. So, aside from interface gfx, I only have 2-7 images 'active' at all times. With anything else I add that contains images, I just use this "ondemand" technique to swap the title with the background-image.

Now it seems I can use the app indefinitely with no crashes. Don't know if this will help anyone else, and it may not be the most elegant solution, but it provided a fix for me.

Transoptic
i'm guessing that you don't preload any of the images?
David Murdoch
I really want to try this.
Jared Updike
A: 

I filed a bug with jQuery as jQuery trys to handle memory leaks...so I'd consider this a bug. Hopefully the team can come up with some concise and clever way of handling this problem in Mobile Safari soon.

http://dev.jquery.com/ticket/6944#preview

David Murdoch
A: 

I'm running in a similar issue in Chrome too, developing an extension that loads images in the same page (the popup, actually) replacing old images with new ones. The memory used by the old images (removed from the DOM) is never freed, consuming all the PC memory in a short time. Have tried various tricks with CSS, without success. Using hardware with less memory than a PC, like the iPad, this problem arises earlier, naturally.

UVL
A: 

I just read over these posts -- what is the upshot? Is there a way to get Mobile Safari to release the memory taken by images that were loaded but are no longer used?

I am showing little animations on a page. I do this by having an img tag and replacing the src over and over to display the frames. This works fine, as the animations are small. when the user switches to a new animation, I delete the old images. But I guess the memory isn't released.

Does using div tags work where img tags do not? I guess I will try that -- Steve S. above said that he had some success with that.

Bob S.

Bob S
A: 

I was unable to find a solution for this. Here are a couple of methods I tried, and all of them failed:

  • Simply changed the background of a DIV using div.style.backgroundImage = "url("+base64+")"

  • Changed the .src of an image using img.src = base64

  • Removed the old and added the new image using ...removeChild( document.getElementById("img") ); document.body.appendChild( newImg ) ...

  • The same as above but with a random height on the new image

  • Removing and adding the image as a HTML5 canvas object. Also doesn't work, since a new Image(); has to be created, see *

  • On launch, created a new Image() object, let's call it container. Displayed the image as , every time the image changed, I would change container's .src and redraw the canvas using ctx.drawImage( container, 0,0 ).

  • The sames as the previous, but without actually redrawing the canvas. Simply changing the Image() object's src uses up memory.

A strange thing I noticed: The bug occurs even if the image isn't displayed! For example, when doing this:

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

Every 5 seconds, and nothing else, no loading or displaying the image, of course wrapped up in an object, also crashes the memory after some time!

mobweb