圖片高效加載(二) 圖片的異步加載


圖片的異步加載是利用AsynTask類對圖像進行后台加載完成后再給ImageView,先轉載一篇前人的較好的總結后面再添加一些自己的見解和貼上完整的實現demo。

前面的轉自:https://my.oschina.net/rengwuxian/blog/183802

摘要: 有沒有過這種體驗:你在Android手機上打開了一個帶有含圖片的ListView的頁面,用手猛地一划,就見那ListView嘎嘎地卡,仿佛每一個新的Item都是頂着阻力蹦出來的一樣?看完這篇文章,你將學會怎樣避免這種情況的發生。

為什么要在后台加載Bitmap?

  有沒有過這種體驗:你在Android手機上打開了一個帶有含圖片的ListView的頁面,用手猛地一划,就見那ListView嘎嘎地卡,仿佛每一個新的Item都是頂着阻力蹦出來的一樣?看完這篇文章,你將學會怎樣避免這種情況的發生。

在Android中,使用BitmapFactory.decodeResource(), BitmapFactory.decodeStream() 等方法可以把圖片加載到Bitmap中。但由於這些方法是耗時的,所以多數情況下,這些方法應該放在非UI線程中,否則將有可能導致界面的卡頓,甚至是觸發ANR。

一般情況下,網絡圖片的加載必須放在后台線程中;而本地圖片就可以根據實際情況自行決定了,如果圖片不多不大的話,也可以在UI線程中操作來圖個方便。至於谷歌官方的說法,是只要是從硬盤或者從網絡加載Bitmap,統統不應該在主線程中進行。

基礎操作:使用AsyncTask,其中decodeSampledBitmapFromResource函數是上一篇中講到的將圖像縮小后加載到控件節約內存和提高加載效率:http://www.cnblogs.com/bokeofzp/p/6064767.html

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    private final WeakReference<ImageView> imageViewReference;
    private int data = 0;

    public BitmapWorkerTask(ImageView imageView) {
        // Use a WeakReference to ensure the ImageView can be garbage collected
        imageViewReference = new WeakReference<ImageView>(imageView);
    }

    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        data = params[0];
        return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
    }

    // Once complete, see if ImageView is still around and set bitmap.
    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (imageViewReference != null && bitmap != null) {
            final ImageView imageView = imageViewReference.get();
            if (imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}

以上代碼摘自Android官方文檔,是一個后台加載Bitmap並在加載完成后自動將Bitmap設置到ImageView的AsyncTask的實現。有了這個AsyncTask之后,異步加載Bitmap只需要下面的簡單代碼:

public void loadBitmap(int resId, ImageView imageView)
 {
    BitmapWorkerTask task = new BitmapWorkerTask(imageView);
    task.execute(resId);
 }

然后,一句loadBitmap(R.id.my_image, mImageView) 就能實現本地圖片的異步加載了。

並發操作:在ListView和GridView中進行后台加載

在實際中,影響性能的往往是ListView和GridView這種包含大量圖片的控件。在滑動過程中,大量的新圖片在短時間內一起被加載,對於沒有進行任何優化的程序,卡頓現象必然會隨之而來。通過使用后台加載Bitmap的方式,這種問題將被有效解決。具體怎么做,我們來看看谷歌推薦的方法。

首先創建一個實現了Drawable接口的類,用來存儲AsyncTask的引用。在本例中,選擇了繼承BitmapDrawable,用來給ImageView設置一個預留的占位圖,這個占位圖用於在AsyncTask執行完畢之前的顯示。

static class AsyncDrawable extends BitmapDrawable {
    private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;

    public AsyncDrawable(Resources res, Bitmap bitmap,
            BitmapWorkerTask bitmapWorkerTask) {
        super(res, bitmap);
        bitmapWorkerTaskReference =
            new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
    }

    public BitmapWorkerTask getBitmapWorkerTask() {
        return bitmapWorkerTaskReference.get();
    }
}

接下來,和上面類似,依然是使用一個loadBitmap()方法來實現對圖片的異步加載。不同的是,要在啟動AsyncTask之前,把AsyncTask傳給AsyncDrawable,並且使用AsyncDrawable為ImageView設置占位圖:

public void loadBitmap(int resId, ImageView imageView) 
{
    if (cancelPotentialWork(resId, imageView)) {
        final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
        final AsyncDrawable asyncDrawable =
                new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
        imageView.setImageDrawable(asyncDrawable);
        task.execute(resId);
    }
}

然后在Adapter的getView()方法中調用loadBitmap()方法,就可以為每個Item中的ImageView進行圖片的動態加載了。

loadBitmap()方法的代碼中有兩個地方需要注意:第一,cancelPotentialWork()這個方法,它的作用是進行兩項檢查:首先檢查當前是否已經有一個AsyncTask正在為這個ImageView加載圖片,如果沒有就直接返回true。如果有,再檢查這個Task正在加載的資源是否與自己正要進行加載的資源相同,如果相同,那就沒有必要再進行多一次的加載了,直接返回false;而如果不同(為什么會不同?文章最后會有解釋),就取消掉這個正在進行的任務,並返回true。第二個需要注意的是,本例中的 BitmapWorkerTask 實際上和上例是有所不同的。這兩點我們分開說,首先我們看cancelPotentialWork()方法的代碼:

public static boolean cancelPotentialWork(int data, ImageView imageView) {
    final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);

    if (bitmapWorkerTask != null) {
        final int bitmapData = bitmapWorkerTask.data;
        if (bitmapData != data) {
            // 取消之前的任務
            bitmapWorkerTask.cancel(true);
        } else {
            // 相同任務已經存在,直接返回false,不再進行重復的加載
            return false;
        }
    }
    // 沒有Task和ImageView進行綁定,或者Task由於加載資源不同而被取消,返回true
    return true;
}

