Implementing a Local Cache to Manage Resources

MIDlet resources such as images, icons, video clips, and ringtones can be stored within the MIDlet suite JAR file. But keeping such resources within the MIDlet suite itself can be expensive in terms of application size. To minimize the size of the applications, resources can be download the first time the application runs, or just on-demand when needed using lazy initialization, storing such resources locally for later use. Using a resource cache not only hides where and how resources are found, downloaded, and stored, but it also results in lowering the overall MIDlet suite size. Another benefit is that resources can be updated on the field.

Contents
 
Using a local cache
Using the ImageCache Class
The ImageCache.java Class
The ImageCacheListener.java Interface
The ImageRmsUtils.java Utility Class
The NetworkUtils.java Utility Class
Considerations and Possible Enhancements
Conclusion
Resources
Acknowledgements
About the Author
 

In this article we will cover an image cache. The local cache covered in this article uses the code and the lessons previously covered in the article Externalizing Resources - Persisting Images in RMS. Note that the concepts described here are applicable to other types of resources beyond images.

An image cache provides a number of benefits, including externalizing image resources from the application, hiding the source, management and retrieval of images, downloading images only when needed or for updates, and keeping only the most-used images around. All these benefits help keep application size smaller. The following block diagram illustrates the elements of the image cache:

Figure 1: Image Cache Block Diagram
 

Using the image cache is pretty straightforward:

  1. The application gets an instance of the image cache
  2. The application retrieves the resource via the cache by name

Then, the image cache:

  1. Attempts to retrieve the resource from memory
  2. If not in memory, tries to load it from storage
  3. If not in storage, it dispatches a thread to load it from the network

This sequence and logic is illustrated in the following activity diagram:

Figure 2: Activity Diagram: Image Cache Get Resource by Name
 

The image cache is implemented by class ImageCache, the interface ImageCacheListener that defines a listener interface for event notifications, and some helper utility methods. All together they are about 17 Kilobytes in size-smaller than embedding a number of static images within the MIDlet suite. The following two class inheritance and association diagrams illustrate the MIDlet and Image cache relationships:

Figure 3: MIDlet / Image Cache Inheritance and Association Diagram
 
 
Figure 4: Image Cache Inheritance and Association Diagram
 

The image cache relies on the Record Management System (RMS) to store images locally for later use. Within RMS, an image record store is created which contains individual images with one image per record. The format for each record is as follows:

Figure 5: ImageCache RMS Record Store Record Formats
 

To manage the image record store, the image cache uses the utility class ImageRmsUtils that is described in the technical tip Externalizing Resources - Persisting Images in RMS. Each record has the following fields: the resource name, width and height, timestamp, length, and the image raw bytes, which is all the minimal information that is needed to manage the images in the cache. To manage the network connectivity, the image cache uses the utility class NetworkUtils that is described in the technical tip Accessing a Resource over HTTP.

Recall that a MIDlet can load an image that is stored in the MIDlet suite's JAR file by calling the method Class.getResourceAsStream(String resouceName) or Image.createImage(String resourceName).

Loading an image from the image cache is very simple. The ImageCache follows a Singleton design pattern, so the application's first step is to retrieve the singleton instance:

ImageCache imageCache = ImageCache.getInstance(); // get the singleton instance
 

Once the MIDlet has gained access to the image cache object instance, all it needs to do is call method imageCache.getImageResource(String name) to retrieve a specific image by name. By convention, the image's name is its Uniform Resource Identifier (URI) - the fully qualified URI that uniquely identifies the image locally, while also specifying where to find it over the network:

private static final String IMG_NAME = " http://javamedeveloper.com/ota/test.png";
:

Image img = imageCache.getImageResource(IMG_NAME); // load the image
 

The above call will search, try to load, store, and return the named resource.

The next sections take a closer look at the image cache Java files ImageCache.java and ImageCacheListener.java.

The image cache is implemented in ImageCache.java. Figure 3 and Figure 4 illustrate the image cache class diagrams, and relationships to the MIDlet application, and the utility classes.

As previously mentioned, ImageCache is a singleton, so its constructor is private, and the getInstance() method is used to retrieve the singleton instance. The next code snippet shows the beginning of the ImageCache class:

