9

Description of the problem: I'm creating a scrollable list of articles with thumbnails that's populated by my SQLite database. In general, it's "working" except being slow:

The images load very slowly... I thought using the "Universal Image Loader" cached the images on the device, and that that would make them appear to just scroll into view if you'd already viewed them (or at least close to that). But - when you drag up/down, none of the images are there, then 3-5 seconds later, the images start popping in (as though it's re-downloading them)

I'm changing the visibility of the thumbnail boxes on the fly, but that is working flawlessly - they don't appear to change - they just scroll into view or not, no flashing or anything. (but then the images don't appear for another few seconds).

I've tested by removing my php script after scrolling around... when I scroll back to prev spot, the images don't show up - making me assume it's loading from my PHP script EVERY time.

But according to the docs: "UsingFreqLimitedMemoryCache (The least frequently used bitmap is deleted when cache size limit is exceeded) - Used by default"

Details:

In my ArticleEntryAdapter.js I have:

@Override
public View getView(final int position, final View convertView, final ViewGroup parent) {

    // We need to get the best view (re-used if possible) and then
    // retrieve its corresponding ViewHolder, which optimizes lookup efficiency
    final View view = getWorkingView(convertView);
    final ViewHolder viewHolder = getViewHolder(view);
    final Article article = getItem(position);

    // Set the title
    viewHolder.titleView.setText(article.title);

    //Set the subtitle (subhead) or description
    if(article.subtitle != null)
    {
        viewHolder.subTitleView.setText(article.subtitle);
    }
    else if(article.description != null)
    {
        viewHolder.subTitleView.setText(article.description);
    }

    ImageLoader imageLoader = ImageLoader.getInstance();

    imageLoader.displayImage("", viewHolder.thumbView); //clears previous one
    if(article.filepath != null && article.filepath.length() != 0) {
        imageLoader.displayImage(
            "http://img.sltdb.com/processes/resize.php?image=" + article.filepath + "&size=100&quality=70",
            viewHolder.thumbView
            );
        viewHolder.thumbView.setVisibility(View.VISIBLE);
    } else {
        viewHolder.thumbView.setVisibility(View.GONE);
    }

    return view;
}

As far as the images being incorrect - it's not often, but sometimes while scrolling, I'll see 2 of the same image, and when I look at the articles, they're not at all related (ie no chance of actually having the same image) So - I scroll away from it, and back, and it's no longer the incorrect image.

NOTE: I'm new to Java/Android - you probably already noticed that.

More code per comment-request:

private View getWorkingView(final View convertView) {
    // The workingView is basically just the convertView re-used if possible
    // or inflated new if not possible
    View workingView = null;

    if(null == convertView) {
        final Context context = getContext();
        final LayoutInflater inflater = (LayoutInflater)context.getSystemService
          (Context.LAYOUT_INFLATER_SERVICE);

        workingView = inflater.inflate(articleItemLayoutResource, null);
    } else {
        workingView = convertView;
    }

    return workingView;
}

UPDATE: My Manifest file has:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

But the cache folder I found is completely empty:

mnt
  -sdcard
    -Android
      -data
        -com.mysite.news
          -cache
            -uil-images
Dave
  • 28,833
  • 23
  • 113
  • 183

4 Answers4

19

I was experiencing similar issues with images in list view. Possibly this answer will correct your wrong image problem.

I just downloaded the sample project with UniversalImageLoader and it exhibits the same behavior you are describing.

A few notes so far from looking through the source code.

public static final int DEFAULT_THREAD_POOL_SIZE = 3;
public static final int DEFAULT_THREAD_PRIORITY = Thread.NORM_PRIORITY - 1;
public static final int DEFAULT_MEMORY_CACHE_SIZE = 2 * 1024 * 1024; // bytes

This says that at any time there will be three threads downloading and max of 2MB of images. How big are the images you are downloading? Also are you caching to disk? If so, that will be slow.

To configure some of the basic options in the ImageLoader you will need to pass in to displayImage:

 DisplayImageOptions options = new DisplayImageOptions.Builder()
     .showStubImage(R.drawable.stub_image)
     .cacheInMemory()
     .cacheOnDisc()
     .build();

I would also like you to try these options:

ImageLoaderConfiguration imageLoaderConfiguration = new ImageLoaderConfiguration.Builder(this)
    .enableLogging()
    .memoryCacheSize(41943040)
    .discCacheSize(104857600)
    .threadPoolSize(10)
    .build();

imageLoader = ImageLoader.getInstance();
imageLoader.init(imageLoaderConfiguration);

With my testing, the images are on disk, but loading is still slow.

After extensive testing, I determined the primary issue is that UniversalImageLoader is just slow. Specifically, the ImageLoader and LoadAndDisplayImageTask are holding up the works. I (very quickly) rewrote the LoadAndDisplayImageTask as an AsyncTask and it immediately performed better. You can download the forked version of the code on GitHub.

Universal Image Loader with AsyncTasks

Community
  • 1
  • 1
Cameron Lowell Palmer
  • 21,528
  • 7
  • 125
  • 126
  • The incorrect image thing happens not very often - the bigger issue is the slow speed of the images loading. If they're cached, shouldn't they just appear to slide in as opposed to being delayed by multiple seconds? Though I appreciate the note and will look into the other questions answers. – Dave Jul 16 '12 at 14:24
  • Without that line, the previous image in that recycled view shows up until the new image is loaded in. I took it out, and there is still a delay. It doesn't appear to be caching anything on the device - if I load the app, scroll down, remove my PHP code, then scroll back up, the images do not appear. – Dave Jul 16 '12 at 15:13
  • The images are not being cached on disk (I'd LIKE them to though). I'll try your suggestions, and I'm hopeful, but according to the docs, the DEFAULT should be caching them to disk. My images are tiny - 100x100px – Dave Jul 17 '12 at 12:47
  • ImageLoaderConfiguration also allows you to specify the maximum image size. I didn't try that option but it might help. – Cameron Lowell Palmer Jul 17 '12 at 13:22
  • I'm lost - I thought it WAS an async task already... it's not holding anything up - the images are just loading slowly - what am I missing? Also - how do I get it to actually CACHE the images locally - that's the big part of my problem currently. My images are tiny 100x100px – Dave Jul 17 '12 at 13:34
  • AsyncTask is an Android API provided method of doing something on a background thread and returning the results on the main thread. They tend to be quite fast. The UniversalImageLoader has 3 threads by default doing the download work and one to update the image view on the main thread. So yes, both the UIL and my reworking using the AsyncTask class are 'Asynchronous' but I think their method leaves a lot to be desired with regards to performance. – Cameron Lowell Palmer Jul 17 '12 at 13:45
  • The suggested settings don't visibly change anything. I'll gladly try your altered version but it still doesn't fix the main problem - it's not caching locally. – Dave Jul 17 '12 at 14:35
  • Are you sure it isn't caching locally? I stepped through the code and it is pulling the data from disk, it is just slow at loading the imageview, giving you the impression it isn't caching on disk. Please test the version I built. I just tested it on Samsung Galaxy S2 and it was fast. – Cameron Lowell Palmer Jul 17 '12 at 15:12
  • When I commented out my PHP script, no images showed up. (after having previously scrolled up and down numerous times and all images loading at least once) – Dave Jul 17 '12 at 15:13
  • OK. Well try that out with my version. If it works, I can cleanup the code more and do some bug fixing. – Cameron Lowell Palmer Jul 17 '12 at 15:18
  • Why would your version work - did you change something related to caching? – Dave Jul 17 '12 at 15:19
  • :) Basically the code works like this... You pass in an ImageView and a URI. Then the background thread is supposed to check for the correct Bitmap and return it to the main thread that updates the ImageView with the retrieved BitMap. The bitmap might be from memory, disk, or cache. I changed the frontend bit that was retrieving the file from cache or internet and attaching it to the image view. – Cameron Lowell Palmer Jul 17 '12 at 15:31
  • I understood that part... but it doesn't solve my main problem - the images aren't being cached. (updated answer to show empty cache folder) – Dave Jul 17 '12 at 15:33
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/14008/discussion-between-dave-and-cameron-lowell-palmer) – Dave Jul 17 '12 at 16:36
  • Your modified universal image loader is not good. I already tried this – sam_k May 11 '13 at 10:21
  • Can u please help me to figure out the issue – sam_k May 11 '13 at 10:24
  • 3
    I think you forgot to call `.defaultDisplayImageOptions(displayImageOptions)` when building your `ImageLoaderConfiguration`. If you don't do this, those `DisplayImageOptions` options are not applied at all. I was experiencing the same slowness, which went away after applying the `DisplayImageOptions`. – pimguilherme Nov 05 '14 at 18:18
  • 2+ years ago. I'm guessing a lot changed – Cameron Lowell Palmer Nov 05 '14 at 19:57