在cancelPotentialWork()的代碼中,首先使用getBitmapWorkerTask()方法獲取到與ImageView相關聯的Task,然后進行上面所說的判斷。好,我們接着來看這個getBitmapWorkerTask()是怎么寫的:

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;
}
從代碼中可以看出, 該方法通過imageView獲取到它內部的Drawable對象,如果獲取到了並且該對象為AsyncDrawable的實例,就調用這個AsyncDrawable的getBitmapWorkerTask()方法來獲取到它對應的Task,也就是通過一個ImageView->Drawable->AsyncTask的鏈來獲取到ImageView所對應的AsyncTask。

好的,cancelPotentialWork()方法分析完了,我們回到剛才提到的第二個點:BitmapWorkerTask類的不同。這個類的改動在於onPostExecute()方法,具體請看下面代碼:

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (isCancelled()) {
            bitmap = null;
        }

        if (imageViewReference != null && bitmap != null) {
            final ImageView imageView = imageViewReference.get();
            final BitmapWorkerTask bitmapWorkerTask =
                    getBitmapWorkerTask(imageView);
            if (this == bitmapWorkerTask && imageView != null) {
                imageView.setImageBitmap(bitmap);
            }
        }
    }
}
從代碼中可以看出,在后台加載完Bitmap之后,它  並不是直接把Bitmap設置給ImageView,而是先判斷這個ImageView對應的Task是不是自己 (為什么會不同?文章最后會有解釋)。如果是自己,才會執行ImageView的setImageBitmap()方法。到此,一個並發的異步加載ListView(或GridView)中圖片的實現全部完成。

延伸:文中兩個“為什么會不同”的解答

首先,簡單說一下ListView中Item和Item對應的View的關系(GridView中同理)。假設一個ListView含有100項,那么它的100個Item應該分別對應一個View用於顯示,這樣一共是100個View。但Android實際上並沒有這樣做。出於內存考慮,Android只會為屏幕上可見的每個Item分配一個View。用戶滑動ListView,當第一個Item移動到可視范圍外后,他所對應的View將會被系統分配給下一個即將出現的Item。

