Android高效加載大圖片,防止OOM

原文出處:http://blog.csdn.net/guolin_blog/article/details/9316683

大家都知道,如果加載的圖片過大,就是出過OOM(內存溢出異常)

int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);  
Log.d("TAG", "Max memory is " + maxMemory + "KB");  

當我們加載大圖片的時候應該將圖片壓縮。

BitmapFactory 提供了多種創建Bitmap的方法,這些方法都會爲bitMap分配內存,這樣很容易出現OOM,不過BitMapFactory提供了一個可選的BtiMapFactory.Options參數,將這個參數的inJustDecodeBounds屬性設置爲true,就可以禁止爲BitMap非配內存,返回值不會是BitMap,而是null,雖然是空但是可以獲取bitmap的outWidth,outHeight,outMimeType.

設置BitMapFactory.Options中的inSampleSize的值可以對圖片進行壓縮。

下面的代碼通過傳入的的值,對大圖片的進行縮放

首先獲取縮放值:

public static int calculateInSampleSize(BitmapFactory.Options options,
		int reqWidth, int reqHeight) {
	// 源圖片的高度和寬度
	final int height = options.outHeight;
	final int width = options.outWidth;
	int inSampleSize = 1;
	if (height > reqHeight || width > reqWidth) {
		// 計算出實際寬高和目標寬高的比率
		final int heightRatio = Math.round((float) height / (float) reqHeight);
		final int widthRatio = Math.round((float) width / (float) reqWidth);
		// 選擇寬和高中最小的比率作爲inSampleSize的值,這樣可以保證最終圖片的寬和高
		// 一定都會大於等於目標的寬和高。
		inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
	}
	return inSampleSize;
}


然後創建Bitfactory.Options();將inJustDecodeBounds這是爲true,然後解析BitMap,設置縮放值後inJustDecodeBounds設置爲false,重新解析圖片。

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
        int reqWidth, int reqHeight) {
	// 第一次解析將inJustDecodeBounds設置爲true,來獲取圖片大小
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);
    // 調用上面定義的方法計算inSampleSize值
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
    // 使用獲取到的inSampleSize值再次解析圖片
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

使用緩存技術:

加載一張大圖片是一個簡單的事,當使用ListView加載一大堆圖片,就要使用到緩存的技術了,不然也會出現OOM。

爲了保證內存始終維持在一個合理的範圍,將移出屏幕的圖片進行回收,此時垃圾回收器認爲你不再持有這些圖片的引用,從而對這些圖片進行GC操作,但是當我們重新滑到上次的位置時,如果再去重新加載圖片無疑性能是非常差的。

這個時候我們內存緩存,它可以讓組件快速的重新加載和處理圖片,內存緩存技術可以對那些大量佔用內存的圖片提供快速訪問的方法。核心類就是LruCache。這個類非常適合用來緩存圖片,它的主要算法是把最近使用的對象的強引用添加到LinkedHashMap中,並且把最近最少使用的對象從內存中刪除。

在2.3之前,我們會用軟引用或者弱引用來實現內存緩存,2.3之後垃圾回收期更傾向於回收軟引用或弱引用的對象。在3.0之後,圖片的數據會存儲在本地的內存中,因而無法用一種可預見的方式將其釋放。

爲了能夠選擇一個適合的緩存大小給lruCache,有多個元素需要考慮。

