【问题标题】:image download: memory leak using LazyList图片下载:使用 LazyList 的内存泄漏
【发布时间】:2011-04-07 16:16:21
【问题描述】:

我在使用 LazyList 时遇到了内存泄漏。 我在整个应用程序中使用 ImageLoader 的一个实例,我在 Application.onCreate() 中创建它,因为我需要在几个活动中下载图像:列表活动、一个带有画廊小部件的活动和全屏画廊活动(它们都使用相同的缓存) 我修改了图像加载器,使其使用基于 SoftReference 的 HashMap。这是 SoftHashMap 的代码:

public class SoftHashMap extends AbstractMap {

    private final Map hash=new HashMap();
    private final int HARD_SIZE;
    private final LinkedList hardCache=new LinkedList();
    private final ReferenceQueue queue=new ReferenceQueue();
    
    public SoftHashMap(){
        this(100);
    }
    
    public SoftHashMap(int hardSize){
        HARD_SIZE=hardSize;
    }
    
    public Object get(Object key){
        Object result=null;
        SoftReference soft_ref=(SoftReference)hash.get(key);
        if(soft_ref!=null){
            result=soft_ref.get();
            if(result==null){
                hash.remove(key);
            }else{
                hardCache.addFirst(result);
                if(hardCache.size()>HARD_SIZE){
                    hardCache.removeLast();
                }
            }
        }
        return result;
    }
    private static class SoftValue extends SoftReference{
        private final Object key;
        public SoftValue(Object k, Object key, ReferenceQueue q) {
            super(k, q);
            this.key=key;
        }
    }
    
    private void processQueue(){
        SoftValue sv;
        while((sv=(SoftValue)queue.poll())!=null){
            hash.remove(sv.key);
       }
    }
    
    public Object put(Object key, Object value){
        processQueue();
        return hash.put(key, new SoftValue(value, key, queue));
    }
    
    public void clear(){
        hardCache.clear();
        processQueue();
        hash.clear();
    }
    
    public int size(){
        processQueue();
        return hash.size();
    }

    public Set entrySet() {
        throw new UnsupportedOperationException();
    }

}

ImageLoader 类:

public class ImageLoader {
    

     private SoftHashMap cache=new SoftHashMap(15);
     
     private File cacheDir;
     final int stub_id=R.drawable.stub;
     private int mWidth, mHeight;
     
