Android 毕加索:内存不足

Android 毕加索:内存不足,android,out-of-memory,picasso,Android,Out Of Memory,Picasso,我有一个RecyclerView使用Picasso展示了几个图像。上下滚动一段时间后,应用程序内存不足,出现如下消息: E/dalvikvm-heap﹕ Out of memory on a 3053072-byte allocation. I/dalvikvm﹕ "Picasso-/wp-content/uploads/2013/12/DSC_0972Small.jpg" prio=5 tid=19 RUNNABLE I/dalvikvm﹕ | group="main" sCount=0 ds

我有一个
RecyclerView
使用
Picasso
展示了几个图像。上下滚动一段时间后,应用程序内存不足,出现如下消息:

E/dalvikvm-heap﹕ Out of memory on a 3053072-byte allocation.
I/dalvikvm﹕ "Picasso-/wp-content/uploads/2013/12/DSC_0972Small.jpg" prio=5 tid=19 RUNNABLE
I/dalvikvm﹕ | group="main" sCount=0 dsCount=0 obj=0x42822a50 self=0x59898998
I/dalvikvm﹕ | sysTid=25347 nice=10 sched=0/0 cgrp=apps/bg_non_interactive handle=1500612752
I/dalvikvm﹕ | state=R schedstat=( 10373925093 843291977 45448 ) utm=880 stm=157 core=3
I/dalvikvm﹕ at android.graphics.BitmapFactory.nativeDecodeStream(Native Method)
I/dalvikvm﹕ at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:623)
I/dalvikvm﹕ at com.squareup.picasso.BitmapHunter.decodeStream(BitmapHunter.java:142)
I/dalvikvm﹕ at com.squareup.picasso.BitmapHunter.hunt(BitmapHunter.java:217)
I/dalvikvm﹕ at com.squareup.picasso.BitmapHunter.run(BitmapHunter.java:159)
I/dalvikvm﹕ at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:390)
I/dalvikvm﹕ at java.util.concurrent.FutureTask.run(FutureTask.java:234)
I/dalvikvm﹕ at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1080)
I/dalvikvm﹕ at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:573)
I/dalvikvm﹕ at java.lang.Thread.run(Thread.java:841)
I/dalvikvm﹕ at com.squareup.picasso.Utils$PicassoThread.run(Utils.java:411)
I/dalvikvm﹕ [ 08-10 18:48:35.519 25218:25347 D/skia     ]
    --- decoder->decode returned false
