图片性能处理的一些总结
在做图片处理之前,我们需要知道图片的内存存储: 在Android 2.3.3(API Level 10)以及之前,Bitmap的backing pixel 数据存储在native memory, 与Bitmap本身是分开的,Bitmap本身存储在dalvik heap 中。导致其pixel数据不能判断是否还需要使用,不能及时释放,容易引起OOM错误。 从Android 3.0(API 11)开始,pixel数据与Bitmap一起存储在Dalvik heap中。所以在Android 2.3.3以及之前,建议使用Bitmap.recycle()方法,及时释放资源。
1. 图片大小的计算。
在Android 中图片有四种属性,分别是: ALPHA_8:每个像素占用1byte内存 ARGB_4444:每个像素占用2byte内存 ARGB_8888:每个像素占用4byte内存 (默认) RGB_565:每个像素占用2byte内存(没有alpha属性) 对于一张图片,假设其尺寸为2592x1936 pixels,默认情况下加载进来,Android系统需要为其分配2592_1936_4 bytes的空间,也就要19M。
2.在加载图片之前获取图片的Dimensions和Type.
我们可以采用BitmapFactory中的decodeByteArray(), decodeFile(),decodeResource() 去加载图片,如果直接加载,Android系统会为图片分配内存,容易出现OOM. 通过设置BitmapFactory.Options中的inJustDecodeBounds属性为true, 先获取Bitmap 的outWidth, outHeight, outMimeType(图片的类型,如image/jpeg, image/png等)选项。再根据获取到的选项,看是否在显示图片之前需要进行相应的处理(缩放)。
3.关于图片的缩放
图片的缩放是通过设置options.inSampleSize来实现。当inSampleSize = 2时,意味着图片的宽高都缩小为原来的1/2, 那么整体上图片的体积就变为原来的1/4. Google的官方文档中给出了具体的操作示例。
计算inSampleSize.
public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) > reqHeight
&& (halfWidth / inSampleSize) > reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
缩放图片
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
调用
mImageView.setImageBitmap(decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
这个方法是为一个100*100像素的ImageView提供合适的bitmap. 并不意味着生成的bitmap的尺寸会是100 * 100,这一点需要注意。 读者也可以用如下代码来进行试验:
InputStream is = getResources().openRawResource(R.drawable.image_1);
BitmapFactory.Options options = new BitmapFactory.Options();
Bitmap btp = BitmapFactory.decodeStream(is, null, options);
Log.i("TEST", "pre width:" + btp.getWidth() + " height:" + btp.getHeight());
Bitmap bitmap = decodeSampledBitmapFromResource(getResources(), res[position], 100, 100);
Log.i("TEST", "width:" + bitmap.getWidth() + " height:" + bitmap.getHeight());
4. 图片的缓存
图片的缓存Google推荐的是采用LruCache这个类,不要再采用SoftReference或WeakReference了,因为自从2.3之后垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。Google给出的例子是这样写的:
LruCache的初始化
private LruCache<String, Bitmap> mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// Get max available VM memory, exceeding this amount will throw an
// OutOfMemory exception. Stored in kilobytes as LruCache takes an
// int in its constructor.
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// Use 1/8th of the available memory for this memory cache.
final int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in kilobytes rather than
// number of items. 返回缓存图片的大小, 每向 lru cache中添加一个item,这个方法便会执行一次
return bitmap.getByteCount() / 1024;
}
};
...
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
加戴图片的方法
public void loadBitmap(int resId, ImageView imageView) {
final String imageKey = String.valueOf(resId);
final Bitmap bitmap = getBitmapFromMemCache(imageKey);
if (bitmap != null) {
mImageView.setImageBitmap(bitmap);
} else {
mImageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
task.execute(resId);
}
}
实际加载图片的Task
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...
// Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100));
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
return bitmap;
}
...
}
5. 采用MAT来定位内存泄露的图片
在上次做我们App性能优化问题的时候,发现在App中泄露的内存很大一部份是因为Memory Leak导致的Activity没有被回收,而在没有被回收的内存中,很大一部份是图片引起的。如果能定位到是哪一张图片的内存泄露,那么就可以有针对性的去检查代码了。在stackoverflow上有一篇文章记录着如利用MAT + GIMP 去定位内存泄露的图片。链接在这里. MAT的使用就不说了,下面把stackoverflow上找到的从MAT中导出图片数据,并用GIMP定位图片的Solution贴出来(此处不想翻译了):
- First, you need to download and install GIMP
- Next, find your Bitmap object in MAT, right-click on mBuffer field, in the popup menu choose “Copy” -> “Save Value To File” menu item and save value of this array to some file
- give extension .data to that file
- launch GIMP, choose “File” -> “Open”, select your .data file and click Open button
- “Load Image from Raw Data” dialog will appear. Here you need to set correct parameters for your bitmap
- first, choose “Image type” as “RGB Alpha” (most Android resources have this image type, but you may need to experiment with other image types)
- second, set correct Width and Height for your bitmap (correct dimensions can be found in the memory dump)
- At that point you should already observe preview of original image. If you didn’t, you can try to change some other parameters in “Load Image from Raw Data” dialog.
NOTE: to get a width and height of image you can look at mWidth and mHeight fields in MAT.