Wednesday, December 25, 2013

Presenting images in Android fast

The idea here was to be able to display very fast a slideshow of all the images in a directory. So, in principle I didn't want to be loading from flash/disk while displaying, therefore I wanted to save them in memory.

Top level, the app will simply store the images in some kind of memory structure and then, periodically, display one. The periodicity is done with an AsyncTask acting as timer. I.e., we call the task, in the task we have a 50ms wait (Thread.sleep(50);) and then when done we send a message back to the UI, which displays the new image, calls the task again and so on... See code below...

For the display of the image, we simply assign a Bitmap to the ImageView object using ImageView.setImageBitmap

So, finally, the question is how do we store all the images in memory for a quick retrieval when we need them. I show here two methods. Either one seems to work fine. The key is to be aware that we have limited memory resources and that Bitmaps can be quite big. To figure the resources use:
  1. final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
  2. Log.v("MyActivity","maxMemory: "+maxMemory);
which in my case (HTC ONE) returns: 196608 (that is 196MBytes).

If we start loading the pictures that the same phone has taken, each one will take 2688*1520*4 Bytes! We can see this by looking at the LogCat when we run the apps below. We see a message saying "Grow heap to XXX for 16343056-byte allocation". So, bottom line, either one of the two methods below will work only if the amount of images you have x the resolution of each one is kept within some boundaries... You can, of course, save the images in full resolution and resize them within your app or save them, to start with, in lower resolution (that is what I did here, using 640x360).

So, now to the two methods to store the pics. The first one uses simply an array of Bitmaps (duh!). PicActivity.java:
 package com.cell0907.pic;  
 import java.io.File;  
 import java.util.Random;  
 import com.cell0907.pic.R;  
 import android.os.Bundle;  
 import android.os.Environment;  
 import android.os.Handler;  
 import android.app.Activity;  
 import android.graphics.Bitmap;  
 import android.graphics.BitmapFactory;  
 import android.util.Log;  
 import android.widget.ImageView;  
 public class PicActivity extends Activity {  
      private Bitmap[] mMemoryCache; // A place to store our pics       
      private ImageView picture;  
      public static final int DONE=1;  
      private int numberofitems;  
      int i;  
      long startTime,stopTime;  
      @Override  
      protected void onCreate(Bundle savedInstanceState) {  
           super.onCreate(savedInstanceState);  
           setContentView(R.layout.activity_pic);  
           String root = Environment.getExternalStorageDirectory().toString();  
           File myDir = new File(root + "/DCIM/3D");   
     picture=(ImageView)findViewById(R.id.imageView1);  
     // FOR LOOP TO LOAD ALL THE PICS IN CACHE  
        File[] file_list = myDir.listFiles();   
        numberofitems=file_list.length;  
        mMemoryCache=new Bitmap[numberofitems];  
        Log.v("MyActivity","items: "+numberofitems);  
        for (int i=0;i<numberofitems;i++){  
             mMemoryCache[i]=BitmapFactory.decodeFile(file_list[i].getPath());  
        }        
     // RANDOM ACCESS TO PRESENT THE PICS VERY FAST  
        // We do this in a separate task, when finishes sends a message, the handler  
        // presents the image and send the task again...  
        i=0;  
        new Timer(getApplicationContext(),threadHandler).execute();  
      }  
      ////////////////////////////////////thread Handler///////////////////////////////////////  
   private Handler threadHandler = new Handler() {  
        public void handleMessage(android.os.Message msg) {       
             switch(msg.what){  
                     case DONE:  
                          //Random r = new Random();  
                       //int i=r.nextInt(numberofitems);   
                          startTime = System.nanoTime();  
                       picture.setImageBitmap(mMemoryCache[i]);  
                       i++;  
                       if (i==numberofitems) i=0;  
                       //if (i==4) i=0;  
                       long endTime = System.nanoTime();  
                       System.out.println(String.format("Elapsed time: %.2f ms", (float)(endTime - startTime)/1000000));  
                          new Timer(getApplicationContext(),threadHandler).execute();  
                          break;                           
             }  
        }  
   };  
 }  