调试时我注意到的事项:

  • 在手机或虚拟设备上安装应用程序时,图像通过网络加载,这就是它的本意。这可以通过图像左上角的红色三角形看到
  • 当滚动以便重新加载图像时,图像将从磁盘中取出。这可以通过图像左上角的蓝色三角形看到
  • 再滚动一些图像时,一些图像会从内存中加载,如左上角的绿色三角形所示
  • 再滚动一段时间后,出现内存不足异常,加载停止。当前未保存在内存中的图像上仅显示占位符图像,而内存中的图像则以绿色三角形正确显示
  • 这是一个示例图像。它相当大,但我使用
    fit()
    来减少应用程序中的内存占用

    所以我的问题是:

    private void setupPicasso()
    {
        Cache diskCache = new Cache(getDir("foo", Context.MODE_PRIVATE), 100000000);
        OkHttpClient okHttpClient = new OkHttpClient();
        okHttpClient.setCache(diskCache);
    
        Picasso picasso = new Picasso.Builder(this)
                .memoryCache(new LruCache(100000000)) // Maybe something fishy here?
                .downloader(new OkHttpDownloader(okHttpClient))
                .build();
    
        picasso.setIndicatorsEnabled(true); // For debugging
    
        Picasso.setSingletonInstance(picasso);
    }
    
    @Override
    public void onBindViewHolder(RecipeViewHolder recipeViewHolder, int position)
    {
        Picasso.with(mMiasMatActivity)
                .load(mRecipes.getImage(position))
                .placeholder(R.drawable.picasso_placeholder)
                .fit()
                .centerCrop()
                .into(recipeViewHolder.recipeImage); // recipeImage is an ImageView
    
        // More...
    }
    
    <ImageView
        android:id="@+id/mm_recipe_item_recipe_image"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:adjustViewBounds="true"
        android:paddingBottom="2dp"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:clickable="true"
    />
    
    • 当内存缓存停止时,是否应该从磁盘重新加载映像 满了吗
    • 这些图像是不是太大了?我能记住多少 假设解码时需要消耗0.5 MB的图像
    • 我下面的代码是否有任何错误/异常
    创建
    活动时设置静态毕加索实例

    private void setupPicasso()
    {
        Cache diskCache = new Cache(getDir("foo", Context.MODE_PRIVATE), 100000000);
        OkHttpClient okHttpClient = new OkHttpClient();
        okHttpClient.setCache(diskCache);
    
        Picasso picasso = new Picasso.Builder(this)
                .memoryCache(new LruCache(100000000)) // Maybe something fishy here?
                .downloader(new OkHttpDownloader(okHttpClient))
                .build();
    
        picasso.setIndicatorsEnabled(true); // For debugging
    
        Picasso.setSingletonInstance(picasso);
    }
    
    @Override
    public void onBindViewHolder(RecipeViewHolder recipeViewHolder, int position)
    {
        Picasso.with(mMiasMatActivity)
                .load(mRecipes.getImage(position))
                .placeholder(R.drawable.picasso_placeholder)
                .fit()
                .centerCrop()
                .into(recipeViewHolder.recipeImage); // recipeImage is an ImageView
    
        // More...
    }
    
    <ImageView
        android:id="@+id/mm_recipe_item_recipe_image"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:adjustViewBounds="true"
        android:paddingBottom="2dp"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:clickable="true"
    />
    
    在my
    RecyclerView.Adapter中使用静态毕加索实例

    private void setupPicasso()
    {
        Cache diskCache = new Cache(getDir("foo", Context.MODE_PRIVATE), 100000000);
        OkHttpClient okHttpClient = new OkHttpClient();
        okHttpClient.setCache(diskCache);
    
        Picasso picasso = new Picasso.Builder(this)
                .memoryCache(new LruCache(100000000)) // Maybe something fishy here?
                .downloader(new OkHttpDownloader(okHttpClient))
                .build();
    
        picasso.setIndicatorsEnabled(true); // For debugging
    
        Picasso.setSingletonInstance(picasso);
    }
    
    @Override
    public void onBindViewHolder(RecipeViewHolder recipeViewHolder, int position)
    {
        Picasso.with(mMiasMatActivity)
                .load(mRecipes.getImage(position))
                .placeholder(R.drawable.picasso_placeholder)
                .fit()
                .centerCrop()
                .into(recipeViewHolder.recipeImage); // recipeImage is an ImageView
    
        // More...
    }
    
    <ImageView
        android:id="@+id/mm_recipe_item_recipe_image"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:adjustViewBounds="true"
        android:paddingBottom="2dp"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:clickable="true"
    />
    
    XML文件中的
    ImageView

    private void setupPicasso()
    {
        Cache diskCache = new Cache(getDir("foo", Context.MODE_PRIVATE), 100000000);
        OkHttpClient okHttpClient = new OkHttpClient();
        okHttpClient.setCache(diskCache);
    
        Picasso picasso = new Picasso.Builder(this)
                .memoryCache(new LruCache(100000000)) // Maybe something fishy here?
                .downloader(new OkHttpDownloader(okHttpClient))
                .build();
    
        picasso.setIndicatorsEnabled(true); // For debugging
    
        Picasso.setSingletonInstance(picasso);
    }
    
    @Override
    public void onBindViewHolder(RecipeViewHolder recipeViewHolder, int position)
    {
        Picasso.with(mMiasMatActivity)
                .load(mRecipes.getImage(position))
                .placeholder(R.drawable.picasso_placeholder)
                .fit()
                .centerCrop()
                .into(recipeViewHolder.recipeImage); // recipeImage is an ImageView
    
        // More...
    }
    
    <ImageView
        android:id="@+id/mm_recipe_item_recipe_image"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:adjustViewBounds="true"
        android:paddingBottom="2dp"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:clickable="true"
    />
    
    
    
    更新 似乎连续滚动
    RecyclerView
    会使内存分配无限增加。我做了一个测试
    RecyclerView
    ,将200张
    cardwiew
    s的单个图像与
    ImageView
    匹配,但问题仍然存在。大多数图像都是从内存加载的(绿色),滚动很平稳,但大约每十分之一的
    ImageView
    会从磁盘加载图像(蓝色)。从磁盘加载映像时,将执行内存分配,从而增加堆上的分配,从而增加堆本身

    我尝试删除我自己的全局
    Picasso
    实例设置,并使用默认设置,但问题是相同的

    我用安卓设备监视器检查了一下,见下图。这是一个星系S3。从磁盘加载映像时执行的每个分配都可以在右侧的“每种大小的分配计数”下看到。每个图像分配的大小略有不同,这也很奇怪。按“原因GB”可使最右边的4.7 MB分配消失

    虚拟设备的行为与此相同。下图显示的是Nexus 5 AVD。同样在这里,当按下“原因GB”时,最大的分配(10.6MB)消失

    此外,这里还有来自Android设备监视器的内存分配位置和线程的图像。重复的分配在
    Picasso
    线程中完成,而使用
    Cause GB
    删除的分配在主线程中完成

    我想说这确实有点可疑-你给了
    LruCache
    100MB的空间。尽管所有设备都不同,但对于某些设备来说,这将达到或超过限制,请记住,这只是
    LruCache
    ,而不考虑应用程序其余部分所需的堆空间。我的猜测是,这是异常的直接原因-您正在告诉
    LruCache
    ,允许它变得比应该的大得多

    我会先将其降低到5MB左右,以证明这一理论,然后在目标设备上进行增量更高值的实验。您还可以查询设备的空间大小,如果愿意,还可以通过编程方式设置此值。最后,还有可以添加到清单中的
    android:largeHeap=“true”
    属性,但我认为这通常是不好的做法

    你的图片确实很大,所以我建议你也减少这些图片。请记住,即使您正在修剪它们,它们仍然暂时需要以其完整大小加载到内存中

    我不确定
    fit()
    是否适用于
    android:adjustViewBounds=“true”
    。据一些人说,这似乎是有问题的

    一些建议:


    • 为ImageView设置固定大小
    • 用户a在计算ImageView后获取其大小,然后调用毕加索添加
      resize()
      方法
    • 试一试-它的默认配置比毕加索的占地面积小(它存储调整大小的图像而不是原始图像,并使用RGB565)

    有了毕加索,你可以用它的特性来解决这个问题 比如:

      Picasso.with(context)
                    .load(url)
                    .resize(300,300)
                    .into(listHolder.imageview);
    

    您需要调整图像的大小。

    我刚刚创建了一个Singleton类来加载图像。问题是我使用了太多的毕加索对象,这些对象是由太多的Picasso.Builder创建的。以下是我的实现:

    public class ImagesLoader {
    
        private static ImagesLoader currentInstance = null;
        private static Picasso currentPicassoInstance = null;
    
        protected ImagesLoader(Context context) {
            initPicassoInstance(context);
        }
    
        private void initPicassoInstance(Context context) {
            Picasso.Builder builder = new Picasso.Builder(context);
            builder.listener(new Picasso.Listener() {
                @Override
                public void onImageLoadFailed(Picasso picasso, Uri uri, Exception exception) {
                    exception.printStackTrace();
                }
            });
            currentPicassoInstance = builder.build();
        }
    
        public static ImagesLoader getInstance(Context context) {
            if (currentInstance == null) {
                currentInstance = new ImagesLoader(context);
            }
            return currentInstance;
        }
    
        public void loadImage(ImageToLoad loadingInfo) {
            String imageUrl = loadingInfo.getUrl().trim();
            ImageView destination = loadingInfo.getDestination();
            if (imageUrl.isEmpty()) {
                destination.setImageResource(loadingInfo.getErrorPlaceholderResourceId());
            } else {
                currentPicassoInstance
                        .load(imageUrl)
                        .placeholder(loadingInfo.getPlaceholderResourceId())
                        .error(loadingInfo.getErrorPlaceholderResourceId())
                        .into(destination);
            }
        }
    }
    
    然后创建一个
    ImageToLoad
    类,该类保存ImageView、Url、占位符和错误占位符

    public class ImageToLoad {
    
        private String url;
        private ImageView destination;
        private int placeholderResourceId;
        private int errorPlaceholderResourceId;
    
        //Getters and Setters
    
    }
    

    是的,这是有道理的。我只是试图“最大化”它,这当然是非常糟糕的做法。使用10 MB缓存时,图像总是从磁盘加载,只有一个例外,这使应用程序看起来有点“关闭”,因为加载图像需要约0.5秒。我想这一切都归结为图像太大。你可能想看看这些线程,了解如何减少解码后位图使用的内存量:(1),(2)干杯,我会深入研究,看看是否能找到答案。我运行了一个内存分析会话,发现了一些奇怪的问题。当我上下滚动我的<代码>回收视图时<