毕加索:失忆
Picasso: out of memory
我有一个 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
调试时注意的事项:
- 在 phone 或虚拟设备上安装应用程序时,图像是通过网络加载的,这就是它的本意。这可以从图像左上角的红色三角形看到。
- 滚动以重新加载图像时,它们是从磁盘中获取的。这是从图像左上角的蓝色三角形看到的。
- 继续滚动时,一些图像会从内存中加载,如左上角的绿色三角形所示。
- 再滚动一些,出现内存不足异常,加载停止。当前未保存在内存中的图像仅显示占位符图像,而内存中的图像以绿色三角形正确显示。
Here 是示例图像。它相当大,但我使用 fit()
来减少应用程序中的内存占用。
所以我的问题是:
- 当内存缓存时,不应该从磁盘重新加载图像吗?
满了吗?
- 图片是不是太大了?我能用多少内存
期望在解码时使用 0.5 MB 图像?
- 我下面的代码中有什么wrong/unusual吗?
创建时设置静态 Picasso 实例 Activity
:
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);
}
在我的RecyclerView.Adapter
中使用静态 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...
}
XML文件中的ImageView
:
<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
精简以匹配 official documentation,使用一个图像 200 CardView
s 和 ImageView
,但问题仍然存在。大多数图像是从内存中加载的(绿色)并且滚动很流畅,但大约每十分之一 ImageView
从磁盘加载图像(蓝色)。从磁盘加载图像时,会执行内存分配,从而增加堆上的分配,从而增加堆本身。
我尝试删除我自己的全局 Picasso
实例设置并改用默认设置,但问题是一样的。
我用 Android 设备监视器进行了检查,见下图。这适用于 Galaxy S3。从磁盘加载图像时完成的每个分配都可以在 "Allocation count per size" 的右侧看到。每次分配图像的大小都略有不同,这也很奇怪。按 "Cause GB" 使最右边的 4.7 MB 分配消失。
虚拟设备的行为相同。下图显示了 Nexus 5 AVD。同样在这里,最大的分配(10.6 MB)在按下 "Cause GB" 时消失了。
此外,这里是 Android 设备监视器中内存分配位置和线程的图像。重复分配在 Picasso
线程中完成,而使用 Cause GB
删除的分配在主线程中完成。
.memoryCache(new LruCache(100000000)) // Maybe something fishy here?
我会说这确实有问题 - 你给 LruCache
100MB 的 space。尽管所有设备都不同,但对于某些设备来说,这将达到或超过限制,请记住,这只是 LruCache
,不考虑堆 space 其余部分应用程序需要。我的猜测是这是异常的直接原因 - 你告诉 LruCache
它被允许变得比它应该的大得多。
我会把它减少到 5MB 之类的东西来首先证明这个理论,然后在你的目标设备上试验逐渐增加的值。您还可以查询设备有多少 space 并根据需要以编程方式设置此值。最后,您可以将 android:largeHeap="true"
属性添加到您的清单中,但我发现这通常是不好的做法。
您的图片确实很大,所以我建议也减少这些图片。请记住,即使您正在修剪它们,它们仍然需要暂时以完整大小加载到内存中。
我不确定 fit()
是否适用于 android:adjustViewBounds="true"
。根据一些 past issues 这似乎是有问题的。
几点建议:
- 为 ImageView 设置固定大小
- 使用 GlobalLayoutListener 来获取 ImageView 的大小,一旦它被计算出来并且在这个调用 Picasso 之后添加
resize()
方法
- 试一试 Glide - 它的默认配置比 Picasso 占用空间更小(它存储调整后的图像而不是原始图像并使用 RGB565)
有了 picasso 你可以用它 属性 解决问题
喜欢:
Picasso.with(context)
.load(url)
.resize(300,300)
.into(listHolder.imageview);
您需要调整图片大小。
我刚刚为 LoadImages 创建了一个单例 class。问题是我使用了太多由太多 Picasso.Builder 创建的 Picasso 对象。这是我的实现:
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
class 来保存 ImageView、Url、占位符和错误占位符。
public class ImageToLoad {
private String url;
private ImageView destination;
private int placeholderResourceId;
private int errorPlaceholderResourceId;
//Getters and Setters
}
我有一个 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
调试时注意的事项:
- 在 phone 或虚拟设备上安装应用程序时,图像是通过网络加载的,这就是它的本意。这可以从图像左上角的红色三角形看到。
- 滚动以重新加载图像时,它们是从磁盘中获取的。这是从图像左上角的蓝色三角形看到的。
- 继续滚动时,一些图像会从内存中加载,如左上角的绿色三角形所示。
- 再滚动一些,出现内存不足异常,加载停止。当前未保存在内存中的图像仅显示占位符图像,而内存中的图像以绿色三角形正确显示。
Here 是示例图像。它相当大,但我使用 fit()
来减少应用程序中的内存占用。
所以我的问题是:
- 当内存缓存时,不应该从磁盘重新加载图像吗? 满了吗?
- 图片是不是太大了?我能用多少内存 期望在解码时使用 0.5 MB 图像?
- 我下面的代码中有什么wrong/unusual吗?
创建时设置静态 Picasso 实例 Activity
:
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);
}
在我的RecyclerView.Adapter
中使用静态 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...
}
XML文件中的ImageView
:
<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
精简以匹配 official documentation,使用一个图像 200 CardView
s 和 ImageView
,但问题仍然存在。大多数图像是从内存中加载的(绿色)并且滚动很流畅,但大约每十分之一 ImageView
从磁盘加载图像(蓝色)。从磁盘加载图像时,会执行内存分配,从而增加堆上的分配,从而增加堆本身。
我尝试删除我自己的全局 Picasso
实例设置并改用默认设置,但问题是一样的。
我用 Android 设备监视器进行了检查,见下图。这适用于 Galaxy S3。从磁盘加载图像时完成的每个分配都可以在 "Allocation count per size" 的右侧看到。每次分配图像的大小都略有不同,这也很奇怪。按 "Cause GB" 使最右边的 4.7 MB 分配消失。
虚拟设备的行为相同。下图显示了 Nexus 5 AVD。同样在这里,最大的分配(10.6 MB)在按下 "Cause GB" 时消失了。
此外,这里是 Android 设备监视器中内存分配位置和线程的图像。重复分配在 Picasso
线程中完成,而使用 Cause GB
删除的分配在主线程中完成。
.memoryCache(new LruCache(100000000)) // Maybe something fishy here?
我会说这确实有问题 - 你给 LruCache
100MB 的 space。尽管所有设备都不同,但对于某些设备来说,这将达到或超过限制,请记住,这只是 LruCache
,不考虑堆 space 其余部分应用程序需要。我的猜测是这是异常的直接原因 - 你告诉 LruCache
它被允许变得比它应该的大得多。
我会把它减少到 5MB 之类的东西来首先证明这个理论,然后在你的目标设备上试验逐渐增加的值。您还可以查询设备有多少 space 并根据需要以编程方式设置此值。最后,您可以将 android:largeHeap="true"
属性添加到您的清单中,但我发现这通常是不好的做法。
您的图片确实很大,所以我建议也减少这些图片。请记住,即使您正在修剪它们,它们仍然需要暂时以完整大小加载到内存中。
我不确定 fit()
是否适用于 android:adjustViewBounds="true"
。根据一些 past issues 这似乎是有问题的。
几点建议:
- 为 ImageView 设置固定大小
- 使用 GlobalLayoutListener 来获取 ImageView 的大小,一旦它被计算出来并且在这个调用 Picasso 之后添加
resize()
方法 - 试一试 Glide - 它的默认配置比 Picasso 占用空间更小(它存储调整后的图像而不是原始图像并使用 RGB565)
有了 picasso 你可以用它 属性 解决问题 喜欢:
Picasso.with(context)
.load(url)
.resize(300,300)
.into(listHolder.imageview);
您需要调整图片大小。
我刚刚为 LoadImages 创建了一个单例 class。问题是我使用了太多由太多 Picasso.Builder 创建的 Picasso 对象。这是我的实现:
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
class 来保存 ImageView、Url、占位符和错误占位符。
public class ImageToLoad {
private String url;
private ImageView destination;
private int placeholderResourceId;
private int errorPlaceholderResourceId;
//Getters and Setters
}