圖片三級快取及OOM--android

desaco發表於2016-03-03

因為從 Android 2.3 (API Level 9)開始,垃圾回收器會更傾向於回收持有軟引用或弱引用的物件,這讓軟引用和弱引用變得不再可靠。另外,Android 3.0 (API Level 11)中,圖片的資料會儲存在本地的記憶體當中,因而無法用一種可預見的方式將其釋放,這就有潛在的風險造成應用程式的記憶體溢位並崩潰。


記憶體快取技術對那些大量佔用應用程式寶貴記憶體的圖片提供了快速訪問的方法。其中最核心的類是LruCache (此類在android-support-v4的包中提供) 。
這個類非常適合用來快取圖片,它的主要演算法原理是把最近使用的物件用強引用儲存在 LinkedHashMap 中,並且把最近最少使用的物件在快取值達到預設定值之前從記憶體中移
除。

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

  • 你的裝置可以為每個應用程式分配多大的記憶體?
  • 裝置螢幕上一次最多能顯示多少張圖片?有多少圖片需要進行預載入,因為有可能很快也會顯示在螢幕上?
  • 你的裝置的螢幕大小和解析度分別是多少?一個超高解析度的裝置(例如 Galaxy Nexus) 比起一個較低解析度的裝置(例如 Nexus S),在持有相同數量圖片的時候,需要更大的快取空間。
  • 圖片的尺寸和大小,還有每張圖片會佔據多少記憶體空間。
  • 圖片被訪問的頻率有多高?會不會有一些圖片的訪問頻率比其它圖片要高?如果有的話,你也許應該讓一些圖片常駐在記憶體當中,或者使用多個LruCache 物件來區分不同組的圖片。
  • 你能維持好數量和質量之間的平衡嗎?有些時候,儲存多個低畫素的圖片,而在後臺去開執行緒載入高畫素的圖片會更加的有效。

看了sdk後,我用:

Bitmap bm = BitmapFactory.decodeResource(this.getResources(), R.drawable.splash);
BitmapDrawable bd = new BitmapDrawable(this.getResources(), bm);

mBtn.setBackgroundDrawable(bd);

來代替mBtn.setBackgroundResource(R.drawable.splash)。

銷燬的時候使用:

BitmapDrawable bd = (BitmapDrawable)mBtn.getBackground();

mBtn.setBackgroundResource(0);//別忘了把背景設為null,避免onDraw重新整理背景時候出現used a recycled bitmap錯誤

bd.setCallback(null);
bd.getBitmap().recycle();

  

現在android應用中不可避免的要使用圖片,有些圖片是可以變化的,需要每次啟動時從網路拉取,這種場景在有廣告位的應用以及純圖片應用(比如百度美拍)中比較多。

採用 記憶體-檔案-網路 三層cache機制,其中記憶體快取包括強引用快取和軟引用快取(SoftReference),其實網路不算cache,這裡姑且也把它劃到快取的層次結構中。當根據url向網路拉取圖片的時候,先從記憶體中找,如果記憶體中沒有,再從快取檔案中查詢,如果快取檔案中也沒有,再從網路上通過http請求拉取圖片。在鍵值對(key-value)中,這個圖片快取的key是圖片url的hash值,value就是bitmap。所以,按照這個邏輯,只要一個url被下載過,其圖片就被快取起來了。 

/*
 * 圖片管理
 * 非同步獲取圖片,直接呼叫loadImage()函式,該函式自己判斷是從快取還是網路載入
 * 同步獲取圖片,直接呼叫getBitmap()函式,該函式自己判斷是從快取還是網路載入
 * 僅從本地獲取圖片,呼叫getBitmapFromNative()
 * 僅從網路載入圖片,呼叫getBitmapFromHttp()
 * 
 */

public class ImageManager implements IManager
{
	private final static String TAG = "ImageManager";
	
	private ImageMemoryCache imageMemoryCache; //記憶體快取
	
	private ImageFileCache   imageFileCache; //檔案快取
	
	//正在下載的image列表
	public static HashMap<String, Handler> ongoingTaskMap = new HashMap<String, Handler>();
	
