在Android中加载位图时出现内存不足问题

时间:2013-01-21 11:01:54

标签: android android-imageview android-image android-memory

我使用Fedor的代码(https://github.com/thest1/LazyList)来加载位图。根据我的要求,我修改了几行代码。当堆内存超过阈值时,我出现内存不足错误。在同一主题上发布了各种问题。大多数人建议使用SoftReference和bitmap recycle()。我正在使用SoftReference,但我仍面临问题。而且我也很困惑在哪里使用位图循环方法。

MemoryCache.java

public class MemoryCache {

private static final String TAG = "MemoryCache";
private static Map<String, SoftReference<Bitmap>> cache=Collections.synchronizedMap(
        new LinkedHashMap<String, SoftReference<Bitmap>>(16,0.75f,false));//Last argument true for LRU ordering
private long size=0;//current allocated size
private long limit=30000000;//max memory in bytes

public MemoryCache(){

    long cacheSize = Runtime.getRuntime().maxMemory();
    setLimit(cacheSize);
}

public void setLimit(long new_limit){
    limit=new_limit;

}

public Bitmap get(String id){
    try{
        if(!cache.containsKey(id))
            return null;
        //NullPointerException sometimes happen here http://code.google.com/p/osmdroid/issues/detail?id=78 
        return cache.get(id).get();
    }catch(NullPointerException ex){
        ex.printStackTrace();
        return null;
    }
}

public void put(String id, Bitmap bitmap){
    try{
        if(cache.containsKey(id))
            size-=getSizeInBytes(cache.get(id).get());
        cache.put(id, new SoftReference<Bitmap>(bitmap));
        size+=getSizeInBytes(bitmap);
        checkSize();
    }catch(Throwable th){
        th.printStackTrace();
    }
}

private void checkSize() {
    Log.i(TAG, "cache size="+size+" length="+cache.size());

    if(size>limit+5000000){
       cache.clear();
        }
        Log.i(TAG, "Clean cache. New size "+cache.size());

}

public void clear() {
    try{
        //NullPointerException sometimes happen here http://code.google.com/p/osmdroid/issues/detail?id=78 
        cache.clear();
        size=0;
    }catch(NullPointerException ex){
        ex.printStackTrace();
    }
}

long getSizeInBytes(Bitmap bitmap) {
    if(bitmap==null)
        return 0;
    return bitmap.getRowBytes() * bitmap.getHeight();
}
}

ImageLoader.java

public class ImageLoader {

MemoryCache memoryCache = new MemoryCache();
FileCache fileCache;
private Map<ImageView, String> imageViews = Collections
        .synchronizedMap(new WeakHashMap<ImageView, String>());
ExecutorService executorService;
Handler handler = new Handler();
Context con;
ProgressBar pb;

public ImageLoader(Context context) {
    fileCache = new FileCache(context);
    this.con = context;
    executorService = Executors.newFixedThreadPool(5);
}

final int stub_id = R.drawable.icon_loading;

public void DisplayImage(String url, ImageView imageView, ProgressBar pb) {
    this.pb = pb;

    imageViews.put(imageView, url);
    Bitmap bitmap = memoryCache.get(url);
    if (bitmap != null) {
        pb.setVisibility(View.GONE);
        imageView.setImageBitmap(bitmap);

    } else {
        queuePhoto(url, imageView);


    }
}

private void queuePhoto(String url, ImageView imageView) {
    PhotoToLoad p = new PhotoToLoad(url, imageView);
    executorService.submit(new PhotosLoader(p));
}

private Bitmap getBitmap(String url) {
    Bitmap result = null;

    File f = fileCache.getFile(url);

    // from SD cache
    Bitmap b = decodeFile(f);
    if (b != null)
        return b;

    // from web
    try {
        Bitmap bitmap = null;
        URL imageUrl = new URL(url);
        HttpURLConnection conn = (HttpURLConnection) imageUrl
                .openConnection();
        conn.setInstanceFollowRedirects(true);
        InputStream is = conn.getInputStream();
        OutputStream os = new FileOutputStream(f);
        Utils.CopyStream(is, os);
        os.close();
        is.close();
        conn.disconnect();
        bitmap = decodeFile(f);
        //Log.v("bitmap size", bitmap.getByteCount() + "");
        //bitmap.recycle();
        return bitmap;
    } catch (Throwable ex) {
        ex.printStackTrace();
        if (ex instanceof OutOfMemoryError)
            memoryCache.clear();
        return null;
    }


}



// decodes image and scales it to reduce memory consumption
private Bitmap decodeFile(File f) {
    try {


        BitmapFactory.Options o = new BitmapFactory.Options();
        o.inJustDecodeBounds = true;
        FileInputStream stream1 = new FileInputStream(f);
        BitmapFactory.decodeStream(stream1, null, o);
        stream1.close();


        final int REQUIRED_SIZE = 70;
        int width_tmp = o.outWidth, height_tmp = o.outHeight;
        int scale = 1;

        BitmapFactory.Options o2 = new BitmapFactory.Options();

        if (f.length() > 300000) {

            o2.inSampleSize = 4;
        } else if (f.length() > 200000) {

            o2.inSampleSize = 2;
        } else {

            o2.inSampleSize = 1;
        }
        FileInputStream stream2 = new FileInputStream(f);
        Bitmap bitmap = BitmapFactory.decodeStream(stream2, null, o2);
        stream2.close();
        return bitmap;
    } catch (FileNotFoundException e) {
    } catch (IOException e) {
        memoryCache.clear();
        e.printStackTrace();
    }
    return null;
}

// Task for the queue
private class PhotoToLoad {
    public String logo_url;
    public ImageView imageView;

    public PhotoToLoad(String u, ImageView i) {
        logo_url = u;
        imageView = i;
    }
}

class PhotosLoader implements Runnable {
    PhotoToLoad photoToLoad;

    PhotosLoader(PhotoToLoad photoToLoad) {
        this.photoToLoad = photoToLoad;
    }

    @Override
    public void run() {
        if (imageViewReused(photoToLoad))
            return;
        Bitmap bmp = getBitmap(photoToLoad.logo_url);
        // Log.v("bitmap size",bmp.getByteCount()+"");


        memoryCache.put(photoToLoad.logo_url, bmp);

        if (imageViewReused(photoToLoad))
            return;
        BitmapDisplayer bd = new BitmapDisplayer(bmp, photoToLoad);
        //bmp.recycle();
        // Activity a=(Activity)photoToLoad.imageView.getContext();
        // a.runOnUiThread(bd);
        handler.post(bd);
    }
}

boolean imageViewReused(PhotoToLoad photoToLoad) {
    String tag = imageViews.get(photoToLoad.imageView);
    if (tag == null || !tag.equals(photoToLoad.logo_url))
        return true;
    return false;
}

// Used to display bitmap in the UI thread
class BitmapDisplayer implements Runnable {
    Bitmap bitmap;
    PhotoToLoad photoToLoad;

    public BitmapDisplayer(Bitmap b, PhotoToLoad p) {
        bitmap = b;
        photoToLoad = p;
    }

    public void run() {
        if (imageViewReused(photoToLoad))
            return;
        if (bitmap != null) {
            //bitmap.recycle();
            // pb.setVisibility(View.GONE);
            photoToLoad.imageView.setImageBitmap(bitmap);
            //bitmap.recycle();
        } else {
            // photoToLoad.imageView.setImageResource(stub_id);
            // pb.setVisibility(View.VISIBLE);
        }
    }
}

public void clearCache() {
    memoryCache.clear();
    fileCache.clear();
}

}

附上Logcat输出:

01-21 16:54:47.348: D/skia(20335): --- decoder->decode returned false
01-21 16:54:47.408: I/dalvikvm-heap(20335): Clamp target GC heap from 69.438MB to 64.000MB
01-21 16:54:47.408: D/dalvikvm(20335): GC_FOR_ALLOC freed 67K, 5% free 62767K/65416K, paused 54ms, total 54ms
01-21 16:54:47.408: I/dalvikvm-heap(20335): Forcing collection of SoftReferences for 228816-byte allocation
01-21 16:54:47.468: I/dalvikvm-heap(20335): Clamp target GC heap from 69.438MB to 64.000MB
01-21 16:54:47.468: D/dalvikvm(20335): GC_BEFORE_OOM freed <1K, 5% free 62767K/65416K, paused 64ms, total 64ms
01-21 16:54:47.468: E/dalvikvm-heap(20335): Out of memory on a 228816-byte allocation.
01-21 16:54:47.468: I/dalvikvm(20335): "pool-21-thread-4" prio=5 tid=63 RUNNABLE
01-21 16:54:47.468: I/dalvikvm(20335):   | group="main" sCount=0 dsCount=0 obj=0x42e4e878 self=0x67693b00
01-21 16:54:47.468: I/dalvikvm(20335):   | sysTid=20520 nice=0 sched=0/0 cgrp=apps handle=1735190240
01-21 16:54:47.468: I/dalvikvm(20335):   | state=R schedstat=( 2851815000 268321000 1461 ) utm=276 stm=9 core=0
01-21 16:54:47.468: I/dalvikvm(20335):   at android.graphics.BitmapFactory.nativeDecodeStream(Native Method)
01-21 16:54:47.468: I/dalvikvm(20335):   at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:529)
01-21 16:54:47.468: I/dalvikvm(20335):   at com.main.util.ImageLoader.decodeFile(ImageLoader.java:211)
01-21 16:54:47.468: I/dalvikvm(20335):   at com.main.util.ImageLoader.getBitmap(ImageLoader.java:85)
01-21 16:54:47.468: I/dalvikvm(20335):   at com.main.util.ImageLoader.access$1(ImageLoader.java:79)
01-21 16:54:47.468: I/dalvikvm(20335):   at com.main.util.ImageLoader$PhotosLoader.run(ImageLoader.java:244)
01-21 16:54:47.468: I/dalvikvm(20335):   at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:390)
01-21 16:54:47.468: I/dalvikvm(20335):   at java.util.concurrent.FutureTask.run(FutureTask.java:234)
01-21 16:54:47.468: I/dalvikvm(20335):   at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1080)
01-21 16:54:47.468: I/dalvikvm(20335):   at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:573)
01-21 16:54:47.468: I/dalvikvm(20335):   at java.lang.Thread.run(Thread.java:856)