爲了能夠選擇一個合適的緩存大小給LruCache, 有以下多個因素應該放入考慮範圍內,例如:

  • 你的設備可以爲每個應用程序分配多大的內存?
  • 設備屏幕上一次最多能顯示多少張圖片?有多少圖片需要進行預加載,因爲有可能很快也會顯示在屏幕上?
  • 你的設備的屏幕大小和分辨率分別是多少?一個超高分辨率的設備(例如 Galaxy Nexus) 比起一個較低分辨率的設備(例如 Nexus S),在持有相同數量圖片的時候,需要更大的緩存空間。
  • 圖片的尺寸和大小,還有每張圖片會佔據多少內存空間。
  • 圖片被訪問的頻率有多高?會不會有一些圖片的訪問頻率比其它圖片要高?如果有的話,你也許應該讓一些圖片常駐在內存當中,或者使用多個LruCache 對象來區分不同組的圖片。
  • 你能維持好數量和質量之間的平衡嗎?有些時候,存儲多個低像素的圖片,而在後臺去開線程加載高像素的圖片會更加的有效。
  • public class BitMapUtils {
        private LruCache<String, Bitmap> lruCache;
        private Context mContext;
    
        public BitMapUtils(Context context) {
            this.mContext = context;
            int maxMemory = (int) Runtime.getRuntime().maxMemory();
            int mCacheSize = maxMemory / 1024;
            lruCache = new LruCache<String, Bitmap>(mCacheSize) {
                @Override
                protected int sizeOf(String key, Bitmap value) {
                    //重寫次方法來衡量每張圖的大小,默認返回圖片的數量
                    return value.getByteCount() / 1024;
                }
            };
    
        }
    
        public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
            if (lruCache.get(key) == null) {
                lruCache.put(key, bitmap);
            }
        }
    
        public Bitmap getBitmapFromMemCache(String key) {
            return lruCache.get(key);
        }
    
        public void loadBitMap(int resId, ImageView imageView) {
            String imageKey = String.valueOf(resId);
            Bitmap bitmap = getBitmapFromMemCache(imageKey);
            if (bitmap != null) {
               imageView.setImageBitmap(bitmap);
            }else{
                imageView.setImageResource(R.drawable.emotionstore_progresscancelbtn);
                BitMapWorkerTask bitMapWorkerTask = new BitMapWorkerTask();
                bitMapWorkerTask.execute(resId);
            }
        }
    
        /**
         * 計算縮放值
         *
         * @param options
         * @param reqWidth
         * @param reqHeight
         * @return
         */
        private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
            int height = options.outHeight;
            int width = options.outWidth;
            int inSampleSize = 1;
            if (height > reqHeight || width > reqWidth) {
                int widthRatio = Math.round(width / reqWidth);
                int heightRatio = Math.round(height / reqHeight);
    
                inSampleSize = Math.min(widthRatio, heightRatio);
    
            }
            return inSampleSize;
        }
    
        /**
         * 加載大圖片
         * @param res
         * @param resId
         * @param reqWidth
         * @param reqHeight
         * @return
         */
        public static Bitmap decodeSampleBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeResource(res, resId, options);
            int inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
            options.inSampleSize = inSampleSize;
            options.inJustDecodeBounds = false;
            return BitmapFactory.decodeResource(res, resId, options);
        }
    
        class BitMapWorkerTask extends AsyncTask<Integer,Void,Bitmap>{
    
            @Override
            protected Bitmap doInBackground(Integer... integers) {
                Bitmap bitmap = decodeSampleBitmapFromResource(mContext.getResources(), integers[0], 100, 100);
                addBitmapToMemoryCache(String.valueOf(integers[0]),bitmap);
                return bitmap;
            }
        }
    

    如果是ListView或者GridView列表加載圖片時
  • 創建一個Set集合
  • 設置onScoll監聽
  • 給ImageView設置 tag
public class PhotoWallAdapter extends ArrayAdapter<String> implements OnScrollListener {

	/**
	 * 記錄所有正在下載或等待下載的任務。
	 */
	private Set<BitmapWorkerTask> taskCollection;

	/**
	 * 圖片緩存技術的核心類,用於緩存所有下載好的圖片,在程序內存達到設定值時會將最少最近使用的圖片移除掉。
	 */
	private LruCache<String, Bitmap> mMemoryCache;

	/**
	 * GridView的實例
	 */
	private GridView mPhotoWall;

	/**
	 * 第一張可見圖片的下標
	 */
	private int mFirstVisibleItem;

	/**
	 * 一屏有多少張圖片可見
	 */
	private int mVisibleItemCount;

	/**
	 * 記錄是否剛打開程序,用於解決進入程序不滾動屏幕,不會下載圖片的問題。
	 */
	private boolean isFirstEnter = true;

