標籤:
作為一名Android開發人員,相信大家對圖片OOM的問題已經耳熟能詳了,關於圖片緩衝和解決OOM的開源項目也是相當的多,被大家熟知的就是Universal_image_loader和Volley了,Volley在前面的文章中已經有介紹。Universal_image_loader在圖片緩衝功能方面應該算功能最強的,但是感覺很多功能用不上,所以在項目中我一般不太喜歡使用Universal_image_loader(因為本身自己的App源碼非常多,加入這些開源庫就就更大了,容易出現無法編譯的問題,因為Android貌似對一個應用中的方法個數好像有限制,貌似是655**個吧,具體多少我也記不清)。
關於處理圖片緩衝上,我接觸的兩個播放器項目中,使用的都是BitmapFun,BitmapFun 是Google為Android開發提供了一個培訓教程,既然是Google提供的,那麼我覺得作為一名合格的Android開發人員很有必要學習學習,而且BitmapFun非常簡單,基本可以滿足我們項目中對於圖片緩衝處理需求了。
對於開源項目的學習,我通常很少在應用程式層面來學習的,因為如何使用一個開源項目的相關部落格已經相當多了,而且寫得都非常詳細,對於大多數開源項目它都是內建sample的,所以如果想學習如何使用某個開源項目,好好研究sample就行了,但是我始終認為,熟悉經典開源項目源碼才是王道。好了廢話不多說,我們開始學習BitmapFun源碼吧。
1、BitmapFun結構
BitmapFun和其他開源庫的結構稍有不同,因為它僅僅是Google的培訓教程,所以BitmapFun和它的sample放在了一個工程裡面,結構圖如下:上面部分是BitmapFun的應用,下面部分是BitmapFun的源碼。
2、相關類介紹
在BitmapFun中最重要的一個類就是ImageFetcher,請求圖片主要就是調用loadImage方法,但是這個類是繼承ImageResizer,而ImageResizser是繼承ImageWorker,所以我們就從ImageWorker開始學習吧
ImageWorker.java/**這個類用來封裝一次圖片的載入過程,包括使用從緩衝中載入 */public abstract class ImageWorker { private static final String TAG = ImageWorker;//這個變數用於動畫效果,沒有實際意義 private static final int FADE_IN_TIME = 200;//緩衝,包括磁碟緩衝和記憶體緩衝 private ImageCache mImageCache;//建立緩衝需要的參數 private ImageCache.ImageCacheParams mImageCacheParams;//載入過程中,ImageView顯示的圖片 private Bitmap mLoadingBitmap;//是否使用漸層效果 private boolean mFadeInBitmap = true;//是否提前退出任務,如果true,那麼圖片請求回來後是不會顯示出來的 private boolean mExitTasksEarly = false;//是否暫停任務 protected boolean mPauseWork = false; private final Object mPauseWorkLock = new Object(); protected Resources mResources; private static final int MESSAGE_CLEAR = 0; private static final int MESSAGE_INIT_DISK_CACHE = 1; private static final int MESSAGE_FLUSH = 2; private static final int MESSAGE_CLOSE = 3; protected ImageWorker(Context context) { mResources = context.getResources(); } /** * 請求一張圖片的介面 * @param 圖片url * @param 要顯示這種圖片的ImageView */ public void loadImage(Object data, ImageView imageView) { if (data == null) { return; } BitmapDrawable value = null;//如果緩衝對象不為空白,那麼從記憶體緩衝中讀取對象 if (mImageCache != null) { value = mImageCache.getBitmapFromMemCache(String.valueOf(data)); } if (value != null) { // 記憶體快取命中,那麼直接顯示 imageView.setImageDrawable(value); } else if (cancelPotentialWork(data, imageView)) {//記憶體緩衝沒有命中,那麼建立一個圖片請求Task,將imageView作為參數 final BitmapWorkerTask task = new BitmapWorkerTask(imageView);//AsyncDrawable 是BitmapDrawable子類,主要用來存放當前任務的弱應用 final AsyncDrawable asyncDrawable = new AsyncDrawable(mResources, mLoadingBitmap, task);//將asyncDrawable設定到imageView中,這樣imageView和當前任務就一一對應了 imageView.setImageDrawable(asyncDrawable); //調用AsyncTask的executeOnExecutor方法,這個AsyncTask和Android系統中的AsyncTask有些區別,但是使用上一樣的 task.executeOnExecutor(AsyncTask.DUAL_THREAD_EXECUTOR, data); } } /** * 設定載入過程中的預設圖片 * * @param bitmap */ public void setLoadingImage(Bitmap bitmap) { mLoadingBitmap = bitmap; } /** * 將本地圖片設定為預設圖片 * * @param resId */ public void setLoadingImage(int resId) { mLoadingBitmap = BitmapFactory.decodeResource(mResources, resId); } /** * 添加一個緩衝對象,建立磁碟緩衝時需要子線程中完成 * @param fragmentManager * @param cacheParams The cache parameters to use for the image cache. */ public void addImageCache(FragmentManager fragmentManager, ImageCache.ImageCacheParams cacheParams) { mImageCacheParams = cacheParams; mImageCache = ImageCache.getInstance(fragmentManager, mImageCacheParams);//完成磁碟緩衝初始化 new CacheAsyncTask().execute(MESSAGE_INIT_DISK_CACHE); } /** * Adds an {@link ImageCache} to this {@link ImageWorker} to handle disk and memory bitmap * caching. * @param activity * @param diskCacheDirectoryName See * {@link ImageCache.ImageCacheParams#ImageCacheParams(Context, String)}. */ public void addImageCache(FragmentActivity activity, String diskCacheDirectoryName) { mImageCacheParams = new ImageCache.ImageCacheParams(activity, diskCacheDirectoryName); mImageCache = ImageCache.getInstance(activity.getSupportFragmentManager(), mImageCacheParams); new CacheAsyncTask().execute(MESSAGE_INIT_DISK_CACHE); } /** * 設定是否使用漸層效果 */ public void setImageFadeIn(boolean fadeIn) { mFadeInBitmap = fadeIn; }//是否提前退出任務 public void setExitTasksEarly(boolean exitTasksEarly) { mExitTasksEarly = exitTasksEarly; setPauseWork(false); } /** * Subclasses should override this to define any processing or work that must happen to produce * the final bitmap. This will be executed in a background thread and be long running. For * example, you could resize a large bitmap here, or pull down an image from the network. * * @param data The data to identify which image to process, as provided by * {@link ImageWorker#loadImage(Object, ImageView)} * @return The processed bitmap */ protected abstract Bitmap processBitmap(Object data); /** * @return The {@link ImageCache} object currently being used by this ImageWorker. */ protected ImageCache getImageCache() { return mImageCache; } /** * Cancels any pending work attached to the provided ImageView. * @param imageView */ public static void cancelWork(ImageView imageView) {//通過ImageView找到task,為什麼可以找到?因為imageView和task一一對應 final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);//如果task不為空白,那麼取消 if (bitmapWorkerTask != null) { bitmapWorkerTask.cancel(true); if (BuildConfig.DEBUG) { final Object bitmapData = bitmapWorkerTask.data; Log.d(TAG, cancelWork - cancelled work for + bitmapData); } } } /** * Returns true if the current work has been canceled or if there was no work in * progress on this image view. * Returns false if the work in progress deals with the same data. The work is not * stopped in that case. */ public static boolean cancelPotentialWork(Object data, ImageView imageView) {//通過imageView找到task final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); if (bitmapWorkerTask != null) {//如果找到的task不為null,並且task的url和給定的url相同,那麼取消任務 final Object bitmapData = bitmapWorkerTask.data; if (bitmapData == null || !bitmapData.equals(data)) { bitmapWorkerTask.cancel(true); if (BuildConfig.DEBUG) { Log.d(TAG, cancelPotentialWork - cancelled work for + data); } } else { // The same work is already in progress. return false; } } return true; } /** * 通過iamgeView找到對應的Task */ private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { if (imageView != null) { final Drawable drawable = imageView.getDrawable(); if (drawable instanceof AsyncDrawable) { final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; return asyncDrawable.getBitmapWorkerTask(); } } return null; } /** * 一個請求圖片的非同步任務, */ private class BitmapWorkerTask extends AsyncTask<object, bitmapdrawable=""> {//請求圖片的url private Object data;//持有ImageView的弱引用 private final WeakReference imageViewReference; public BitmapWorkerTask(ImageView imageView) { imageViewReference = new WeakReference(imageView); } /** * Background processing. */ @Override protected BitmapDrawable doInBackground(Object... params) { if (BuildConfig.DEBUG) { Log.d(TAG, doInBackground - starting work); } data = params[0]; final String dataString = String.valueOf(data); Bitmap bitmap = null; BitmapDrawable drawable = null; // 如果work已經暫停並且圖片請求沒有取消,那麼就等待 synchronized (mPauseWorkLock) { while (mPauseWork && !isCancelled()) { try { mPauseWorkLock.wait(); } catch (InterruptedException e) {} } }//如果有緩衝,並且沒有取消,當前弱引用中的imageView對應的task還是自己(task),那麼從磁碟緩衝中讀取//為什麼在這裡讀磁碟緩衝?因為磁碟緩衝只能在非同步線程讀取,doingbackground就是在非同步線程執行 if (mImageCache != null && !isCancelled() && getAttachedImageView() != null && !mExitTasksEarly) { bitmap = mImageCache.getBitmapFromDiskCache(dataString); } //如果沒有命中,並且沒有取消,並且當前弱引用中的ImageView對應的task還是自己,那麼請求網狀圖片,//調用processBitmap方法,這個方法是個抽象的,在ImageFecter中實現 if (bitmap == null && !isCancelled() && getAttachedImageView() != null && !mExitTasksEarly) { bitmap = processBitmap(params[0]); } // If the bitmap was processed and the image cache is available, then add the processed // bitmap to the cache for future use. Note we don‘t check if the task was cancelled // here, if it was, and the thread is still running, we may as well add the processed // bitmap to our cache as it might be used again in the future if (bitmap != null) { if (Utils.hasHoneycomb()) { // Running on Honeycomb or newer, so wrap in a standard BitmapDrawable drawable = new BitmapDrawable(mResources, bitmap); } else { // Running on Gingerbread or older, so wrap in a RecyclingBitmapDrawable // which will recycle automagically drawable = new RecyclingBitmapDrawable(mResources, bitmap); }//將圖片加入緩衝 if (mImageCache != null) { mImageCache.addBitmapToCache(dataString, drawable); } } if (BuildConfig.DEBUG) { Log.d(TAG, doInBackground - finished work); } return drawable; } /** * Once the image is processed, associates it to the imageView */ @Override protected void onPostExecute(BitmapDrawable value) { // 如果取消了或者提前退出,那麼不顯示這個圖片,直接設定null if (isCancelled() || mExitTasksEarly) { value = null; } final ImageView imageView = getAttachedImageView(); if (value != null && imageView != null) { if (BuildConfig.DEBUG) { Log.d(TAG, onPostExecute - setting bitmap); }//將圖片顯示出來 setImageDrawable(imageView, value); } } @Override protected void onCancelled(BitmapDrawable value) { super.onCancelled(value);//任務取消了,必須通知後台線程停止等待 synchronized (mPauseWorkLock) { mPauseWorkLock.notifyAll(); } } private ImageView getAttachedImageView() { final ImageView imageView = imageViewReference.get(); final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); if (this == bitmapWorkerTask) { return imageView; } return null; } } /** *用於實現imageView和task一一對應的類 */ private static class AsyncDrawable extends BitmapDrawable { private final WeakReference bitmapWorkerTaskReference; public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { super(res, bitmap); bitmapWorkerTaskReference = new WeakReference(bitmapWorkerTask); } public BitmapWorkerTask getBitmapWorkerTask() { return bitmapWorkerTaskReference.get(); } } /** * 顯示圖片,漸層顯示或者普通顯示 * * @param imageView * @param drawable */ private void setImageDrawable(ImageView imageView, Drawable drawable) { if (mFadeInBitmap) { // Transition drawable with a transparent drawable and the final drawable final TransitionDrawable td = new TransitionDrawable(new Drawable[] { new ColorDrawable(android.R.color.transparent), drawable }); // Set background to loading bitmap imageView.setBackgroundDrawable( new BitmapDrawable(mResources, mLoadingBitmap)); imageView.setImageDrawable(td); td.startTransition(FADE_IN_TIME); } else { imageView.setImageDrawable(drawable); } }}
分析完ImageWorker之後,我們發現在ImageWorker中已經提供了擷取網狀圖片的方法loadImage,當我調用了此方法後,首先會試圖從記憶體緩衝擷取圖片,如果擷取成功,直接返回,如果沒有擷取成功,則啟動一個BitmapWorkerTask,使用非同步線程擷取圖片,在非同步線程中,首先到磁碟中擷取,如果磁碟沒有擷取,最後才從網路擷取,我們發現在BitmapWorkerTask中是通過調用processBitmap方法完成圖片擷取的,但是這個方法是一個抽象方法,需要子類去實現,那我們到它的子類ImageResizer中
@Override protected Bitmap processBitmap(Object data) { return processBitmap(Integer.parseInt(String.valueOf(data))); }
它調用的是另外一個重載的processBitmap方法,我們看看另外一個方法吧
private Bitmap processBitmap(int resId) { if (BuildConfig.DEBUG) { Log.d(TAG, processBitmap - + resId); } return decodeSampledBitmapFromResource(mResources, resId, mImageWidth, mImageHeight, getImageCache()); }
我們發現這個方法僅僅是用來載入本地圖片的,那它是如何?網狀圖片的載入的呢,如果你把ImageResizer源碼通讀一邊,你會發現ImageResizer這個類的主要功能如下:
1、設定顯示圖片的sizse
2、從磁碟緩衝中載入圖片
所以從網路載入圖片根本不是這個類的功能,聰明的同學馬上就應該想到了ImageFetcher這個類,對!,我們就直接看看ImageFetcher這個類吧
private Bitmap processBitmap(String data) { final String key = ImageCache.hashKeyForDisk(data); FileDescriptor fileDescriptor = null; FileInputStream fileInputStream = null; DiskLruCache.Snapshot snapshot;//檢查mHttpDiskCache是否已經初始化,這裡一定要注意,mHttpDiskCache這個磁碟緩衝是在ImageFetcher調用addImageCache時初始化的,如果你沒有調用addImageCache//那麼這裡就會阻塞,從而無法擷取圖片,具體情況還請大家自己分析代碼吧 synchronized (mHttpDiskCacheLock) { // Wait for disk cache to initialize while (mHttpDiskCacheStarting) { try { mHttpDiskCacheLock.wait(); } catch (InterruptedException e) {} }//下面這段代碼就是從mHttpDiskCache裡面寫入圖片 if (mHttpDiskCache != null) { try { snapshot = mHttpDiskCache.get(key); if (snapshot == null) { if (BuildConfig.DEBUG) { Log.d(TAG, processBitmap, not found in http cache, downloading...); } DiskLruCache.Editor editor = mHttpDiskCache.edit(key); if (editor != null) {//下載圖片邏輯在這裡 if (downloadUrlToStream(data, editor.newOutputStream(DISK_CACHE_INDEX))) { editor.commit(); } else { editor.abort(); } } snapshot = mHttpDiskCache.get(key); } if (snapshot != null) { fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX); fileDescriptor = fileInputStream.getFD(); } } catch (IOException e) { Log.e(TAG, processBitmap - + e); } catch (IllegalStateException e) { Log.e(TAG, processBitmap - + e); } finally { if (fileDescriptor == null && fileInputStream != null) { try { fileInputStream.close(); } catch (IOException e) {} } } } } Bitmap bitmap = null; if (fileDescriptor != null) {//調用ImageResizer中的方法來將mHttpDiskCache中的緩衝產生指定大小的圖片 bitmap = decodeSampledBitmapFromDescriptor(fileDescriptor, mImageWidth, mImageHeight, getImageCache()); } if (fileInputStream != null) { try { fileInputStream.close(); } catch (IOException e) {} } return bitmap; } /** * 從網路通過HttpURLConnection下載圖片,並寫入到磁碟緩衝 * * @param urlString The URL to fetch * @return true if successful, false otherwise */ public boolean downloadUrlToStream(String urlString, OutputStream outputStream) { disableConnectionReuseIfNecessary(); HttpURLConnection urlConnection = null; BufferedOutputStream out = null; BufferedInputStream in = null; try { final URL url = new URL(urlString); urlConnection = (HttpURLConnection) url.openConnection(); in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE); out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE); int b; while ((b = in.read()) != -1) { out.write(b); } return true; } catch (final IOException e) { Log.e(TAG, Error in downloadBitmap - + e); } finally { if (urlConnection != null) { urlConnection.disconnect(); } try { if (out != null) { out.close(); } if (in != null) { in.close(); } } catch (final IOException e) {} } return false; }
好了,對於Bitmapfun的整個代碼邏輯我就簡單的分析到這裡吧,其實瞭解了Bitmapfun的代碼邏輯後,我們完全可以對其進行最佳化,我在這裡僅僅提出一點可以最佳化的地方,最佳化的方法就交給大家完成吧
比如BitmapWorkerTask在擷取圖片的時候先是讀取磁碟緩衝,然後從網路擷取,也就是說如果讀取本地和讀取網狀圖片時在同一條線程中完成的,這個時候就有可能出現一個問題,本地圖片存在卻無法載入出來:例如:在網路條件不好的情況下,前面的五個圖片請求剛好用完了所有的線程,由於網路條件不好,一直沒有返回,而第六個圖片剛好有緩衝,那麼它是無法載入出來的,因為沒有線程了,所以解決方案就是學習Volley(我前面的文章對於Volley已經介紹了)中的解決方案,讓一條線程專門處理本地圖片,其他線程用於處理網狀圖片。
就寫到這裡吧,如果大家有什麼沒看明白或者我寫錯了的,歡迎留言.....
Android圖片處理神器BitmapFun源碼分析