閱讀536 返回首頁    go 阿裏雲 go 技術社區[雲棲]


高效地顯示Bitmap圖片 2 - 在UI線程之外處理Bitmaps

Processing Bitmaps Off the UI Thread [在UI Thread之外處理Bitmap]
  • 在上一課中有介紹一係列的BitmapFactory.decode* 方法,當數據源是網絡或者是磁盤時(或者是任何實際源不在內存的),這些方法都不應該在main UI 線程中執行。那些情況下加載數據是不可以預知的,它依賴於許多因素(從網絡或者硬盤讀取數據的速度, 圖片的大小, CPU的速度, etc.)。如果其中任何一個任務卡住了UI thread, 係統會出現ANR的錯誤。
  • 這一節課會介紹如何使用 AsyncTask 在後台線程中處理bitmap並且演示了如何處理並發(concurrency)的問題。

Use an AsyncTask [使用AsyncTask]

  1. class BitmapWorkerTask extends AsyncTask {  
  2.     private final WeakReference imageViewReference;  
  3.     private int data = 0;  
  4.   
  5.     public BitmapWorkerTask(ImageView imageView) {  
  6.         // Use a WeakReference to ensure the ImageView can be garbage collected  
  7.         imageViewReference = new WeakReference(imageView);  
  8.     }  
  9.   
  10.     // Decode image in background.  
  11.     @Override  
  12.     protected Bitmap doInBackground(Integer... params) {  
  13.         data = params[0];  
  14.         return decodeSampledBitmapFromResource(getResources(), data, 100100));  
  15.     }  
  16.   
  17.     // Once complete, see if ImageView is still around and set bitmap.  
  18.     @Override  
  19.     protected void onPostExecute(Bitmap bitmap) {  
  20.         if (imageViewReference != null && bitmap != null) {  
  21.             final ImageView imageView = imageViewReference.get();  
  22.             if (imageView != null) {  
  23.                 imageView.setImageBitmap(bitmap);  
  24.             }  
  25.         }  
  26.     }  
  27. }  
  • ImageView使用WeakReference 確保了 AsyncTask 所引用的資源可以被GC(garbage collected)。因為當任務結束時不能確保 ImageView 仍然存在,因此你必須在 onPostExecute() 裏麵去檢查引用.  這個ImageView 也許已經不存在了,例如,在任務結束時用戶已經不在那個Activity或者是設備已經發生配置改變(旋轉屏幕等)。
  • 開始異步加載位圖,隻需要創建一個新的任務並執行它即可:
  1. public void loadBitmap(int resId, ImageView imageView) {  
  2.     BitmapWorkerTask task = new BitmapWorkerTask(imageView);  
  3.     task.execute(resId);  
  4. }  

