2

I'm creating a simple Android widget that fetches and displays some movie covers from a website. It's simple GridView that displays the images. The widget works fine but the moment I begin scrolling, I get and OutOfMemoryError and Android kills my process.

Here's the code of my RemoteViewsFactory. I can't seem to understand how to resolve this. The images I'm using are properly sized so I don't waste memory in Android. They are perfectly sized. As you can see I'm using a very small LRU cache and in my manifest, I've even enabled android:largeHeap="true". I've racked my head on this issue for a full two days and I'm wondering if what I'm trying to do is even possible?

public class SlideFactory implements RemoteViewsFactory {

    private JSONArray jsoMovies = new JSONArray();
    private Context ctxContext;
    private Integer intInstance;
    private RemoteViews remView;
    private LruCache<String, Bitmap> lruCache;
    private static String strCouch = "http://192.168.1.110:5984/movies/";

    public SlideFactory(Context ctxContext, Intent ittIntent) {
        this.ctxContext = ctxContext;
        this.intInstance = ittIntent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
        this.lruCache = new LruCache<String, Bitmap>(4 * 1024 * 1024);
        final Integer intMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        final Integer intBuffer = intMemory / 8;

        lruCache = new LruCache<String, Bitmap>(intBuffer) {
            @Override
            protected int sizeOf(String strDigest, Bitmap bmpCover) {
                return bmpCover.getByteCount() / 1024;
            }
        };
    }

    public RemoteViews getViewAt(int intPosition) {
        if (intPosition <= getCount()) {
            try {
                String strDocid = this.jsoMovies.getJSONObject(intPosition).getString("id");
                String strDigest = this.jsoMovies.getJSONObject(intPosition).getJSONObject("value").getJSONObject("_attachments").getJSONObject("thumb.jpg").getString("digest");
                String strTitle = this.jsoMovies.getJSONObject(intPosition).getJSONObject("value").getString("title");
                Bitmap bmpThumb = this.lruCache.get(strDigest);

                if (bmpThumb == null) {
                    String strUrl = strCouch + strDocid + "/thumb.jpg";
                    System.out.println("Fetching" + intPosition);
                    bmpThumb = new ImageFetcher().execute(strUrl).get();
                    this.lruCache.put(strDigest, bmpThumb);
                }
                remView.setImageViewBitmap(R.id.movie_cover, bmpThumb);
                remView.setTextViewText(R.id.movie_title, strTitle);
            } catch (Exception e) {
                e.printStackTrace();
            }
            return remView;
        }
        return null;
    }

    public void onCreate() {
        return;
    }

    public void onDestroy() {
        jsoMovies = null;
    }

    public int getCount() {
        return 20;
    }

    public RemoteViews getLoadingView() {
        return null;//new RemoteViews(this.ctxContext.getPackageName(), R.layout.loading);
    }

    public int getViewTypeCount() {
        return 1;
    }

    public long getItemId(int intPosition) {
        return intPosition;
    }

    public boolean hasStableIds() {
        return true;
    }

    public void onDataSetChanged() {
        this.remView = new RemoteViews(this.ctxContext.getPackageName(), R.layout.slide);
        try {
            DefaultHttpClient dhcNetwork = new DefaultHttpClient();
            String strUrl = strCouch + "_design/application/_view/language?" + URLEncoder.encode("descending=true&startkey=[\"hi\", {}]&attachments=true");
            HttpGet getMovies = new HttpGet(strUrl);
            HttpResponse resMovies = dhcNetwork.execute(getMovies);
            Integer intMovies = resMovies.getStatusLine().getStatusCode();
            if (intMovies != HttpStatus.SC_OK) {
                throw new HttpResponseException(intMovies, "Server responded with an error");
            }
            String strMovies = EntityUtils.toString(resMovies.getEntity(), "UTF-8");
            this.jsoMovies = new JSONObject(strMovies).getJSONArray("rows");
        } catch (Exception e) {
            Log.e("SlideFactory", "Unknown error encountered", e);
        } 
    }
}

Here's the source of the AsyncTask that fetches the images:

