接上篇,发现应用崩溃是因为OutOfMemory错误,下一步需要分析应用内存,找出哪里有内存泄露。
首先在eclipse中安装android内存分析工具MAT插件。
DDMS查看应用内存分配
打开DDMS,点击“Update Heap”
打开Heap视图
Heap Size:应用内存大小
Allocated:应用已分配内存
Free:应用空闲内存
一般占用内存最多的是字节数据“1-byte array”。点击“Cause GC”,触发内存回收。点击一次就好,之后会自动触发。
应用运行一段时间后,发现内存不断增长,快要达到应用内存上限了,点击“Dump HPROF file”生成HPROF文件,再使用MAT分析该文件。
MAT分析内存
MAT有4个主要视图
Overview:应用内存概览
- default_report:描述问题所在,列举占用内存较大的对象
- dominator_tree:列出内存中占用内存的对象
Shallow Heap:对象自身占用的内存
Retained Heap:对象引用的对象占用的内存
2个值相差越大,说明有更多的内存没有得到释放。 - Histogram:以数据类型分组显示占用的内存
byte[]还原为图片
经过分析,发现占用内存最大的是图片,而且有张图片居然占用了8M内存,这就需要把byte[]还原为图片,才能找出是哪张。
- 在dominator_tree中,右击对应的byte[],选择List objects-with incoming references
- 在打开的视图中,右击对应的byte[],选择Copy-Save Value To File,将数据保存为*.data格式。
- 选中byte[]对应的Bitmap对象,在Inspector视图中找出mHeight和mWidth值
- 使用图片编辑软件GIMP,打开之前保存的data格式文件,在弹出的对话框中,图像类型选择RGB Alpha,高度和宽度填入之前的mHeight和mWidth值
bitmap占用内存计算方式
找出原图后,发现图片占用内存大小和物理大小没有关系,和宽高有直接关系。
计算公式:宽*高*单位像素占用字节数
单位像素占用的字节数由其参数BitmapFactory.Options的inPreferredConfig变量决定,inPreferredConfig为Bitmap.Config类型,默认为Bitmap.Config.ARGB_8888,对应单位像素占用字节数为4。
那张8M的资源图片就是因为宽高太大,导致占用内存太多,对资源图片的宽高要严格限制。
bitmap缓存
项目中使用volley框架,volley已经实现了磁盘缓存,我们只需要实现volley的ImageLoader.ImageCache接口实现内存缓存。
但是,在内存分析中发现BitmapLruImageCache 对象并没有及时的回收内存。所以在代码中增加了达到上限时清除10%缓存的功能。
import java.util.Iterator;
import java.util.Map;
import android.graphics.Bitmap;
import android.os.SystemClock;
import android.support.v4.util.LruCache;
import android.util.Log;
import com.android.volley.toolbox.ImageLoader.ImageCache;
public class BitmapLruImageCache extends LruCache<String, Bitmap> implements
ImageCache {
private final String TAG = this.getClass().getSimpleName();
private static final float HYSTERESIS_FACTOR = 0.9f;
public BitmapLruImageCache(int maxSize) {
super(maxSize);
}
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight();
}
@Override
public Bitmap getBitmap(String url) {
Log.v(TAG, "Retrieved item from Mem Cache");
return get(url);
}
@Override
public void putBitmap(String url, Bitmap bitmap) {
Log.v(TAG, "Added item to Mem Cache");
pruneIfNeeded(sizeOf(url, bitmap));
put(url, bitmap);
Log.v(TAG,String.format("bitmap Cache size %f MB,maxSize %f MB",
this.size()/1024.0/1024.0, this.maxSize()/1024.0/1024.0));
}
private void pruneIfNeeded(int neededSpace) {
if ((this.size() + neededSpace) < this.maxSize()) {
return;
}
Log.v(TAG,"Pruning old cache entries.");
long before = this.size();
int prunedFiles = 0;
long startTime = SystemClock.elapsedRealtime();
Iterator<Map.Entry<String, Bitmap>> iterator = this.snapshot().entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Bitmap> entry = iterator.next();
remove(entry.getKey());
iterator.remove();
prunedFiles++;
if ((this.size() + neededSpace) < this.maxSize() * HYSTERESIS_FACTOR) {
break;
}
}
Log.v(TAG,String.format("pruned %d bitmaps, %d bytes, %d ms",
prunedFiles, (this.size() - before), SystemClock.elapsedRealtime() - startTime));
}
}
fresco
Fresco 是一个强大的图片加载组件。
Fresco 中设计有一个叫做 image pipeline 的模块。它负责从网络,从本地文件系统,本地资源加载图片。为了最大限度节省空间和CPU时间,它含有3级缓存设计(2级内存,1级文件)。
Fresco 中设计有一个叫做 Drawees 模块,方便地显示loading图,当图片不再显示在屏幕上时,及时地释放内存和空间占用。
Fresco 支持 Android2.3(API level 9) 及其以上系统。
Fresco 的 Image Pipeline 负责图片的获取和管理。图片可以来自远程服务器,本地文件,或者Content Provider,本地资源。压缩后的文件缓存在本地存储中,Bitmap数据缓存在内存中。
在5.0系统以下,Image Pipeline 使用`pinned purgeables*将Bitmap数据避开Java堆内存,存在ashmem中。这要求图片不使用时,要显式地释放内存。
解压后的图片,即Android中的Bitmap,占用大量的内存。大的内存占用势必引发更加频繁的GC。在5.0以下,GC将会显著地引发界面卡顿。
在5.0以下系统,Fresco将图片放到一个特别的内存区域。当然,在图片不显示的时候,占用的内存会自动被释放。这会使得APP更加流畅,减少因图片内存占用而引发的OOM。
Fresco 在低端机器上表现一样出色,你再也不用因图片内存占用而思前想后。