Handle Concurrency [處理並發問題]

  • 通常類似 ListView 與 GridView 等視圖組件在使用上麵演示的AsyncTask 方法時會同時帶來另外一個問題。為了更有效的處理內存,那些視圖的子組件會在用戶滑動屏幕時被循環使用(關於這個原理請參考【Android】ListView中getView的原理與解決多輪重複調用的方法). 如果每一個子視圖都觸發一個 AsyncTask  ,那麼就無法確保當前視圖在結束時,分配的視圖已經進入循環隊列中給另外一個子視圖進行重用。而且, 無法確保所有的異步任務能夠按順序執行完畢。
  • Multithreading for Performance 這篇博文更進一步的討論了如何處理並發並且提供了一種解決方法,當任務結束時ImageView 保存一個最近常使用的AsyncTask引用。(暫時打不開那篇博文,下次試試看,仔細看下) 。使用類似的方法,  AsyncTask 可以擴展出一個類似的模型.
  • 創建一個專用的 Drawable 子類來保存一個可以回到當前工作任務的引用。 在這種情況下,BitmapDrawable 被用來作為占位圖片,它可以在任務結束時顯示到ImageView中。
  1. static class AsyncDrawable extends BitmapDrawable {  
  2.     private final WeakReference bitmapWorkerTaskReference;  
  3.   
  4.     public AsyncDrawable(Resources res, Bitmap bitmap,  
  5.             BitmapWorkerTask bitmapWorkerTask) {  
  6.         super(res, bitmap);  
  7.         bitmapWorkerTaskReference =  
  8.             new WeakReference(bitmapWorkerTask);  
  9.     }  
  10.   
  11.     public BitmapWorkerTask getBitmapWorkerTask() {  
  12.         return bitmapWorkerTaskReference.get();  
  13.     }  
  14. }  
  1. public void loadBitmap(int resId, ImageView imageView) {  
  2.     if (cancelPotentialWork(resId, imageView)) {  
  3.         final BitmapWorkerTask task = new BitmapWorkerTask(imageView);  
  4.         final AsyncDrawable asyncDrawable =  
  5.                 new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);  
  6.         imageView.setImageDrawable(asyncDrawable);  
  7.         task.execute(resId);  
  8.     }  
  9. }  
  • 在上麵的代碼示例中,cancelPotentialWork 方法檢查確保了另外一個在跑的任務已經在ImageView視圖中。如果是這樣,它通過執行 cancel() 方法來取消之前的一個任務. 在小部分情況下, New出來的任務有可能已經存在,這樣就不需要執行這個任務了。下麵演示了如何實現一個 cancelPotentialWork 。
  1. public static boolean cancelPotentialWork(int data, ImageView imageView) {  
  2.     final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);  
  3.   
  4.     if (bitmapWorkerTask != null) {  
  5.         final int bitmapData = bitmapWorkerTask.data;  
  6.         if (bitmapData != data) {  
  7.             // Cancel previous task  
  8.             bitmapWorkerTask.cancel(true);  
  9.         } else {  
  10.             // The same work is already in progress  
  11.             return false;  
  12.         }  
  13.     }  
  14.     // No task associated with the ImageView, or an existing task was cancelled  
  15.     return true;  
  16. }  
  • 在上麵有一個幫助方法, getBitmapWorkerTask(), 被用作檢索任務是否已經被分配到指定的 ImageView:
  1. private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {  
  2.    if (imageView != null) {  
  3.        final Drawable drawable = imageView.getDrawable();  
  4.        if (drawable instanceof AsyncDrawable) {  
  5.            final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;  
  6.            return asyncDrawable.getBitmapWorkerTask();  
  7.        }  
  8.     }  
  9.     return null;  
  10. }  
  • 最後一步是在BitmapWorkerTask 的onPostExecute() 方法裏麵做更新操作:
  1. class BitmapWorkerTask extends AsyncTask {  
  2.     ...  
  3.   
  4.     @Override  
  5.     protected void onPostExecute(Bitmap bitmap) {  
  6.         if (isCancelled()) {  
  7.             bitmap = null;  
  8.         }  
  9.   
  10.         if (imageViewReference != null && bitmap != null) {  
  11.             final ImageView imageView = imageViewReference.get();  
  12.             final BitmapWorkerTask bitmapWorkerTask =  
  13.                     getBitmapWorkerTask(imageView);  
  14.             if (this == bitmapWorkerTask && imageView != null) {  
  15.                 imageView.setImageBitmap(bitmap);  
  16.             }  
  17.         }  
  18.     }  
  19. }  
  • 這個方法不僅僅適用於 ListView 與 GridView 組件,在那些需要循環利用子視圖的組件中同樣適用。隻需要在設置圖片到ImageView的地方調用 loadBitmap 方法。例如,在GridView 中實現這個方法會是在 getView() 方法裏麵調用。

最後更新:2017-04-04 07:03:06

  上一篇:go 高效地顯示Bitmap圖片 3 - 兩種緩存Bitmap的方式
  下一篇:go 高效地顯示Bitmap圖片 1 - 有效率地加載大尺寸的位圖