public class ImageFetcher extends AsyncTask<String, Void, Bitmap> {
    @Override
    protected Bitmap doInBackground(String... strUrl) {
        Bitmap bmpThumb = null;
        try {
            URL urlThumb = new URL(strUrl[0]);
            HttpURLConnection hucConnection = (HttpURLConnection) urlThumb.openConnection();
            InputStream istThumb = hucConnection.getInputStream();
            bmpThumb = BitmapFactory.decodeStream(istThumb);
            istThumb.close();
            hucConnection.disconnect();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return bmpThumb;
    }
}
Mridang Agarwalla
  • 38,521
  • 65
  • 199
  • 353
  • @commonsware, would you be able to shed any light on this? You've always had an answer. :) Thank you. – Mridang Agarwalla Nov 21 '13 at 08:43
  • Can you post some logcat output? – bgse Nov 21 '13 at 22:30
  • @MridangAgarwalla Can you post code for `ImageFetcher`? Can you also mention the size of the images that you are loading (or maybe size of the largest among them)..?? – Amulya Khare Nov 22 '13 at 03:55
  • @AmulyaKhare, I've posted the source of `ImageFetcher`. I could play with the sample-size but it drastically reduces the quality. My images are all 250x375 pixels. I put a `println` into the method `getViewAt` method and notice that Android tries to load all the images at once even though only a few images are visible in the `GridView`. I thought the way these adapters for the `ListView`, `GridView` and `StackVIew` work is that they only preload a few items and the moment the user scrolls and they out of focus, Android discards the old layouts. I don't see why it tries to load all the items. – Mridang Agarwalla Nov 22 '13 at 07:36
  • [Have you read it?](http://stackoverflow.com/questions/10743381/when-should-i-recycle-a-bitmap-using-lrucache) And this `this.lruCache = new LruCache(4 * 1024 * 1024);` line is useless. – Leonidos Nov 22 '13 at 08:25
  • @Leonidos, I've tried this by stripping all the `LRUCache` functionality from my code but it hasn't helped. – Mridang Agarwalla Nov 22 '13 at 15:27
  • If you found a solution of OOM when using `setImageViewBitmap`, please post it! There is no information about it at all! – anil Jun 23 '15 at 07:42

2 Answers2

2

I had similar bitter experience and after lots of digging I found that setImageViewBitmap copies all bitmap into new instance, so taking double memory. Consider changing following line into either static resource or something else !

remView.setImageViewBitmap(R.id.movie_cover, bmpThumb);

This takes lots of memory and pings garbage collector to clean memory, but so slow that your app can't use the freed memory in time.

Mridang Agarwalla
  • 38,521
  • 65
  • 199
  • 353
AVEbrahimi
  • 13,285
  • 12
  • 70
  • 151
  • Since I'm downloading the images from a remote sever, how would I create a static reference. By static-reference, do you mean assets in the drawable folder? It would also be great if you could link to the Android sources where you found the issue with the `setImageViewBitmap ` method. Thanks @AVEbrahimi – Mridang Agarwalla Nov 26 '13 at 08:36
  • Thank you! This really saved the day! – Mridang Agarwalla Nov 26 '13 at 20:39
  • @MridangAgarwalla Could you post your solution? – Eloar Jan 06 '15 at 00:23
  • I don't think the problem had been solved. If you found a solution, please post it! – anil Jun 23 '15 at 07:33
0

My workaround is using LRUCache to store widget's bitmaps:

First, init as usual:

protected void initializeCache()
    {
        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

        // Use 1/10th of the available memory for this memory cache.
        final int cacheSize = maxMemory / 10;

        bitmapLruCache = 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;
            }
        };
    }

Then reuse bitmaps:

void updateImageView(RemoteViews views, int resourceId, final String imageUrl)
    {
        Bitmap bitmap = bitmapLruCache.get(imageUrl);

        if (bitmap == null)
        {
            bitmap = // get bitmap from web with your loader
            bitmapLruCache.put(imageUrl, bitmap);
        }

        views.setImageViewBitmap(resourceId, bitmap);
    }

With this code widget does not crash my app now.

More info here

anil
  • 1,881
  • 17
  • 33