/*
 * ImageCache.java
 * Provides a image cache utilities
 *
 * Author: C. Enrique Ortiz, http://CEnriqueOrtiz.com
 *
 */

package com.javamedeveloper.ttips.utils;

import java.io.IOException;
import java.io.InputStream;
import java.util.Hashtable;
import javax.microedition.lcdui.*;

/**
 * Class ImageCache provides a number of image 
 * methods for image caching
 *
 * @author C. Enrique Ortiz
 */
public final class ImageCache implements Runnable {

    /** Singleton instance */
    private static ImageCache instance;

    /** Name of RMS Record Store */
    private static final String IMAGES_RS = "IMAGES_RS";

    /** In-memory image collection */
    private Hashtable images = new Hashtable();

    /** Image Cache event listener */
    private ImageCacheListener listener;

    /** Uri used by thread */
    private String thPngUri;

    /** Number of seconds for cache expiration */
    private static final int EXPIRE_TIME_SECS = 60*60*24*3; // 3 days    
 

The above snippet defines various member variables and constants-to hold the private static singleton instance, the name of the RMS record store, a Hashtable to hold images in-memory, the active ImageCacheListener if any, the active image URI, and the default images expiration time.

Next are the singleton's constructor and getInstance() methods:

    /**
     * Private Constructor! Creates a new instance of ImageCache
     */
    private ImageCache() {
    }

    /**
     * Get singleton instance
     */
    synchronized public static ImageCache getInstance() {
        if (instance == null) {
            instance = new ImageCache();
        }
        return instance;
    }
 

Again, note that the constructor is private, and the static public getInstance() method is synchronized, and returns the singleton instance.

Next is the setListener() method, used to register a listener that will receive cache event notifications. Later in the article I will cover the ImageCacheListener interface:

    /**
     * Sets the image listener
     *
     * @param listener the listener to invoke
     */
    public void setListener(ImageCacheListener listener) {
        this.listener = listener;
    }
 

Next are the ImageCache public methods. The first exposed method is isImageLoaded() which is a helper method that provides a quick way to test if the image is already loaded into memory:

    /**
     * Test if image is already loaded into memory
     *
     * @param resourceName the name of the resource to test for
     */
    public boolean isImageLoaded(String resourceName) {
        return images.get(resourceName) != null;
    }            
 

The next method, getImageResource(String resourceName) is the main method used by applications to retrieve images. This method is the one the implements the logic described in the activity diagram in Figure 2:

   /**
     * Get Image Resource by name
     *
     * @param   resourceName is the name of the resource to load
     * @return  the loaded image or null
     */
    public Image getImageResource(String resourceName) {
        Image img = null;
        // Try to load from memory
        img = (Image)images.get(resourceName);
        // If not in memory, load from RMS, network, in that order
        if (img == null) {
            // Try to load from RMS
            img = ImageRmsUtils.loadPngFromRMS(IMAGES_RS, resourceName);
            if (img != null) {
                // Keep track of loaded Images
                images.put(resourceName, img);
            } else {
                // If not in RMS, try to load over the network
                // dispatch network retrieval
                getImageOverHttp(resourceName); // name is URi
            }
        }
        return img; // returns null for network retrievals
    }
 

As illustrated in the activity diagram, method getImageResource(name) will first try to load the image from memory (from the Hashtable), and if not found in memory, it tries to load from storage (RMS), and if the resource can't be loaded from storage, the resource is loaded from the network.

The next method loadImageFromRMS() is used to load an image from RMS, and it can also be used to quickly test if an image is already cached into RMS (if not found in RMS it will return null):

    /**
     * Loads Image From RMS
     *
     * @param resourceName the name of the resource to load from RMS
     *
     * @return the image if found, null otherwise
     */
    public Image loadImageFromRMS(String resourceName) {
        Image img = null;
        img = ImageRmsUtils.loadPngFromRMS(IMAGES_RS, resourceName);
        return img;
    }
 

For RMS operations, the image cache uses the utility class ImageRmsUtils explained in the technical tip Externalizing Resources - Persisting Images in RMS. To retrieve the image over the network, private method getResourceOverHttp(), described shortly, dispatches the following thread of execution-remember to always dispatch network operations on its own thread of execution to avoid blocking the main system threads:

    /**
     * Thread of execution
     */
    public void run() {
        InputStream is = null;
        try {
            is = NetworkUtils.getResourceOverHTTP(
                    thPngUri, NetworkUtils.MIMETYPE_IMAGE_PNG);
        } catch (IOException ioe) {
            // Log error, return
            return;
        }
        notifyHttpPngRetrievalComplete(thPngUri, is);
    }
 

The above method invokes the method NetworkUtils.getDocumentOverHTTP(), which is described in the technical tip Accessing a Resource over HTTP. The method uses HTTP to retrieve the named resource, returning it as an InputStream. After the method getDocumentOverHTTP() completes, with error or not, the method notifyHttpPngRetrievalComplete() is called to complete the network operation.

The next set of operations clears the cache and checks for the expiration of image resources. The method clearImageCache() is used to clear all images that are stored in the cache, both in-memory and local:

    /**
     * Clears the Images Cache, both storage and in-memory
     */
    public void clearImageCache() {
        ImageRmsUtils.clearImageRecordStore(IMAGES_RS);
        images.clear();
    }
 

The method expireCache() is used to remove images from the local storage (RMS) that are older than the specified time:

    /**
     * Expires (clears) old cached images from local store
     *
     * @param expireSeconds the number of seconds for the expiration
     *
     * @return the number of images that expired
     */
    public int expireCache(int expireSeconds) {
        return ImageRmsUtils.clearExpiredImages(
                IMAGES_RS,
                ((expireSeconds>0)?expireSeconds:EXPIRE_TIME_SECS));
    }
 

The rest of the methods in class ImageRmsUtils are private. Method getImageOverHttp() is called by getImageResource() to dispatch the network thread of execution described above:

    /**
     * Get Image over HTTP
     *
     * @param pngUri The URI to load
     */
    private void getImageOverHttp(String pngUri) {
        thPngUri = pngUri; // set class-wide member, used by thread
        Thread th = new Thread(this);
        th.start();
    }
 

The next method notifyHttpPngRetrievalComplete() is invoked to notify the application that the network operation has completed; note the method is synchronized to serialize notifications and related processing:

   /**
     * This method is called by the getPngOverHttp to notify when
     * image retrieval has completed.
     *
     * @param resourceName is the name of the resource
     * @param pngInputStream is the InputStream for the PNG
     */
    synchronized private void notifyHttpPngRetrievalComplete(
            String resourceName, InputStream pngInputStream) {
        // Create Image from input stream, add it to image collection
        if (pngInputStream != null) {
            Image pngImage = null;
            try {
                // Creates image from decoded image data obtained
                //  from an InputStream. After this method completes 
                //  the stream is left open and its current position 
                //  is undefined.
                pngImage = Image.createImage(pngInputStream);
                // Keep track of loaded Images
                images.put(resourceName, pngImage);
                // Save PngImage into RMS.
                //  Note that input stream is null if PNG 
                // retrieval was unsuccessful.
                ImageRmsUtils.savePngImage(
                    IMAGES_RS, resourceName, pngImage);
                if (listener != null) {
                    listener.onEvent(
                            ImageCacheListener.EVENT_LOADED,
                            resourceName,
                            pngImage);
                }

            } catch (IOException ioe) {
                // Log error
            } finally {
                // Close after use
                if (pngInputStream != null) {
                    try {
                        pngInputStream.close();
                    } catch (IOException ignore) {
                        // ignore
                    }
                }
            }

        }
    }
 

Of interest is the listener.onEvent() statement. ImageCache supports an event listener per instance that will be invoked when cache events such as "load complete" have occurred. ImageCacheListher.java is covered next.

The interface ImageCacheListener defines an image cache event listener. By implementing this interface, and registering with the image cache, an application receives event notifications. In this example only one event, image LOADED, has been defined to indicate when image loading has completed.

/*
 * ImageCacheListener.java
 * Defines the behavior of ImageCacheListeners
 *
 * Author: C. Enrique Ortiz, http://CEnriqueOrtiz.com
 *
 */

package com.javamedeveloper.ttips.utils;

import javax.microedition.lcdui.*;

/**
 * Image Utils Event Listener
 */
public interface ImageCacheListener {
    /** Event imaged loaded */
    public final static int EVENT_LOADED = 1;