	//等待下載的image列表
	public static HashMap<String, Handler> waitingTaskMap = new HashMap<String, Handler>();
	
	//同時下載圖片的執行緒個數
	final static int MAX_DOWNLOAD_IMAGE_THREAD = 4;
	
	private final Handler downloadStatusHandler = new Handler(){
		public void handleMessage(Message msg)
		{
			startDownloadNext();
		}
	};
	
	public ImageManager()
	{
		imageMemoryCache = new ImageMemoryCache();
		imageFileCache = new ImageFileCache();
    }
	
    /**
     * 獲取圖片,多執行緒的入口
     */
    public void loadBitmap(String url, Handler handler) 
    {
        //先從記憶體快取中獲取,取到直接載入
        Bitmap bitmap = getBitmapFromNative(url);
        
        if (bitmap != null)
        {
            Logger.d(TAG, "loadBitmap:loaded from native");
        	Message msg = Message.obtain();
            Bundle bundle = new Bundle();
            bundle.putString("url", url);
            msg.obj = bitmap;
            msg.setData(bundle);
            handler.sendMessage(msg);
        } 
        else
        {
        	Logger.d(TAG, "loadBitmap:will load by network");
        	downloadBmpOnNewThread(url, handler);
        }
    }
    
    /**
     * 新起執行緒下載圖片
     */
    private void downloadBmpOnNewThread(final String url, final Handler handler)
    {
		Logger.d(TAG, "ongoingTaskMap'size=" + ongoingTaskMap.size());
    	
		if (ongoingTaskMap.size() >= MAX_DOWNLOAD_IMAGE_THREAD) 
		{
			synchronized (waitingTaskMap) 
			{
				waitingTaskMap.put(url, handler);
			}
		} 
		else 
		{
			synchronized (ongoingTaskMap) 
			{
				ongoingTaskMap.put(url, handler);
			}

			new Thread() 
			{
				public void run() 
				{
					Bitmap bmp = getBitmapFromHttp(url);

					// 不論下載是否成功,都從下載佇列中移除,再由業務邏輯判斷是否重新下載
					// 下載圖片使用了httpClientRequest,本身已經帶了重連機制
					synchronized (ongoingTaskMap) 
					{
						ongoingTaskMap.remove(url);
					}
					
					if(downloadStatusHandler != null)
					{
						downloadStatusHandler.sendEmptyMessage(0);
					
					}

					Message msg = Message.obtain();
					msg.obj = bmp;
					Bundle bundle = new Bundle();
					bundle.putString("url", url);
					msg.setData(bundle);
					
					if(handler != null)
					{
						handler.sendMessage(msg);
					}

				}
			}.start();
		}
	}

    
	/**
     * 依次從記憶體,快取檔案,網路上載入單個bitmap,不考慮執行緒的問題
     */
	public Bitmap getBitmap(String url)
	{
	    // 從記憶體快取中獲取圖片
	    Bitmap bitmap = imageMemoryCache.getBitmapFromMemory(url);
	    if (bitmap == null) 
	    {
	        // 檔案快取中獲取
	    	bitmap = imageFileCache.getImageFromFile(url);
	        if (bitmap != null) 
	        {	            
	        	// 新增到記憶體快取
	        	imageMemoryCache.addBitmapToMemory(url, bitmap);
	        } 
	        else 
	        {
	            // 從網路獲取
	        	bitmap = getBitmapFromHttp(url);
	        }
	    }
	    return bitmap;
	}
	
	/**
	 * 從記憶體或者快取檔案中獲取bitmap
	 */
	public Bitmap getBitmapFromNative(String url)
	{
		Bitmap bitmap = null;
		bitmap = imageMemoryCache.getBitmapFromMemory(url);
		
		if(bitmap == null)
		{
			bitmap = imageFileCache.getImageFromFile(url);
			if(bitmap != null)
			{
				// 新增到記憶體快取
				imageMemoryCache.addBitmapToMemory(url, bitmap);
			}
		}
		return bitmap;
	}
	