3

An alternate solution is "RemoteImageView" from the ignition open source project.

http://kaeppler.github.com/ignition-docs/ignition-core/apidocs/com/github/ignition/core/widgets/RemoteImageView.html

Effectively, RemoteImageView extends ImageView and does all the fetching/caching for you behind the scenes.

While it doesn't necessarily solve your problem you listed, it might be worth investigating as an alternate solution.

EDIT: I would highly recommend Picasso if you're still in need of a remote image solution. I've replaced RemoteImageView in my applications with Picasso: http://square.github.io/picasso/

theelfismike
  • 1,621
  • 12
  • 18
  • Are there any instruction / tutorial sites for that? – Dave Jul 16 '12 at 20:55
  • The example Activity in the ignition project has a pretty straightforward usage here: https://github.com/kaeppler/ignition/blob/master/ignition-core/ignition-core-samples/src/com/github/ignition/samples/core/RemoteImageViewActivity.java – theelfismike Jul 16 '12 at 22:17
1

I suspect the resize.php is slow, especially if it has to resize large pages, and several requests are received. And somehow caching in imageLoader is not done.

First I would do the rest after the image loading: subtitle, description and all. Because if the image loading takes too long, there is a more instantaneous effect, if description and all the rest appear together. Ordinarily your order of statements is fine.