2 个答案:

答案 0 :(得分:6)

我的答案有两个(大)部分。第一部分更直接地针对你的问题,第二部分退后一步,因为我分享了我如何学会实现我自己的解决方案,更多的是针对第一次遇到这种情况的人。

不要使用bitmap.recycle(),因为你真的不应该这样做。虽然这会清除用于该位图的内存,但您可能会遇到仍然在某处使用的位图的问题。

您还应该在任何地方使用WeakReference,对象可能会挂起到位图(加载任务,ImageViews等)。来自documentation

弱引用对于映射有用,一旦它们不再被引用(从外部),它们的条目应自动删除。 SoftReference和WeakReference之间的区别在于决定清除和排列引用的时间点:
  • 应尽快清除并排入SoftReference,即万一VM存在内存不足的危险。
  • 一旦知道弱引用,就可以清除WeakReference并将其排队。

两者理论上都应该有效,但我们有一点问题:Java终结器。它们并不能保证及时运行,不幸的是,我们的小朋友Bitmap正在清理它的内存。如果有问题的位图创建得足够慢,那么GC可能有足够的时间来识别我们的SoftReferenceWeakReference对象并从内存中清除它,但实际情况并非如此。< / p>

缺点是,当使用使用像Bitmaps这样的终结器的对象时,它很容易超过垃圾收集器(我认为一些IO类也使用它们)。 WeakReference会比SoftReference更好地帮助我们解决时间问题。是的,如果我们能够将大量图像保存在内存中以获得疯狂的性能,那就太好了,但是很多Android设备根本没有记忆能够做到这一点,而且我发现无论如何如果您没有尽快清除引用,那么缓存有多大,您仍会遇到此问题。