And for the timer portion we will do (Timer.java):
 package com.cell0907.pic;  
 import android.content.Context;  
 import android.os.Handler;  
 import android.os.Message;  
 import android.util.Log;  
 import android.os.AsyncTask;  
 public class Timer extends AsyncTask<Void, Void, Void> {  
   Context mContext;  
      private Handler threadHandler;  
   public Timer(Context context,Handler threadHandler) {  
     super();  
     this.threadHandler=threadHandler;  
     mContext = context;  
       }  
   @Override  
      protected Void doInBackground(Void...params) {   
        try {  
                Thread.sleep(50);  
           } catch (InterruptedException e) {  
                // TODO Auto-generated catch block  
                e.printStackTrace();  
           }   
        Message.obtain(threadHandler, PicActivity.DONE, "").sendToTarget();   
         return null;  
   }  
 }  

AndroidManifest.xml:
 <?xml version="1.0" encoding="utf-8"?>  
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"  
   package="com.cell0907.pic"  
   android:versionCode="1"  
   android:versionName="1.0" >  
   <uses-sdk  
     android:minSdkVersion="12"  
     android:targetSdkVersion="17" />  
   <application  
     android:allowBackup="true"  
     android:icon="@drawable/ic_launcher"  
     android:label="@string/app_name"  
     android:theme="@android:style/Theme.NoTitleBar.Fullscreen" >  
     <activity  
       android:name="com.cell0907.pic.PicActivity"  
       android:label="@string/app_name"   
       android:screenOrientation="landscape">  
       <intent-filter>  
         <action android:name="android.intent.action.MAIN" />  
         <category android:name="android.intent.category.LAUNCHER" />  
       </intent-filter>  
     </activity>  
   </application>  
 </manifest>  

And the layout activity_pic.xml:
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
   xmlns:tools="http://schemas.android.com/tools"  
   android:layout_width="match_parent"  
   android:layout_height="match_parent"  
   tools:context=".PicActivity" >  
   <ImageView  
     android:id="@+id/imageView1"  
     android:layout_width="match_parent"  
     android:layout_height="match_parent"  
     android:layout_centerHorizontal="true"  
     android:layout_centerVertical="true"  
     android:src="@drawable/ic_launcher" />  
 </RelativeLayout>  

We profile the execution to see if there is any difference between this method and the next:
12-25 14:03:25.469: I/System.out(15336): Elapsed time: 0.70 ms
12-25 14:03:25.519: I/System.out(15336): Elapsed time: 0.24 ms
12-25 14:03:25.579: I/System.out(15336): Elapsed time: 0.61 ms
12-25 14:03:25.629: I/System.out(15336): Elapsed time: 0.18 ms
12-25 14:03:25.679: I/System.out(15336): Elapsed time: 0.21 ms
12-25 14:03:25.729: I/System.out(15336): Elapsed time: 0.18 ms
12-25 14:03:25.779: I/System.out(15336): Elapsed time: 0.15 ms
12-25 14:03:25.839: I/System.out(15336): Elapsed time: 0.18 ms
12-25 14:03:25.889: I/System.out(15336): Elapsed time: 0.18 ms
12-25 14:03:25.940: I/System.out(15336): Elapsed time: 0.15 ms
12-25 14:03:25.990: I/System.out(15336): Elapsed time: 0.15 ms
12-25 14:03:26.040: I/System.out(15336): Elapsed time: 0.18 ms
12-25 14:03:26.090: I/System.out(15336): Elapsed time: 0.18 ms
12-25 14:03:26.140: I/System.out(15336): Elapsed time: 0.18 ms
12-25 14:03:26.190: I/System.out(15336): Elapsed time: 0.18 ms
12-25 14:03:26.240: I/System.out(15336): Elapsed time: 0.18 ms
12-25 14:03:26.300: I/System.out(15336): Elapsed time: 0.21 ms

