1.GridView显示图片
在聊天应用程序中经常会显示手机所有图片,供用户浏览选择发送,然而在显示时常常会导致OOM。虽然Android系统会给一个应用程序分配16M的内存,但是一张几KB大小的图片,加载到内存中时,可能会有几M的大小,这样加载几张图片就导致OOM。下面从图片的加载,引用和显示三方面来解决图片显示问题。
(1)加载
加载一张图片的时候,会以字节流的方式将图片读到内存中,这样就开始占用操作系统分给程序的内存。但是根据界面显示图片的大小,并不需要加载图片真正的大小,因此通常在加载的时候,可以对图片进行一定的比例压缩。
<span style="font-size:14px;">package com.example.album.utils;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.media.ExifInterface;
public class BitmapUtil {
@SuppressLint("NewApi")
public static final Bitmap compress(Context context, String uri,
int reqWidth, int reqHeight) {
Bitmap bitmap = null;
try {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
BitmapFactory.decodeStream(new BufferedInputStream(
new FileInputStream(uri)), null, opts);
int height = opts.outHeight;
int width = opts.outWidth;
int inSampleSize = 1;
int degree = readPictureDegree(uri);
if (degree == 0 || degree == 180) {
if (width > height && width > reqWidth) {
inSampleSize = Math.round((float) width / (float) reqWidth);
} else if (height > width && height > reqHeight) {
inSampleSize = Math.round((float) height
/ (float) reqHeight);
}
} else if (degree == 90 || degree == 270) {
// 图片有旋转时,宽和高调换了
if (width > height && width > reqHeight) {
inSampleSize = Math
.round((float) width / (float) reqHeight);
} else if (height > width && height > reqWidth) {
inSampleSize = Math
.round((float) height / (float) reqWidth);
}
}
if (inSampleSize <= 1)
inSampleSize = 1;
opts.inSampleSize = inSampleSize;
opts.inPreferredConfig = Config.RGB_565;
opts.inPurgeable = true;
opts.inInputShareable = true;
opts.inTargetDensity = context.getResources().getDisplayMetrics().densityDpi;
opts.inScaled = true;
opts.inTempStorage = new byte[16 * 1024];
opts.inJustDecodeBounds = false;
bitmap = BitmapFactory.decodeStream(new BufferedInputStream(
new FileInputStream(uri)), null, opts);
// 处理旋转了一定角度的图片,比如有些机型拍出的照片默认旋转了90度的
if (bitmap != null) {
// 处理旋转了一定角度的图片,比如有些机型拍出的照片默认旋转了90度的
Matrix matrix = new Matrix();
matrix.postRotate(degree);
bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
bitmap.getHeight(), matrix, false);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (OutOfMemoryError e) {
// 防止内存溢出导致程序崩溃而强制退出
e.printStackTrace();
}
return bitmap;
}
/**
* @param path
* 图片路径
* @return 图片旋转的度数
*/
private static int readPictureDegree(String path) {
int degree = 0;
try {
ExifInterface exifInterface = new ExifInterface(path);
int orientation = exifInterface.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
degree = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
degree = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
degree = 270;
break;
}
} catch (IOException e) {
e.printStackTrace();
}
return degree;
}
}</span>
inSampleSize=4表示width/height变为原来的1/4,当然只有inSampleSize>1才会节约内存,inSampleSize<=1时会被当成是1。inPreferredConfig表示加载Bitmap的色彩模式,色彩模式有四种:ARGB_8888(一个像素四个字节),ARGB_4444(一个像素两个字节),RGB_565(一个像素两个字节),ALPHA_8(一个像素一个字节),默认加载的色彩模式是ARGB_8888,这种色彩模式显示质量高,但是占用内存大,所以应该选择ARGB_4444,RGB_565,显示质量一般,内存占用不大的色彩模式。BitmapFactory有很多decode的方法,最好用BitmapFactory.decodeStream()或者BitmapFactory.decodeByteArray(),其它的方法会间接调用decodeStream()方法。最后,计算完缩小比例和设置各种参数之后,应将opts.inJustDecodeBounds = false,否则返回的Bitmap对象为null。
(2)引用
在创建一个Bitmap对象的时候,不仅Java代码会分配一段内存,而且C代码也会分配一段内存,因此在不用Bitmap对象的时候,一定要调用recycle()方法,释放C代码分配的内存。在显示过多的图片的时候,Android提供了一个LruCache类,该类是内部实现是LinkedHashMap类,可以用该类来管理和记录加载的Bitmap(可以参考操作系统的LRU分页算法)。
<span style="font-size:14px;">private LruCache<String, Bitmap> mLruCach;
mLruCache = new LruCache<String, Bitmap>((int) Runtime.getRuntime()
.maxMemory() / 8) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
};</span>
在测试的过程中,用WeakHashMap类来管理Bitmap,效果也差不多。
(3)显示
为了提高显示效率,在GridView滑动的时候,不要去加载图片,当停止滑动的时候再加载图片,因此可以实现OnScrollListener接口。
<span style="font-size:14px;"> @SuppressWarnings("unchecked")
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
switch (scrollState) {
//停止滑动,但刚进入页面的时候,不会调用该方法
case OnScrollListener.SCROLL_STATE_IDLE:
// 取消未加载完的任务
for (BitmapAsyncTask bat : bitmapAsyncTasks) {
if (bat != null
&& bat.getStatus() != AsyncTask.Status.FINISHED) {
bat.cancel(true);
bat = null;
}
}
//开始加载当前页面要显示的图片
for (int i = 0; i < mVisibleItemCount; i++) {
String uri = uris.get(mFirstVisibleItem + i);
ViewHolder viewHolder = (ViewHolder) view.getChildAt(i)
.getTag();
if (lruCache.get(uri) == null) {
BitmapAsyncTask bitmapAsyncTask = new BitmapAsyncTask(
context,
viewHolder.picture,
uri,
new int[] {
viewHolder.picture.getMeasuredWidth(),
viewHolder.picture.getMeasuredHeight() });
bitmapAsyncTasks.add(bitmapAsyncTask);
bitmapAsyncTask.execute(lruCache);
} else {
viewHolder.picture.setImageBitmap(lruCache.get(uri));
}
}
break;
//正在滑动
case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
if (!isScroll) {
isScroll = true;
}
break;
}
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
mFirstVisibleItem = firstVisibleItem;
mVisibleItemCount = visibleItemCount;
mTotalItemCount = totalItemCount;
}
如果内存实在很有限,那么可以在停止滑动之后,清空除当前页面图片占用的内存。
for (int j = 0; j < mFirstVisibleItem; j++) {
String uri = uris.get(j);
Bitmap bitmap = lruCache.get(uri);
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
bitmap = null;
lruCache.remove(uri);
}
}
int invisibleAfter = mFirstVisibleItem + mVisibleItemCount;
for (int k = invisibleAfter; k < mTotalItemCount; k++) {
String uri = uris.get(k);
Bitmap bitmap = lruCache.get(uri);
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
bitmap = null;
lruCache.remove(uri);
}
}</span>
当Bitmap回收之后,再次显示时会出现java.lang.IllegalArgumentException: Cannot draw recycled bitmaps异常,这是Bitmap的重用导致的,只需自定义ImageView,并在onDraw()方法中捕获异常就行了。
<span style="font-size:14px;">package com.example.album.view;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.widget.ImageView;
public class MyImageView extends ImageView {
public MyImageView(Context context) {
super(context);
}
public MyImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public MyImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onDraw(Canvas canvas) {
try {
super.onDraw(canvas);
} catch (Exception e) {
e.printStackTrace();
}
}
}
</span>
另外,通常情况下,希望按照相册那样显示图片,图片的宽和高都相等,因此在View的onMeasure()方法中设置高度和宽度相等。onMeasure()方法主要是用做计算View的大小。
<span style="font-size:14px;">package com.example.album.view;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.FrameLayout;
public class SqaureLayout extends FrameLayout {
public SqaureLayout(Context context) {
super(context);
}
public SqaureLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public SqaureLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@SuppressWarnings("unused")
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// For simple implementation, or internal size is always 0.
// We depend on the container to specify the layout size of
// our view. We can't really know what it is since we will be
// adding and removing different arbitrary views and do not
// want the layout to change as this happens.
setMeasuredDimension(getDefaultSize(0, widthMeasureSpec),
getDefaultSize(0, heightMeasureSpec));
// Children are just made to fill our space.
int childWidthSize = getMeasuredWidth();
int childHeightSize = getMeasuredHeight();
// set height and width
heightMeasureSpec = widthMeasureSpec = MeasureSpec.makeMeasureSpec(
childWidthSize, MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}</span>
由于GridView条目的宽高大小不知道,所以会引起多次重复调用position=0时的getView()方法,在实际开发中可以固定条目的宽高。
到此,使用GridView显示图片的问题都已经解决,在三星、索尼、联想、海信、酷派等手机上测试都不会出现内存溢出导致程序崩溃。唯一会出现问题的手机是小米,有些会出现崩溃,有些不会。现在还在测试解决。
源代码下载地址:Android中使用GridView和ViewPager显示图片的优化处理
本项目迁移到了我的GitHub上,可以到我的GitHub上去下载,下载地址:https://github.com/WJRye/Album-for-Android-Studio.git