The answer of @CameronLowellPallmer takes care of switched images and caching.

Joop Eggen
  • 107,315
  • 7
  • 83
  • 138
  • The resize.php files are cached and load instantly when you hit the url in a browser (since they're TINY files). I'm not sure what you mean by "rest after the image loading..." (remember I'm a noob :) – Dave Jul 16 '12 at 14:20
  • I moved the image stuff before the title...etc stuff - no change. – Dave Jul 16 '12 at 14:29
  • (and more importantly, the images are supposed to be cached on the device - if it's hitting my PHP script every time, that's an issue unto itself!) – Dave Jul 16 '12 at 14:32
  • I have put clarification into my answer. If you have control over the PHP, you can verify the caching behaviour on android, and whether resize serves from _its_ cache. – Joop Eggen Jul 16 '12 at 14:50
  • It is either a) not caching locally or b) not using the local cache - when I remove my PHP code (after scrolling around a bit), the images no longer load at all. – Dave Jul 16 '12 at 15:14
0

This class worked for me:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.HttpParams;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.util.Log;
import android.widget.ImageView;

public class ImageDownloader {

    Map<String,Bitmap> imageCache;

    public ImageDownloader(){
        imageCache = new HashMap<String, Bitmap>();

    }

    //download function
    public void download(String url, ImageView imageView) {
         if (cancelPotentialDownload(url, imageView)&&url!=null) {

             //Caching code right here
             String filename = String.valueOf(url.hashCode());
             File f = new File(getCacheDirectory(imageView.getContext()), filename);

              // Is the bitmap in our memory cache?
             Bitmap bitmap = null;

              bitmap = (Bitmap)imageCache.get(f.getPath());
                BitmapFactory.Options bfOptions=new BitmapFactory.Options();
                bfOptions.inDither=false;                     //Disable Dithering mode
                bfOptions.inPurgeable=true;                   //Tell to gc that whether it needs free memory, the Bitmap can be cleared
                bfOptions.inInputShareable=true;              //Which kind of reference will be used to recover the Bitmap data after being clear, when it will be used in the future
                bfOptions.inTempStorage=new byte[32 * 1024]; 
                FileInputStream fs=null;

              if(bitmap == null){

                  //bitmap = BitmapFactory.decodeFile(f.getPath(),options);
                  try {
                      fs = new FileInputStream(f);
                        if(fs!=null) bitmap=BitmapFactory.decodeFileDescriptor(fs.getFD(), null, bfOptions);
                    } catch (IOException e) {
                        //TODO do something intelligent
                        e.printStackTrace();
                    } finally{ 
                        if(fs!=null) {
                            try {
                                fs.close();
                            } catch (IOException e) {
                                // TODO Auto-generated catch block
                                e.printStackTrace();
                            }
                        }
                    }

                  if(bitmap != null){
                      imageCache.put(f.getPath(), bitmap);
                  }

              }
              //No? download it
              if(bitmap == null){
                  BitmapDownloaderTask task = new BitmapDownloaderTask(imageView);
                  DownloadedDrawable downloadedDrawable = new DownloadedDrawable(task);
                  imageView.setImageDrawable(downloadedDrawable);
                  task.execute(url);
              }else{
                  //Yes? set the image
                  imageView.setImageBitmap(bitmap);
              }
         }
    }

    //cancel a download (internal only)
    private static boolean cancelPotentialDownload(String url, ImageView imageView) {
        BitmapDownloaderTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView);