    /**
     * Callback invoked by ImageUtils when an event has occured
     *
     * @param event is the image event
     * @param name is the name of the image that caused the event
     * @param image is the image that cause the event
     */
    public void onEvent(int event, String name, Image image);
}
 

The following listing shows a MIDlet snippet that implements the ImageCacheListener:

import com.javamedeveloper.ttips.utils.*;
:

/**
 * BasicMIDlet
 * @author C. Enrique Ortiz
 */
public class BasicMIDlet extends MIDlet 
        implements CommandListener, ImageCacheListener {

    /** The Image Cache */
    private ImageCache imageCache;        
    :
    :

    imageCache = ImageCache.getInstance();
    imageCache.setListener(this);
    :
    :

    /**
     * 
     * Callback invoked by ImageCache when an event has occured
     * 
     * 
     * @param event is the image event
     * @param name is the name of the image that caused the event
     * @param image is the image that cause the event
     */
    public void onEvent(int event, String name, Image image) {
        if (event == ImageCacheListener.EVENT_LOADED) {
                form.append(image);
        }
    }

}
 

Next, let's cover the two utility classes used by the image cache to manage the local record store, and to retrieve an image over the network.

The ImageCache uses the utility class ImageRmsUtils to manage the local record store. The utility class is described in technical tip Externalizing Resources - Persisting Images in RMS, and it exposes the following four methods:

  • void savePngImage(String recordStore, String resourceName, Image image)
  • Image loadPngFromRMS(String recordStore, String resourceName)
  • int clearExpiredImages(String recordStore, int expireNumSeconds)
  • void clearImageRecordStore(String recordStore)
 

Please refer to the technical tip for more information.

The ImageCache uses the utility class NetworkUtils to retrieve the image resource over the network over HTTP. The utility class is described in technical tip Accessing a Resource over HTTP, and it exposes one method to get the resource and return an InputStream to the resource:

static public InputStream getResourceOverHTTP(
       String uri, 
       String mimeType) throws IOException
 

Please refer to the technical tip for more information.

The image cache can be enhanced in different ways. One possible enhancement is to add a method to configure the caching system itself, to account for parameters such as maximum vs. remaining RMS size, record size, and volatile memory.

Another enhancement is adding expirations to loaded images so that rarely accessed images are removed from the handset and reloaded when needed-helping maximize memory and storage utilization. As part of expiration, a cyclic thread could be implemented to automatically check for image expiration every so often.

You could also enhance the image cache to make the cache generic and support different types of resources. You would modify the RMS record format described above to include a resource type, other fields as needed, and implement the appropriate resource management methods for the particular resource type.

There are other considerations to keep in mind:

  • If the sum of all the resources to be consumed by the application is less than the size of the caching system, and/or the application doesn't require support for resource updates on the field, then using the caching system might not be necessary.

  • Because memory and storage are critical resources, caching might not be the best approach for memory and storage-intensive applications.

  • Some MIDP implementations are single-threaded.

  • Keep in mind the shared use of the RMS by both the caching system and the application itself.

Here, you've seen the design and implementation of an image cache for MIDP that helps MIDP applications minimize their size. It allows applications to update resources, in this example, images while in the field. The caching system helps keep resources separate from the MIDlet suite itself, hiding the source, management, and retrieval of resources from the application. And it helps maximize storage utilization by keeping only the most-frequently used resources local.

Many thanks to Boris Kvartskhava for his feedback, and helping improve this article.

C. Enrique Ortiz is a mobile technologist, software architect and developer, and a writer and blogger. He has been author or co-author of many publications, and is an active participant in the Java mobility community and in various Java ME Expert Groups. Enrique holds a B.S. in Computer Science from the University of Puerto Rico and has more than 17 years of software engineering, product development, and management experience.

<script type="text/javascript"> </script>  
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值