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