就你的缓存而言,我做的第一个改变是放弃你自己的内存缓存类,只使用Android兼容性库中的LruCache。并不是说你的缓存有问题或任何问题,但它消除了另一个令人头疼的问题,它已经为你完成了,你不必维护它。

否则,我看到的最大问题是PhotoToLoad对ImageView 有强烈的引用,但是这整个类中的更多可能会使用一些调整。

一篇简短但写得很好的博客文章解释了在下载图像时保存正确的ImageView参考的好方法,可以在Android的博客Multithreading for Performance上找到。您还可以在Google的I / O应用程序中看到此类练习,其源代码可用。我在第二部分对此进行了一些扩展。

无论如何,不​​要试图将正在加载的网址映射到您正在进行的收藏的ImageView,而是按照上面的博客文章所做的那样优雅引用回到ImageView的方法,同时避免错误地使用回收的ImageView。当然,它是ImageViews如何被弱引用的一个很好的例子,这意味着我们的垃圾收集器可以更快地释放内存。

行。现在是第二部分。

在我继续讨论这个问题之前,让我更加啰嗦,我会说你正在走上正轨,而我的其余部分可能会在很多方面取得进展你已经了解并了解,但我希望它也会让更新的人受益,所以请耐心等待。

正如您已经知道的那样,这是Android上一个非常常见的问题,其中有一个相当长的解释,之前已经讨论过(在终结者身上握拳)。在将我自己的头撞在墙上数小时之后,尝试各种装载机和装载机的实施,观察堆积增长/清理比赛&#34;在日志中无休止地,分析内存使用情况和跟踪各种实现的对象,直到我的眼睛流血,我已经清楚了一些事情:

  1. 如果你发现自己试图告诉GC什么时候开火,那你就走错了路。
  2. 如果您尝试在用户界面中使用的位图(例如ImageViews)上调用bitmap.recycle(),您就会陷入痛苦的世界。
  3. 这是一个令人头疼的主要原因是因为关于如何解决这个问题,关于这个话题的错误信息太多了。许多教程或示例在理论上看起来都很好,但实际上是绝对的垃圾(我在上面提到的所有分析和跟踪都证实了这一点)。真是个迷宫!
  4. 您有两种选择。首先是使用一个着名且经过测试的库。第二是学习正确的方法来完成这项任务,并在此过程中获得一些有见地的知识。对于某些库,您可以同时执行这两个选项。

    如果你看this question,你会找到一些能够完成你想要做的事情的图书馆。还有一些很好的答案指出非常有用的学习资源。

    我自己走的路线比较困难,但我很痴迷于理解解决方案,而不仅仅是使用它们。如果你想走相同的路线(这是值得的),你应该首先关注谷歌的教程"Displaying Bitmaps Efficiently"

    如果没有,或者您想研究Google自己在实践中使用的解决方案,请查看其I / O 2012应用中的utility classes that handle bitmap loading and caching。特别是,研究以下课程:

    当然,研究一些活动以了解他们如何使用这些课程。在官方的Android教程和I / O 2012应用程序之间,我能够成功地完成自己的工作以适应我更具体的工作并知道实际发生了什么。您可以随时学习我在上面链接的问题中提到的一些库,以及稍微不同的看法。

答案 1 :(得分:0)

使用后回收位图。 你可以通过

来做到这一点
bitmap.recycle();