skrat
skrat

Reputation: 5562

Image caching with JS on iPad

I'm working on a texture picker intended for use on iPad. So basically just a bunch of image elements. To avoid image reloading and lag, I cache and reuse the Image objects in JS. Sort of this

/**
 * Asynchronous version of memoize for use with callback functions. Asserts
 * that last argument is the callback.
 *
 * @param  {Function} func
 * @return {Function}
 */
 util.memoize.async = function(func) {
    var cache = {};
    return function() {
        var hash = JSON.stringify(arguments);
        var args = Array.prototype.splice.call(arguments, 0);
        var callback = args.pop();
        if (hash in cache) {
            return callback.apply(this, cache[hash]);
        }
        args.push(function() {
            cache[hash] = Array.prototype.splice.call(arguments, 0);
            callback.apply(this, cache[hash]);
        });
        return func.apply(this, args);
    };
};

/**
 * Creates new Image element and calls back with loaded image.
 * @param {string} url
 */
io.GetImage = function(url, callback) {
    var img = new Image();
    img.onload = function() {
        callback(img);
    };
    img.src = url;
};

picker.image_ = util.memoize.async(io.GetImage);

Then whenever I need the image, I call picker.image_ and get the cached one. It works flawlessly on the desktop, Chrome, Firefox, Safari, but on the iPad, I'm getting empty (not loaded) image back. Why is that? I really like this approach, it performs really well.

It looks like as if Mobile Safari drops the image data when it's removed from DOM. Could that be?

UPDATE: To clarify, the data being loaded is dynamic, therefore it's not the fittest use case for AppCache.

UPDATE*: There was not fully satisfying answer, here's my solution. Note that copy method is quite slow.

/**
 * Creates new Image element and calls back with loaded image.
 * @param {string} url
 */
var GetImage = function(url, callback) {
    var img = new Image();
    img.onload = function() {
        callback(img);
    };
    img.src = url;
};

/**
 * @param {number} num maximum number of stored images
 */
var ImagePool = function(num) {
    this.limit_ = num;
    this.canvases_ = {};
    this.order_ = [];
};

/**
 * Retrieve image from cache.
 *
 * @param  {string}   url      URL of request image
 * @param  {function(HTMLCanvasElement)} callback
 */
ImagePool.prototype.get = function(url, callback) {
    if (this.canvases_[url] !== undefined) {
        callback(this.copy_(url));
    } else {
        if (this.limit_ && this.order_.length == this.limit_) {
            delete this.canvases_[url];
            this.order_.pop();
        }
        GetImage(realUrl, function(img) {
            var c = document.createElement('canvas');
            c.width = img.width;
            c.height = img.height;
            var ctx = c.getContext('2d');
            ctx.drawImage(img, 0, 0);

            this.canvases_[url] = c;
            this.order_.unshift(url);
            callback(this.copy_(url));
        }.bind(this));
    }
};

/**
 * @param  {string} url
 * @return {HTMLCanvasElement}
 * @private
 */
ImagePool.prototype.copy_ = function(url) {
    var c = document.createElement('canvas'),
        cached = this.canvases_[url];
    c.width = cached.width;
    c.height = cached.height;
    var ctx = c.getContext('2d');
    ctx.drawImage(cached, 0, 0);
    return c;
};

Upvotes: 4

Views: 2365

Answers (4)

Onur Topal
Onur Topal

Reputation: 3061

I would use embedded image src base64 encoded. Just check here

So you can store your data as string as JSON if you want and get it back when ever you want you can also save it to localStorage if you are calling images from a different URL.

And yes it cause you a huge html content and storage space but in your case it definitely worth it.

Upvotes: 1

cuzzea
cuzzea

Reputation: 1535

if you attach the Image object to the DOM directly you might lose it on remove, you could try cloning it before deleting or attaching

function clone(obj) {
  if (null == obj || "object" != typeof obj) return obj;
  var copy = obj.constructor();
  for (var attr in obj) {
    if (obj.hasOwnProperty(attr)) copy[attr] = obj[attr];
  }
  return copy;
}

Upvotes: 3

Tom
Tom

Reputation: 8180

Have you tried just setting the appropriate headers at the server and letting the browser manage the caching instead of using this scheme?

Upvotes: 2

RustyTheBoyRobot
RustyTheBoyRobot

Reputation: 5955

I think your problem could be best solved by using an HTML 5 offline application cache. You list your resources that you would like cached, and after users visit a page that requests those resources, they are cached for later use. You would still have to have your UI wait until your images are loaded, but once they have been loaded, you won't have to worry about them being dropped simply because they're not in the DOM (This SO question suggests that images that are in the DOM, but not displayed on screen, are dropped as well).

Apparently, Safari Mobile has a 5MB cache limit, which can be increased by asking users to agree to expand it (Source). A comment in this blog post suggests that this expansion prompt is available as soon as iOS 4.

Helpful links:

Upvotes: 5

Related Questions