     public ImageLoader(Context context, int h, int w){
         mWidth=w;
         mHeight=h;
         
         photoLoaderThread.setPriority(Thread.NORM_PRIORITY);
         if (android.os.Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED))
                cacheDir=new File(android.os.Environment.getExternalStorageDirectory(),"CacheDir");
            else
                cacheDir=context.getCacheDir();
            if(!cacheDir.exists())
                cacheDir.mkdirs();
     }
     public void DisplayImage(String url, Activity activity, ImageView imageView)
        {
           
           
           Log.d("IMAGE LOADER", "getNativeHeapSize()-"+String.valueOf(Debug.getNativeHeapSize()/1024)+" kb");
           Log.d("IMAGE LOADER", "getNativeHeapAllocatedSize()-"+String.valueOf(Debug.getNativeHeapAllocatedSize()/1024)+" kb");
           Log.d("IMAGE LOADER", "getNativeHeapFreeSize()-"+String.valueOf(Debug.getNativeHeapFreeSize()/1024)+" kb");
           if(cache.get(url)!=null){
               imageView.setImageBitmap((Bitmap)cache.get(url));
           }
            else
            {
                queuePhoto(url, activity, imageView);
                imageView.setImageResource(stub_id);
            }    
        }
            
        private void queuePhoto(String url, Activity activity, ImageView imageView)
        {
            //This ImageView may be used for other images before. So there may be some old tasks in the queue. We need to discard them. 
            photosQueue.Clean(imageView);
            PhotoToLoad p=new PhotoToLoad(url, imageView);
            synchronized(photosQueue.photosToLoad){
                photosQueue.photosToLoad.push(p);
                photosQueue.photosToLoad.notifyAll();
            }
            
            //start thread if it's not started yet
            if(photoLoaderThread.getState()==Thread.State.NEW)
                photoLoaderThread.start();
        }
     private Bitmap getBitmap(String url) 
        {
            //I identify images by hashcode. Not a perfect solution, good for the demo.
            String filename=String.valueOf(url.hashCode());
            File f=new File(cacheDir, filename);
            
            //from SD cache
            Bitmap b = decodeFile(f);
            if(b!=null)
                return b;
            
            //from web
            try {
                Bitmap bitmap=null;
                InputStream is=new URL(url).openStream();
                OutputStream os = new FileOutputStream(f);
                Utils.CopyStream(is, os);
                os.close();
                bitmap = decodeFile(f);
                return bitmap;
            } catch (Exception ex){
               ex.printStackTrace();
               return null;
            }
        }

        //decodes image and scales it to reduce memory consumption
        private Bitmap decodeFile(File f){
            Bitmap b=null;
            try {
                //decode image size
                
                BitmapFactory.Options o = new BitmapFactory.Options();
                o.inJustDecodeBounds = true;
                FileInputStream fis=new FileInputStream(f);
                BitmapFactory.decodeStream(fis,null,o);
                try {
                    fis.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                
                //Find the correct scale value. It should be the power of 2.
                //final int REQUIRED_SIZE=mWidth;
                int width_tmp=o.outWidth, height_tmp=o.outHeight;
                int scale=1;
                
                while(true){
                    if(width_tmp/2<=mWidth || height_tmp/2<=mHeight)
                        break;
                    width_tmp/=2;
                    height_tmp/=2;
                    scale*=2;
                }
                
                //decode with inSampleSize
                BitmapFactory.Options o2 = new BitmapFactory.Options();
                o2.inSampleSize=scale;
                //o2.inPurgeable=true;
                fis=new FileInputStream(f);
                b=BitmapFactory.decodeStream(fis, null, o2);
                try {
                    fis.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                return b;
            } catch (FileNotFoundException e) {}
            return null;
        }
     class PhotoToLoad{
         public String url;
         public ImageView imageView;
         
         public PhotoToLoad(String u, ImageView i){
             url=u;
             imageView=i;
         }
     }
     PhotosQueue photosQueue=new PhotosQueue();
        
        public void stopThread()
        {
            photoLoaderThread.interrupt();
        }
     class PhotosQueue{
         private Stack<PhotoToLoad> photosToLoad=new Stack<PhotoToLoad>(); 
         
         public void Clean(ImageView image)
            {
                for(int j=0 ;j<photosToLoad.size();){
                    if(photosToLoad.get(j).imageView==image)
                        photosToLoad.remove(j);
                    else
                        ++j;
                }
            }
     }
     class PhotosLoader extends Thread{
         public void run(){
             try {
                while(true)
                    {
                        //thread waits until there are any images to load in the queue
                        if(photosQueue.photosToLoad.size()==0)
                            synchronized(photosQueue.photosToLoad){
                                photosQueue.photosToLoad.wait();
                            }
                        if(photosQueue.photosToLoad.size()!=0)
                        {
                            PhotoToLoad photoToLoad;
                            synchronized(photosQueue.photosToLoad){
                                photoToLoad=photosQueue.photosToLoad.pop();
                            }
                            Bitmap bmp=getBitmap(photoToLoad.url);
                            cache.put(photoToLoad.url, bmp);
                            Object tag=photoToLoad.imageView.getTag();
                            if(tag!=null && ((String)tag).equals(photoToLoad.url)){
                                BitmapDisplayer bd=new BitmapDisplayer(bmp, photoToLoad.imageView);
                                Activity a=(Activity)photoToLoad.imageView.getContext();
                                a.runOnUiThread(bd);
                            }
                        }
                        if(Thread.interrupted())
                            break;
                    }
                } catch (InterruptedException e) {
                    //allow thread to exit
                }
         }
     }
     PhotosLoader photoLoaderThread=new PhotosLoader();
     
     class BitmapDisplayer implements Runnable
        {
            Bitmap bitmap;
            ImageView imageView;
            public BitmapDisplayer(Bitmap b, ImageView i){bitmap=b;imageView=i;}
            public void run()
            {
                if(bitmap!=null)
                    imageView.setImageBitmap(bitmap);
                else
                    imageView.setImageResource(stub_id);
            }
        }

        public void clearCache() {
            //clear memory cache
            cache.clear();
            
            //clear SD cache
            File[] files=cacheDir.listFiles();
            for(File f:files)
                f.delete();
        }
}

还有我的 Application 类,但不是最好的方法:

public class MyApplication extends Application {
    
    ImageLoader mImageLoader;

    
    @Override 
    public void onCreate(){
    
        int h =((WindowManager)getApplicationContext().getSystemService(WINDOW_SERVICE)).getDefaultDisplay().getHeight();
        
        int w =((WindowManager)getApplicationContext().getSystemService(WINDOW_SERVICE)).getDefaultDisplay().getWidth();
        mImageLoader=new ImageLoader(getApplicationContext(), h, w);
        super.onCreate();
  
    public ImageLoader getImageLoader(){
        return mImageLoader;
    }
    
    @Override
    public void onLowMemory(){
        mImageLoader.clearCache();
        Log.d("MY APP", "ON LOW MEMORY");
        super.onLowMemory();
    }
}

最糟糕的是:一段时间后,当 ImageLoader 尝试解码另一个位图时,我收到 OOM 异常。 我会很感激你的帮助。谢谢。

编辑我已经摆脱了硬缓存,但我仍然得到这个 OOM 异常。在我看来,我在做某事很有趣。我什至不知道我应该提供什么额外的信息...... 不过,我从服务器下载的图像非常大。并且app无法分配appr。 1.5 mb,这就是我在 LogCat 中看到的。但我就是想不通如果需要内存,为什么 vm 不清除我的 SoftHashMap...

【问题讨论】:

  • 你能发布一个项目来玩吗?如果我使用您的代码,也许我可以告诉您问题所在。
  • Fedor,很遗憾我无法发布项目。所有这些OOM问题都是由上下文泄漏引起的吗?您的加载程序使用 Activity 对象在 UI 线程上运行 BitmapDisplayer。但在我的应用程序中,我有几个活动(即用户选择列表项,我开始新的活动),我刚刚意识到可能有对死活动对象的引用。
  • 我认为问题在于图像的大小。如果我有一个 5mpx 图像,BitmapFactory 需要大量内存来解码它。并且如果有几个大图像,GC 没有时间清理原生堆。所以唯一的解决办法是避免使用大的位图。

标签: android


【解决方案1】:
  1. onLowMemory 对您没有帮助,因为它不会在您的应用程序内存不足时生成,当 Android 系统在终止进程之前需要内存用于不同的应用程序或自身时调用它。
  2. 我认为不需要硬缓存 - 这会阻止可回收对象被回收。只需将可绘制对象留在软缓存中 - GC 不会收集内存,而可绘制对象具有非软引用,因此您无需担心当前设置在 ImageView 中的可绘制对象被回收。

另外,您同时在屏幕上显示多少张图片?它们有多大?

【讨论】:

  • 感谢您的回复。可绘制的数量从全屏画廊中的 1 个到列表活动中的 6-7 个不等。我从服务器接收图像,但不知道它们的确切大小。所以我对它们进行解码以适应屏幕尺寸。但在列表视图中为画廊和缩略图提供此类图像似乎并不有效。我不知道使用硬缓存可以防止位图被垃圾收集。
  • 好吧,不多(我有一个应用程序,屏幕上有大约 40 个小图像,效果很好)。在这种情况下,您加载的图像文件有多大? (也摆脱了硬缓存!)
  • 我不知道从服务器加载的图像的确切大小,大约从 VGA 图像到 1600x1200 像素。我用 inSampleSize 对它们进行解码,以使它们小于屏幕尺寸。因此,提供给视图的图像大约为 70kb。
  • 另外,你能告诉更多关于硬缓存的信息吗?我想了解为什么它会阻止 GC 收集位图。
  • SoftReference 不会将某些东西标记为 GC 的“可收集”,它是一种保留对对象的引用而不阻止 GC 在需要时回收对象占用的内存的方法。通常,GC 只会收集不再引用的对象持有的内存。对于 SoftReference,GC 将只收集 only 引用是软(或弱等)的对象。因此,如果您对一个对象有两个引用,一个是软引用,一个不是,那么 GC 将无法收集内存,因为该对象仍然被一个或多个对象“硬”引用。
【解决方案2】:
  1. 这是一篇关于分析内存泄漏的精彩文章。它绝对可以帮助你。 http://android-developers.blogspot.com/2011/03/memory-analysis-for-android.html.

  2. 您确定您的 SoftHashMap 实现工作正常吗?看起来比较复杂。您可以使用调试器来确保 SoftHashMap 永远不会保存超过 15 个位图。 MAT 还可以帮助您确定内存中有多少位图。
    您还可以评论 cache.put(photoToLoad.url, bmp) 调用。这样,您将禁用内存缓存以识别是否是问题的原因。

  3. 是的,它可能是 Activity 泄漏。你可以识别出来。如果您只是在同一个活动中滚动并获得 OOM,则意味着其他东西正在泄漏而不是活动。如果您多次停止/启动活动并获得 OOM,则意味着活动正在泄漏。 如果您查看 MAT 直方图,您也可以肯定地说活动是否泄漏。

  4. 当您使用 inSampleSize 时,图像大小无关紧要。即使使用 5mpx 图像,它也应该可以正常工作。

  5. 您可以尝试仅将 SoftHashMap 实现替换为 HashMap>。阅读有关 SoftReference 的信息。它适用于非常简单的内存缓存实现。如果有足够的内存,它将对象保存在内存中。如果内存太少,SoftReference 会释放对象。

  6. 我还可以推荐您使用 LinkedHashMap 进行内存缓存。它有一个特殊的构造函数,可以按照最后一次访问其条目的顺序迭代项目。因此,当缓存中的项目超过 15 个时,您可以删除最近最少访问的项目。正如文档所说:

    这种映射非常适合构建 LRU 缓存。

  7. 您知道我的实现在设计时考虑了小图像,例如 50*50。如果您有更大的图像,您应该考虑它们消耗了多少内存。如果它们占用太多,您可以将它们缓存到 SD 卡而不是内存。性能可能会变慢,但OOM不再是问题。

  8. 与 OOM 无关。我可以看到你在 onLowMemory() 中调用 clearCache()。不好,因为 clearCache() 也会从 SD 中删除缓存。您应该只清除内存缓存而不是 SD 缓存。

【讨论】:

  • 7.仅 SD 缓存太慢,但我没有得到 OOM。 8. onLowMemory 永远不会被调用。
  • 我还查看了 MAT hist,它显示仅使用了 2.5-3mb 的磁头。所以我可以假设我有本机堆泄漏。感谢您的回复,Fedor。
  • 你是对的。位图数据存储在本机内存中。您应该使用 MAT 来确定位图泄漏的原因。您应该注意的不是消耗的内存,而是对象的数量。当您修复 Java 堆中的泄漏时,本机内存泄漏也将得到修复。祝你好运!
  • Read about WeakReference. It is good for very simple implementation of in-memory cache. It holds objects in memory if there's enough memory... 您在这里将 WeakReference 与 SoftReference 混淆了。 WR 在下一次 GC 时被清除(如果没有对对象的硬或软引用),而 SR 在内存需求时被清除。 JavaDoc:Soft reference objects, which are cleared at the discretion of the garbage collector in response to memory demand. Soft references are most often used to implement memory-sensitive caches. 所以应该是 Map>.
  • 绝对!对此感到抱歉。固定。
【解决方案3】:

我看到下面一行在你的代码中被注释了,首先取消注释那行 plz。

//o2.inPurgeable=true;

在 NonPurgeable 情况下,编码的比特流被一遍又一遍地解码为不同的位图,直到出现内存不足。在可清除的情况下,一个图像分配的内存将在需要时由任何新图像共享,如果旧图像引用被激活,操作系统将在任何时候通过使用另一个图像的空间来管理它自己的内存引用,并且反之亦然,这样它总是可以避免内存不足的错误。

如果即使你有可清除的情况并且仍然面临内存泄漏,那么现在使用下面的 try catch 博客来跟踪错误并让我知道堆栈跟踪详细信息。

    try{
    //your code to download or decode image
    }catch(Error e){
    //Print the stack trace
}

【讨论】:

    【解决方案4】:

    看起来您正在创建位图,但从未调用 Bitmap.recycle()。

    编辑:详细地说,Bitmap.recycle() 释放当前用于存储位图的内存。由于 BitmapFactory 创建的位图是不可变的,(您可以在新创建的位图上使用 Bitmap.isMutable() 进行检查),简单地从哈希表中删除对它们的引用并等待垃圾收集器并不足以释放记忆。

    每当您完成特定位图(例如在 photoQueue 的“clean”方法或 clearCache() 中)时,调用 Bitmap Recycle。

    【讨论】:

      猜你喜欢
      • 2022-07-04
      • 2012-08-17
      • 1970-01-01
      • 2018-09-19
      • 1970-01-01
      • 1970-01-01
      • 2022-01-10
      • 2020-02-15
      相关资源
      最近更新 更多