For the second method, I wanted to use the LruCache class.
 package com.cell0907.pic;  
 import java.io.File;  
 import java.util.Random;  
 import com.cell0907.pic.R;  
 import android.os.Bundle;  
 import android.os.Environment;  
 import android.os.Handler;  
 import android.app.Activity;  
 import android.graphics.Bitmap;  
 import android.graphics.BitmapFactory;  
 import android.support.v4.util.LruCache;  
 import android.util.Log;  
 import android.widget.ImageView;  
 public class PicActivity extends Activity {  
      private LruCache<String, Bitmap> mMemoryCache; // A place to store our pics       
      private ImageView picture;  
      public static final int DONE=1;  
      private int numberofitems;  
      int i;  
      long startTime,endTime;  
      @Override  
      protected void onCreate(Bundle savedInstanceState) {  
           super.onCreate(savedInstanceState);  
           setContentView(R.layout.activity_pic);  
           String root = Environment.getExternalStorageDirectory().toString();  
           File myDir = new File(root + "/DCIM/3D");   
     picture=(ImageView)findViewById(R.id.imageView1);  
     // SETUP THE CACHE  
        // Get max available VM memory, exceeding this amount will throw an  
        // OutOfMemory exception. Stored in kilobytes as LruCache takes an  
        // int in its constructor.  
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);  
        Log.v("MyActivity","maxMemory: "+maxMemory);  
        // Use half of the available memory at max for this memory cache.  
        final int cacheSize = maxMemory / 2;  
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {  
          @Override  
          protected int sizeOf(String key, Bitmap bitmap) {  
            // The cache size will be measured in kilobytes rather than  
            // number of items.  
            return bitmap.getByteCount() / 1024;  
          }  
        };  
     // FOR LOOP TO LOAD ALL THE PICS IN CACHE  
        File[] file_list = myDir.listFiles();   
        numberofitems=file_list.length;  
        Log.v("MyActivity","items: "+numberofitems);  
        String imageKey;  
        for (int i=0;i<numberofitems;i++){  
             imageKey = String.valueOf(i);  
             addBitmapToMemoryCache(imageKey,BitmapFactory.decodeFile(file_list[i].getPath()));  
        }        
     // RANDOM ACCESS TO PRESENT THE PICS VERY FAST  
        // We do this in a separate task, when finishes sends a message, the handler  
        // presents the image and send the task again...  
        i=0;  
        new Timer(getApplicationContext(),threadHandler).execute();  
      }  
      public void addBitmapToMemoryCache(String key, Bitmap bitmap) {  
        if (getBitmapFromMemCache(key) == null) {  
          mMemoryCache.put(key, bitmap);  
        }  
      }  
      public Bitmap getBitmapFromMemCache(String key) {  
        return mMemoryCache.get(key);  
      }  
      ////////////////////////////////////thread Handler///////////////////////////////////////  
   private Handler threadHandler = new Handler() {  
        public void handleMessage(android.os.Message msg) {       
             switch(msg.what){  
                     case DONE:  
                       //Random r = new Random();  
                       //int i=r.nextInt(numberofitems);  
                       startTime = System.nanoTime();  
                       picture.setImageBitmap(getBitmapFromMemCache(String.valueOf(i)));  
                       i++;  
                       if (i==numberofitems) i=0;  
                       //if (i==4) i=0;  
                       long endTime = System.nanoTime();  
                       System.out.println(String.format("Elapsed time: %.2f ms", (float)(endTime - startTime)/1000000));  
                       new Timer(getApplicationContext(),threadHandler).execute();  
                          break;  
             }  
        }  
   };  
 }  

For the timer, manifest and layout we used the same as the first case. Notice that in both cases I had provision to display the pics in random access. I left it there commented out, just for reference...

Profiling this 2nd method, it seems that, curiously, this method actually seems to be slower than simple array of Bitmaps!
12-25 13:58:43.408: I/System.out(14553): Elapsed time: 0.37 ms
12-25 13:58:43.458: I/System.out(14553): Elapsed time: 0.37 ms
12-25 13:58:43.508: I/System.out(14553): Elapsed time: 0.34 ms
12-25 13:58:43.558: I/System.out(14553): Elapsed time: 0.92 ms
12-25 13:58:43.608: I/System.out(14553): Elapsed time: 0.34 ms
12-25 13:58:43.668: I/System.out(14553): Elapsed time: 0.43 ms
12-25 13:58:43.718: I/System.out(14553): Elapsed time: 0.37 ms
12-25 13:58:43.768: I/System.out(14553): Elapsed time: 0.34 ms
12-25 13:58:43.819: I/System.out(14553): Elapsed time: 0.46 ms

Oh well, good to know... I just wonder why then somebody would use the cache approach (?)
Cheers!!

PS.: Please, click here to see an index of other posts on Android. 

No comments:

Post a Comment