	public PhotoWallAdapter(Context context, int textViewResourceId, String[] objects,
			GridView photoWall) {
		super(context, textViewResourceId, objects);
		mPhotoWall = photoWall;
		taskCollection = new HashSet<BitmapWorkerTask>();
		// 獲取應用程序最大可用內存
		int maxMemory = (int) Runtime.getRuntime().maxMemory();
		int cacheSize = maxMemory / 8;
		// 設置圖片緩存大小爲程序最大可用內存的1/8
		mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
			@Override
			protected int sizeOf(String key, Bitmap bitmap) {
				return bitmap.getByteCount();
			}
		};
		mPhotoWall.setOnScrollListener(this);
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		final String url = getItem(position);
		View view;
		if (convertView == null) {
			view = LayoutInflater.from(getContext()).inflate(R.layout.photo_layout, null);
		} else {
			view = convertView;
		}
		final ImageView photo = (ImageView) view.findViewById(R.id.photo);
		// 給ImageView設置一個Tag,保證異步加載圖片時不會亂序
		photo.setTag(url);
		setImageView(url, photo);
		return view;
	}

	/**
	 * 給ImageView設置圖片。首先從LruCache中取出圖片的緩存,設置到ImageView上。如果LruCache中沒有該圖片的緩存,
	 * 就給ImageView設置一張默認圖片。
	 * 
	 * @param imageUrl
	 *            圖片的URL地址,用於作爲LruCache的鍵。
	 * @param imageView
	 *            用於顯示圖片的控件。
	 */
	private void setImageView(String imageUrl, ImageView imageView) {
		Bitmap bitmap = getBitmapFromMemoryCache(imageUrl);
		if (bitmap != null) {
			imageView.setImageBitmap(bitmap);
		} else {
			imageView.setImageResource(R.drawable.empty_photo);
		}
	}

	/**
	 * 將一張圖片存儲到LruCache中。
	 * 
	 * @param key
	 *            LruCache的鍵,這裏傳入圖片的URL地址。
	 * @param bitmap
	 *            LruCache的鍵,這裏傳入從網絡上下載的Bitmap對象。
	 */
	public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
		if (getBitmapFromMemoryCache(key) == null) {
			mMemoryCache.put(key, bitmap);
		}
	}

	/**
	 * 從LruCache中獲取一張圖片,如果不存在就返回null。
	 * 
	 * @param key
	 *            LruCache的鍵,這裏傳入圖片的URL地址。
	 * @return 對應傳入鍵的Bitmap對象,或者null。
	 */
	public Bitmap getBitmapFromMemoryCache(String key) {
		return mMemoryCache.get(key);
	}

	@Override
	public void onScrollStateChanged(AbsListView view, int scrollState) {
		// 僅當GridView靜止時纔去下載圖片,GridView滑動時取消所有正在下載的任務
		if (scrollState == SCROLL_STATE_IDLE) {
			loadBitmaps(mFirstVisibleItem, mVisibleItemCount);
		} else {
			cancelAllTasks();
		}
	}

	@Override
	public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
			int totalItemCount) {
		mFirstVisibleItem = firstVisibleItem;
		mVisibleItemCount = visibleItemCount;
		// 下載的任務應該由onScrollStateChanged裏調用,但首次進入程序時onScrollStateChanged並不會調用,
		// 因此在這裏爲首次進入程序開啓下載任務。
		if (isFirstEnter && visibleItemCount > 0) {
			loadBitmaps(firstVisibleItem, visibleItemCount);
			isFirstEnter = false;
		}
	}

	/**
	 * 加載Bitmap對象。此方法會在LruCache中檢查所有屏幕中可見的ImageView的Bitmap對象,
	 * 如果發現任何一個ImageView的Bitmap對象不在緩存中,就會開啓異步線程去下載圖片。
	 * 
	 * @param firstVisibleItem
	 *            第一個可見的ImageView的下標
	 * @param visibleItemCount
	 *            屏幕中總共可見的元素數
	 */
	private void loadBitmaps(int firstVisibleItem, int visibleItemCount) {
		try {
			for (int i = firstVisibleItem; i < firstVisibleItem + visibleItemCount; i++) {
				String imageUrl = Images.imageThumbUrls[i];
				Bitmap bitmap = getBitmapFromMemoryCache(imageUrl);
				if (bitmap == null) {
					BitmapWorkerTask task = new BitmapWorkerTask();
					taskCollection.add(task);
					task.execute(imageUrl);
				} else {
					ImageView imageView = (ImageView) mPhotoWall.findViewWithTag(imageUrl);
					if (imageView != null && bitmap != null) {
						imageView.setImageBitmap(bitmap);
					}
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	/**
	 * 取消所有正在下載或等待下載的任務。
	 */
	public void cancelAllTasks() {
		if (taskCollection != null) {
			for (BitmapWorkerTask task : taskCollection) {
				task.cancel(false);
			}
		}
	}

	/**
	 * 異步下載圖片的任務。
	 * 
	 * @author guolin
	 */
	class BitmapWorkerTask extends AsyncTask<String, Void, Bitmap> {

		/**
		 * 圖片的URL地址
		 */
		private String imageUrl;

		@Override
		protected Bitmap doInBackground(String... params) {
			imageUrl = params[0];
			// 在後臺開始下載圖片
			Bitmap bitmap = downloadBitmap(params[0]);
			if (bitmap != null) {
				// 圖片下載完成後緩存到LrcCache中
				addBitmapToMemoryCache(params[0], bitmap);
			}
			return bitmap;
		}

		@Override
		protected void onPostExecute(Bitmap bitmap) {
			super.onPostExecute(bitmap);
			// 根據Tag找到相應的ImageView控件,將下載好的圖片顯示出來。
			ImageView imageView = (ImageView) mPhotoWall.findViewWithTag(imageUrl);
			if (imageView != null && bitmap != null) {
				imageView.setImageBitmap(bitmap);
			}
			taskCollection.remove(this);
		}

		/**
		 * 建立HTTP請求,並獲取Bitmap對象。
		 * 
		 * @param imageUrl
		 *            圖片的URL地址
		 * @return 解析後的Bitmap對象
		 */
		private Bitmap downloadBitmap(String imageUrl) {
			Bitmap bitmap = null;
			HttpURLConnection con = null;
			try {
				URL url = new URL(imageUrl);
				con = (HttpURLConnection) url.openConnection();
				con.setConnectTimeout(5 * 1000);
				con.setReadTimeout(10 * 1000);
				bitmap = BitmapFactory.decodeStream(con.getInputStream());
			} catch (Exception e) {
				e.printStackTrace();
			} finally {
				if (con != null) {
					con.disconnect();
				}
			}
			return bitmap;
		}

	}

}




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章