        if (bitmapDownloaderTask != null) {
            String bitmapUrl = bitmapDownloaderTask.url;
            if ((bitmapUrl == null) || (!bitmapUrl.equals(url))) {
                bitmapDownloaderTask.cancel(true);
            } else {
                // The same URL is already being downloaded.
                return false;
            }
        }
        return true;
    }

    //gets an existing download if one exists for the imageview
    private static BitmapDownloaderTask getBitmapDownloaderTask(ImageView imageView) {
        if (imageView != null) {
            Drawable drawable = imageView.getDrawable();
            if (drawable instanceof DownloadedDrawable) {
                DownloadedDrawable downloadedDrawable = (DownloadedDrawable)drawable;
                return downloadedDrawable.getBitmapDownloaderTask();
            }
        }
        return null;
    }

    //our caching functions
    // Find the dir to save cached images
    public static File getCacheDirectory(Context context){
        String sdState = android.os.Environment.getExternalStorageState();
        File cacheDir;

        if (sdState.equals(android.os.Environment.MEDIA_MOUNTED)) {
            File sdDir = android.os.Environment.getExternalStorageDirectory();  

            //TODO : Change your diretcory here
            cacheDir = new File(sdDir,"data/tac/images");
        }
        else
            cacheDir = context.getCacheDir();

        if(!cacheDir.exists())
            cacheDir.mkdirs();
            return cacheDir;
    }

    private void writeFile(Bitmap bmp, File f) {
          FileOutputStream out = null;

          try {
            out = new FileOutputStream(f);
            bmp.compress(Bitmap.CompressFormat.PNG, 80, out);
          } catch (Exception e) {
            e.printStackTrace();
          }
          finally { 
            try { if (out != null ) out.close(); }
            catch(Exception ex) {} 
          }
    }
    ///////////////////////

    //download asynctask
    public class BitmapDownloaderTask extends AsyncTask<String, Void, Bitmap> {
        private String url;
        private final WeakReference<ImageView> imageViewReference;

        public BitmapDownloaderTask(ImageView imageView) {
            imageViewReference = new WeakReference<ImageView>(imageView);
        }

        @Override
        // Actual download method, run in the task thread
        protected Bitmap doInBackground(String... params) {
             // params comes from the execute() call: params[0] is the url.
             url = (String)params[0];
             return downloadBitmap(params[0]);
        }

        @Override
        // Once the image is downloaded, associates it to the imageView
        protected void onPostExecute(Bitmap bitmap) {
            if (isCancelled()) {
                bitmap = null;
            }

            if (imageViewReference != null) {
                ImageView imageView = imageViewReference.get();
                BitmapDownloaderTask bitmapDownloaderTask = getBitmapDownloaderTask(imageView);
                // Change bitmap only if this process is still associated with it
                if (this == bitmapDownloaderTask) {
                    imageView.setImageBitmap(bitmap);

                    //cache the image


                    String filename = String.valueOf(url.hashCode());
                    File f = new File(getCacheDirectory(imageView.getContext()), filename);

                    imageCache.put(f.getPath(), bitmap);

                    writeFile(bitmap, f);
                }
            }
        }


    }

    static class DownloadedDrawable extends ColorDrawable {
        private final WeakReference<BitmapDownloaderTask> bitmapDownloaderTaskReference;

        public DownloadedDrawable(BitmapDownloaderTask bitmapDownloaderTask) {
            super(Color.BLACK);
            bitmapDownloaderTaskReference =
                new WeakReference<BitmapDownloaderTask>(bitmapDownloaderTask);
        }

        public BitmapDownloaderTask getBitmapDownloaderTask() {
            return bitmapDownloaderTaskReference.get();
        }
    }

    //the actual download code
    static Bitmap downloadBitmap(String url) {
        HttpParams params = new BasicHttpParams();
        params.setParameter(CoreProtocolPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_1);
        HttpClient client = new DefaultHttpClient(params);
        final HttpGet getRequest = new HttpGet(url);

        try {
            HttpResponse response = client.execute(getRequest);
            final int statusCode = response.getStatusLine().getStatusCode();
            if (statusCode != HttpStatus.SC_OK) { 
                Log.w("ImageDownloader", "Error " + statusCode + " while retrieving bitmap from " + url); 
                return null;
            }

            final HttpEntity entity = response.getEntity();
            if (entity != null) {
                InputStream inputStream = null;
                try {
                    inputStream = entity.getContent(); 
                    final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
                    return bitmap;
                } finally {
                    if (inputStream != null) {
                        inputStream.close();  
                    }
                    entity.consumeContent();
                }
            }
        } catch (Exception e) {
            // Could provide a more explicit error message for IOException or IllegalStateException
            getRequest.abort();
            Log.w("ImageDownloader", "Error while retrieving bitmap from " + url + e.toString());
        } finally {
            if (client != null) {
                //client.close();
            }
        }
        return null;
    }
}

Example of use:

downloader = new ImageDownloader();
ImageView image_profile =(ImageView) row.findViewById(R.id.image_profile);
downloader.download(url, image_profile);
jzafrilla
  • 1,416
  • 3
  • 18
  • 41