回到問題。

我們不妨假設屏幕上顯示了一個ListView,並且它最多能顯示10個Item,而用戶在最頂部的Item(不妨稱他為第1個Item)使用Task加載Bitmap的時候進行了滑動,並且直到第1個Item消失而第11個Item已經在屏幕底部出現的時候,這個Task還沒有加載完成。那么此時,原先與第1個Item綁定的ImageView已經被重新綁定到了第11個Item上,並且第11個Item觸發了getItem()方法。在getItem()方法中,ImageView第二次使用Task為自己加載Bitmap,但這時它需要加載的圖片資源已經變了(由第1個Item對應的資源變成了第11個Item對應的資源),因此在cancelPotentialWork()方法執行時會判斷兩個資源不一致。這就是為什么相同ImageView卻對應了不同的資源。

同理,一個Task持有了一個ImageView,但由於這個Task有可能已經過時,因此這個ImageView所對應的Task未必就是這個Task本身,也有可能是另一個更年輕的Task。

 

好了以上是大神寫的一篇文章,將如何異步加載圖片寫的非常透徹,但有些地方與我的理解有些偏差:

1、listView中的view為了節約資源是只創建了屏幕顯示的個數的view,然而並非在一個view消失時候並非調用getItem,Imageview第二次為自己使用Task加載Bitmap的,通過查看源碼發現,listView是AdapterView的子類,在AdapterView中Adapter接口是一個成員,用戶利用Adapter的接口方法來完成AdapterView的一些函數功能的,如調用:

listView.getItemAtPosition(int position)

返回的就是getItem()返回的值,而若返回空並不影響listView的顯示功能,getItem()只是AdapterView的成員Adapter拋出的方法,而AdapterView利用這些未實現的方法來實現自己預先的功能。具體listView如何將getView返回的view筆者看源碼牽扯太多沒看下去。

2、就是最后一塊代碼里,若cancel(true)了 onPostExecute(Bitmap bitmap)這個函數是不會調用的。可以將其改進:一些解釋看注視吧。

 protected void onPostExecute(Bitmap bitmap) {
        //如果cancel了就不會執行此函數了,所以覺得沒必要這一步判斷
//        if(isCancelled())
//            bitmap = null;
        final ImageView imageView = weakReference.get();
        if(imageView!=null&&bitmap!=null)//將imageView!=null提當上面來應該可以加快判斷效率,實驗weakReference不為空時ImageView可能為空
        {
            final ImageTask bitmapWorkerTask = getBitmapWorkerTask(imageView);//如快速滑動listview,View的圖片未加載完成就會讓其加載另外一張,此時imageview為同一個而任務AsynTask不同,且此時ImageView的drawable的綁定已經換了,asynTask也換了
//這里通過getBitmapWorkerTask(imageView)永遠獲取的是最新的asynTask
                    if (this == bitmapWorkerTask)
                    {
                        imageView.setImageBitmap(bitmap);
                    }
        }
        super.onPostExecute(bitmap);
    }

好了指出了一些瑕疵,但仍是值得我們學習的好文章。下面給出我實現出來代碼,其中將drawable類寫成了asyntask的靜態內部類了,當我們需要異步加載圖片的時候只需要調用這個類就可以完成啦。

package com.example.user.imagecashdemo;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.widget.ImageView;
import java.lang.ref.WeakReference;

/**
 * Created by user on 2016/11/15.
 */
public class ImageTask extends AsyncTask <Integer , Void  , Bitmap>{
    private WeakReference <ImageView> weakReference;
    private Context context;
    private Bitmap defaultImage;
    public int data;