	/**
	 * 通過網路下載圖片,與執行緒無關
	 */
	public Bitmap getBitmapFromHttp(String url)
	{
		Bitmap bmp = null;
		
		try
		{
			byte[] tmpPicByte = getImageBytes(url);
	
			if (tmpPicByte != null) 
			{
				bmp = BitmapFactory.decodeByteArray(tmpPicByte, 0,
						tmpPicByte.length);
			}
			tmpPicByte = null;
		}
		catch(Exception e)
		{
			e.printStackTrace();
		}
		
		if(bmp != null)
		{
			// 新增到檔案快取
			imageFileCache.saveBitmapToFile(bmp, url);
			// 新增到記憶體快取
			imageMemoryCache.addBitmapToMemory(url, bmp);
		}

		return bmp;
	}
	
	/**
	 * 下載連結的圖片資源
	 * 
	 * @param url
	 *            
	 * @return 圖片
	 */
	public byte[] getImageBytes(String url) 
	{
		byte[] pic = null;
		if (url != null && !"".equals(url)) 
		{
			Requester request = RequesterFactory.getRequester(
					Requester.REQUEST_REMOTE, RequesterFactory.IMPL_HC);
			// 執行請求
			MyResponse myResponse = null;
			MyRequest mMyRequest;
			mMyRequest = new MyRequest();
			mMyRequest.setUrl(url);
			mMyRequest.addHeader(HttpHeader.REQ.ACCEPT_ENCODING, "identity");
			InputStream is = null;
			ByteArrayOutputStream baos = null;
			try {
				myResponse = request.execute(mMyRequest);
				is = myResponse.getInputStream().getImpl();
				baos = new ByteArrayOutputStream();
				byte[] b = new byte[512];
				int len = 0;
				while ((len = is.read(b)) != -1) 
				{
					baos.write(b, 0, len);
					baos.flush();
				}
				pic = baos.toByteArray();
				Logger.d(TAG, "icon bytes.length=" + pic.length);

			} 
			catch (Exception e3) 
			{
				e3.printStackTrace();
				try 
				{
					Logger.e(TAG,
							"download shortcut icon faild and responsecode="
									+ myResponse.getStatusCode());
				} 
				catch (Exception e4) 
				{
					e4.printStackTrace();
				}
			} 
			finally 
			{
				try 
				{
					if (is != null) 
					{
						is.close();
						is = null;
					}
				} 
				catch (Exception e2) 
				{
					e2.printStackTrace();
				}
				try 
				{
					if (baos != null) 
					{
						baos.close();
						baos = null;
					}
				} 
				catch (Exception e2) 
				{
					e2.printStackTrace();
				}
				try 
				{
					request.close();
				} 
				catch (Exception e1) 
				{
					e1.printStackTrace();
				}
			}
		}
		return pic;
	}
	
	/**
	 * 取出等待佇列第一個任務,開始下載
	 */
	private void startDownloadNext()
	{
		synchronized(waitingTaskMap)
		{	
			Logger.d(TAG, "begin start next");
			Iterator iter = waitingTaskMap.entrySet().iterator(); 
		
			while (iter.hasNext()) 
			{
				
				Map.Entry entry = (Map.Entry) iter.next();
				Logger.d(TAG, "WaitingTaskMap isn't null,url=" + (String)entry.getKey());
				
				if(entry != null)
				{
					waitingTaskMap.remove(entry.getKey());
					downloadBmpOnNewThread((String)entry.getKey(), (Handler)entry.getValue());
				}
				break;
			}
		}
	}
	
	public String startDownloadNext_ForUnitTest()
	{
		String urlString = null;
		synchronized(waitingTaskMap)
		{
			Logger.d(TAG, "begin start next");
			Iterator iter = waitingTaskMap.entrySet().iterator(); 
		
			while (iter.hasNext()) 
			{
				Map.Entry entry = (Map.Entry) iter.next();
				urlString = (String)entry.getKey();
				waitingTaskMap.remove(entry.getKey());
				break;
			}
		}
		return urlString;
	}
	
	/**
	 * 圖片變為圓角
	 * @param bitmap:傳入的bitmap
	 * @param pixels:圓角的度數,值越大,圓角越大
	 * @return bitmap:加入圓角的bitmap
	 */
	public static Bitmap toRoundCorner(Bitmap bitmap, int pixels) 
	{ 
        if(bitmap == null)
        	return null;
        
		Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Config.ARGB_8888); 
        Canvas canvas = new Canvas(output); 
 
        final int color = 0xff424242; 
        final Paint paint = new Paint(); 
        final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); 
        final RectF rectF = new RectF(rect); 
        final float roundPx = pixels; 
 
        paint.setAntiAlias(true); 
        canvas.drawARGB(0, 0, 0, 0); 
        paint.setColor(color); 
        canvas.drawRoundRect(rectF, roundPx, roundPx, paint); 
 
        paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN)); 
        canvas.drawBitmap(bitmap, rect, rect, paint); 
 
        return output; 
    }
	
	public byte managerId() 
	{
		return IMAGE_ID;
	}
}


OOM,

出現oom,無非主要是以下幾個方面:
  一、載入物件過大
  二、相應資源過多,沒有來不及釋放。
解決這樣的問題,也有一下幾個方面:
  一:在記憶體引用上做些處理,常用的有軟引用、強化引用、弱引用
  二:在記憶體中載入圖片時直接在記憶體中做處理,如:邊界壓縮.
  三:動態回收記憶體
  四:優化Dalvik虛擬機器的堆記憶體分配
  五:自定義堆記憶體大小等

16M = dalvik記憶體(Java) + native記憶體(C/C++)
APP記憶體由 dalvik記憶體 和 native記憶體 2部分組成,dalvik也就是java堆,建立的物件就是就是在這裡分配的,而native是通過c/c++方式申請的記憶體,Bitmap就是以這種方式分配的。(android3.0以後,系統都預設通過dalvik分配的,native作為堆來管理)。這2部分加起來不能超過android對單個程式,虛擬機器的記憶體限制。

造成OOM的可以概括為兩種情況:
1、Bitmap的使用上 (利用Lru的LruCache和DiskLruCache兩個類來解決)
2、執行緒的管理上(利用執行緒池管理解決。不納入本次探討)

Bitmap導致的OOM是比較常見的,而針對Bitmap,常見的有兩種情況:

  • 單個ImageView載入高清大圖的時候
  • ListView或者GridView等批量快速載入圖片的時候
    簡而言之,幾乎都是操作Bitmap的時候發生的。
谷歌提供的方法:
import java.io.FileDescriptor;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;


public class ImageResizer {
    private static final String TAG = "ImageResizer";


    public ImageResizer() {
    }

    // 從資源載入 
    public 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);

        // inJustDecodeBounds 置為 false 真正開始載入圖片
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }

    public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
        // 設定inJustDecodeBounds = true ,表示先不載入圖片
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFileDescriptor(fd, null, options);


        // 呼叫方法計算合適的 inSampleSize
        options.inSampleSize = calculateInSampleSize(options, reqWidth,
                reqHeight);

        // inJustDecodeBounds 置為 false 真正開始載入圖片
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFileDescriptor(fd, null, options);
    }

    // 計算 BitmapFactpry 的 inSimpleSize的值的方法 
    public int calculateInSampleSize(BitmapFactory.Options options,
            int reqWidth, int reqHeight) {
        if (reqWidth == 0 || reqHeight == 0) {
            return 1;
        }

        // 獲取圖片原生的寬和高
        final int height = options.outHeight;
        final int width = options.outWidth;
        Log.d(TAG, "origin, w= " + width + " h=" + height);
        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;
            }
        }

        Log.d(TAG, "sampleSize:" + inSampleSize);
        return inSampleSize;
    }
}

快取主要利用的一個機制是Lru,(Least Recently Used)最近最少使用的。
而Lru和只要是利用兩個類,LruCache 和 DiskLruCache。
LruCache主要針對的是 記憶體快取 (快取);DiskLruCache 主要針對的是 儲存快取 (本地)。

使用ImageLoader、Afinal、xUtils三個框架來解決OOM問題


相關文章