  public  ImageTask(ImageView view,Context context ,Bitmap defaultImage)
  {
      this.defaultImage = defaultImage;
      this.weakReference = new WeakReference(view);
      this.context = context;
  }
    @Override
    protected Bitmap doInBackground(Integer... params) {
        data = params[0];
       Bitmap bitmap = BitmapFactory.decodeResource(context.getResources() , data);
        return bitmap;
    }

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        //如果cancel了就不會執行此函數了,所以覺得沒必要這一步判斷
//        if(isCancelled())
//            bitmap = null;
        final ImageView imageView = weakReference.get();
        if(imageView!=null&&bitmap!=null)//將imageView!=null提當上面來應該可以加快判斷效率,實驗weakReference不為空時ImageView可能為空
        {
            final ImageTask bitmapWorkerTask = getBitmapWorkerTask(imageView);//如快速滑動listview,View的圖片未加載完成就會讓其加載另外一張,此時imageview為同一個而任務AsynTask不同,且此時ImageView的drawable的綁定已經換了,asynTask也換了
//這里通過getBitmapWorkerTask(imageView)永遠獲取的是最新的asynTask
                    if (this == bitmapWorkerTask)
                    {
                        imageView.setImageBitmap(bitmap);
                    }
        }
        super.onPostExecute(bitmap);
    }

    static class AsyncDrawable extends BitmapDrawable {
        private final WeakReference<ImageTask> bitmapWorkerTaskReference;

        public AsyncDrawable(Resources res, Bitmap bitmap,
                             ImageTask bitmapWorkerTask) {
            super(res, bitmap);
            bitmapWorkerTaskReference =
                    new WeakReference<ImageTask>(bitmapWorkerTask);
        }

        public ImageTask getBitmapWorkerTask() {
            return bitmapWorkerTaskReference.get();
        }
    }

    public static boolean cancelPotentialWork(int data, ImageView imageView) {
        final ImageTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
        if (bitmapWorkerTask != null) {
            final int bitmapData = bitmapWorkerTask.data;
            if (bitmapData != data) {
                // 取消之前的任務
                bitmapWorkerTask.cancel(true);
            } else {
                // 相同任務已經存在,直接返回false,不再進行重復的加載
                return false;
            }
        }
        // 沒有Task和ImageView進行綁定,或者Task由於加載資源不同而被取消,返回true
        return true;
    }

    private static ImageTask getBitmapWorkerTask(ImageView imageView) {
        if (imageView != null) {
            //通過imageView的drawable反過來獲取與之對應的AsynTask
            final Drawable drawable = imageView.getDrawable();
            if (drawable instanceof ImageTask.AsyncDrawable) {
                final ImageTask.AsyncDrawable asyncDrawable = (ImageTask.AsyncDrawable) drawable;
                return asyncDrawable.getBitmapWorkerTask();
            }
        }
        return null;
    }

    public void loadImage(int data ,ImageView imageView)
    {
        if(cancelPotentialWork(data ,imageView))
        {
            ImageTask.AsyncDrawable asyncDrawable= new ImageTask.AsyncDrawable(context.getResources() , defaultImage ,this);//defaultBitmap默認的一張圖所以只需要加載一次到內存即可
            imageView.setImageDrawable(asyncDrawable);
            this.execute(data);
        }
    }

}
View Code

下面外面只需要在Adapter的getView里創建ImageTask對象,然后loadImage方法就可以了

package com.example.user.imagecashdemo;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Adapter;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ListView;

public class MainActivity extends AppCompatActivity {

    private ListView list;
    private Bitmap defaultBitmap;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        list = (ListView) findViewById(R.id.list);

        list.setAdapter(new MyAdapter());
        defaultBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.fy_default);

    }

    class MyAdapter extends BaseAdapter {
        int number = 30;

        @Override
        public int getCount() {
            return number;
        }

        @Override
        public Object getItem(int position) {
            return R.drawable.fy;

        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ImageView view = new ImageView(MainActivity.this);
            ImageTask imageTask = new ImageTask(view , MainActivity.this , defaultBitmap);
            imageTask.loadImage(R.drawable.fy ,view);
            return view;
        }
    }




}
View Code

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM