android 之圖片異步加載


一.概述

本文來自"慕課網" 的學習,只是對代碼做一下分析

圖片異步加載有2種方式:  (多線程/線程池) 或者 用其實AsyncTask , 其實AsyncTask底層也是用的多線程.

使用緩存的好處是 , 提高流暢度, 節約流量.

二.代碼

1.先看圖片加載工具類

public class ImageLoader {
    private ImageView mImageview;
    private String mUrl;
    //創建緩存
    private LruCache<String, Bitmap> mCaches;
    private ListView mListView;
    private Set<NewsAsyncTask> mTask;

    public ImageLoader(ListView listView) {
        mListView = listView;
        mTask = new HashSet<>();
        //獲得最大的緩存空間
        int maxMemory = (int) Runtime.getRuntime().maxMemory();
        //賦予緩存區最大緩存的四分之一進行緩存
        int cacheSize = maxMemory / 4;
        mCaches = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                //在每次存入緩存的時候調用
                return value.getByteCount();
            }
        };
    }

    //將圖片通過url與bitmap的鍵值對形式添加到緩存中
    public void addBitmapToCache(String url, Bitmap bitmap) {
        if (getBitmapFromCache(url) == null) {
            mCaches.put(url, bitmap);
        }
    }

    //通過緩存得到圖片
    public Bitmap getBitmapFromCache(String url) {
        return mCaches.get(url);
    }

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (mImageview.getTag().equals(mUrl))
                mImageview.setImageBitmap((Bitmap) msg.obj);
        }
    };

    //通過線程的方式去展示圖片
    public void showImageByThread(ImageView imageView, String url) {
        mImageview = imageView;
        mUrl = url;
        new Thread() {
            @Override
            public void run() {
                super.run();
                Bitmap bitmap = getBitmapFromUrl(mUrl);
                Message message = Message.obtain();
                message.obj = bitmap;
                mHandler.sendMessage(message);
            }
        }.start();
    }

    //通過異步任務的方式去加載圖片

    public void showImageByAsyncTask(ImageView imageView, String url) {
        //先從緩存中獲取圖片
        Bitmap bitmap = getBitmapFromCache(url);
        if (bitmap == null) {
           imageView.setImageResource(R.mipmap.ic_launcher);
        } else {
            imageView.setImageBitmap(bitmap);
        }
    }

    private class NewsAsyncTask extends AsyncTask<String, Void, Bitmap> {

   //     private ImageView mImageView;
        private String mUrl;

        public NewsAsyncTask( String url) {
   //         mImageview = imageView;
            mUrl = url;
        }

        @Override
        protected Bitmap doInBackground(String... params) {
            String url = params[0];
            //從網絡獲取圖片
            Bitmap bitmap = getBitmapFromUrl(url);
            //將圖片加入緩存中
            if (bitmap != null) {
                addBitmapToCache(url, bitmap);
            }
            return bitmap;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
            ImageView imageView = (ImageView) mListView.findViewWithTag(mUrl);
            if (imageView!=null&&bitmap!=null){
                imageView.setImageBitmap(bitmap);
            }
            mTask.remove(this);
        }
    }

    //滑動時加載圖片
    public void loadImages(int start, int end) {
        for (int i = start; i < end; i++) {
            String url = NewsAdapter.URLS[i];
            //先從緩存中獲取圖片
            Bitmap bitmap = getBitmapFromCache(url);
            if (bitmap == null) {
                NewsAsyncTask task = new NewsAsyncTask(url);
                task.execute(url);
                mTask.add(task);
            } else {
                ImageView imageView = (ImageView) mListView.findViewWithTag(url);
                imageView.setImageBitmap(bitmap);
            }
        }
    }

    //停止時取消所有任務加載
    public void cancelAllTasks(){
        if (mTask!=null){
            for (NewsAsyncTask task :mTask){
                task.cancel(false);
            }
        }
    }
    //網絡獲取圖片
    private Bitmap getBitmapFromUrl(String urlString) {
        Bitmap bitmap;
        InputStream is = null;
        try {
            URL url = new URL(urlString);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            is = new BufferedInputStream(connection.getInputStream());
            bitmap = BitmapFactory.decodeStream(is);
            connection.disconnect();
            return bitmap;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                assert is != null;
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}

需要注意的幾個部分:

  <1

LruCache<String, Bitmap> mCaches 這是創建一個集合去存儲緩存的圖片,底層是HashMap實現的,其實和我們之前java中用到HashMap  弱引用/軟引用比較類似, 但是
自2.3以后Android將更頻繁的調用GC,導致軟引用緩存的數據極易被釋放。所以不能用之前的方式來緩存圖片了,
LruCache使用一個LinkedHashMap簡單的實現內存的緩存,沒有軟引用,都是強引用。如果添加的數據大於設置的最大值,就刪除最先緩存的數據來調整內存。
我們可以在構造方法中,先得到當前應用所占總緩存大小,然后分出1/4用於存儲圖片,對應代碼如下:

 //獲得當前應用最大的緩存空間
        int maxMemory = (int) Runtime.getRuntime().maxMemory();
        //賦予緩存區最大緩存的四分之一進行緩存
        int cacheSize = maxMemory / 4;
        mCaches = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                //在每次存入緩存的時候調用
                return value.getByteCount();
            }
        };

 

<2
Set<NewsAsyncTask> mTask
定義一個Task任務集合,每個任務對應一個圖片,當該圖片被加載后要是否這個對應的task,對應代碼如下:
 ImageView imageView = (ImageView) mListView.findViewWithTag(mUrl);
            if (imageView!=null&&bitmap!=null){
                imageView.setImageBitmap(bitmap);
            }
            mTask.remove(this);

<3代碼中根據 adapter 給每個圖片設置 Tag 標識來獲取圖片,作用是: 避免 listview滾動時,由於convertView緩存造成圖片錯位顯示, 對應代碼如下: ----------> adapter代碼后面給出

 private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (mImageview.getTag().equals(mUrl))//--------------根據adapter設置的tag獲取
                mImageview.setImageBitmap((Bitmap) msg.obj);
        }
    };

<4

當listview一邊滾動,一邊加載圖片會造成一個問題,可能會出現暫時的卡頓現象,盡管這個現象是偶爾發生,如果網絡不好情況下,會加重這種情況,這是為什么呢?

因為listview滾動時,對畫面流暢度要求比較高
雖然異步加載是在新線程中執行的,並未阻塞UI線程,當加載好圖片后,去更新UI線程
就會導致UI線程發生一次重繪,如果這次重繪正好發生在listview滾動的時候
就會導致這個listview滾動過程中卡頓一下, 這樣用戶體驗大大滴不好

為解決該問題:

我們可以在 listview滾動停止后 才去加載可見項, listview滾動過程中,取消加載項(滾動過程中不加載圖片數據)
就能解決這個問題, 因為我們在滾動過程中,其實我並不關心 滾動的內容,我只會關心 滾動停止后要顯示的內容,所以這么做是 完全OK的.  對應代碼如下:

 //滑動時加載圖片 , 這里的 start 和end是 listview第一個和最后一個可見項
// adapter代碼中會有詳述
public void loadImages(int start, int end) { for (int i = start; i < end; i++) { String url = NewsAdapter.URLS[i]; //先從緩存中獲取圖片 Bitmap bitmap = getBitmapFromCache(url); if (bitmap == null) { NewsAsyncTask task = new NewsAsyncTask(url); task.execute(url); mTask.add(task); } else { ImageView imageView = (ImageView) mListView.findViewWithTag(url); imageView.setImageBitmap(bitmap); } } }

以上就是圖片工具類比較重點的部分 ,下面介紹adapter

常常的分割線-------------------------------------------------------------------------------------------------------------

public class NewsAdapter extends BaseAdapter implements AbsListView.OnScrollListener{

    private List<NewsBeans> mList;
    private LayoutInflater mInflater;
    private ImageLoader mImageLoader;
    private int mStart;
    private int mEnd;
    //創建靜態數組保存圖片的url地址
    public static String[] URLS;
    private boolean mFirstIn;

    public NewsAdapter(Context context, List<NewsBeans> data,ListView listView) {
        mList = data;
        mInflater = LayoutInflater.from(context);
        mImageLoader = new ImageLoader(listView);
        URLS = new String[data.size()];
        for(int i=0;i<data.size();i++){
            URLS[i] = data.get(i).iv_title;
        }
        listView.setOnScrollListener(this);
        mFirstIn = true;
    }

    @Override
    public int getCount() {
        return mList.size();
    }

    @Override
    public Object getItem(int position) {
        return mList.get(position);
    }

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

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder viewHolder;
        if (convertView == null) {
            viewHolder = new ViewHolder();
            convertView = mInflater.inflate(R.layout.list_item, null);
            viewHolder.iv_title = (ImageView) convertView.findViewById(R.id.iv_icon);
            viewHolder.tv_title = (TextView) convertView.findViewById(R.id.tv_title);
            viewHolder.tv_content = (TextView) convertView.findViewById(R.id.tv_content);
            convertView.setTag(viewHolder);
        } else {
            viewHolder = (ViewHolder) convertView.getTag();
        }
        //設置默認顯示的圖片
        viewHolder.iv_title.setImageResource(R.mipmap.ic_launcher);
        //避免緩存影響使同一位置圖片加載多次混亂
        String url = mList.get(position).iv_title;
        viewHolder.iv_title.setTag(url);
     //   new ImageLoader().showImageByThread(viewHolder.iv_title, url);
        mImageLoader.showImageByAsyncTask(viewHolder.iv_title, url);
        viewHolder.tv_content.setText(mList.get(position).tv_content);
        viewHolder.tv_title.setText(mList.get(position).tv_title);
        return convertView;
    }

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        if(scrollState==SCROLL_STATE_IDLE){
            //加載可見項
            mImageLoader.loadImages(mStart,mEnd);
        }else{
            //停止加載
            mImageLoader.cancelAllTasks();
        }
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        mStart = firstVisibleItem;
        mEnd = firstVisibleItem + visibleItemCount;
        if (mFirstIn && visibleItemCount>0){
            mImageLoader.loadImages(mStart,mEnd);
        }
    }

    class ViewHolder {
        private ImageView iv_title;
        private TextView tv_title;
        private TextView tv_content;
    }
}

這里我們只需要注意3點

1.設置圖片唯一標識Tag,避免圖片錯位顯示

viewHolder.iv_title.setTag(url);

2.滾動過程中不加載圖片,只有滾動停止后加載,下面重點分析2個方法

@Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        if(scrollState==SCROLL_STATE_IDLE){
            //加載可見項
            mImageLoader.loadImages(mStart,mEnd);
        }else{
            //停止加載
            mImageLoader.cancelAllTasks();
        }
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        mStart = firstVisibleItem;
        mEnd = firstVisibleItem + visibleItemCount;
        if (mFirstIn && visibleItemCount>0){
            mImageLoader.loadImages(mStart,mEnd);
        }
    }
onScrollStateChanged()該方法,在listview第一次出現的時候,並不會執行,注意"並不會執行".


oncroll()該方法在listview創建的時候就會執行,所以我們定義一個標志mFirstIn,在構造方法中初始為true,表示我們是第一次啟動listview
對 mFirstIn && visibleItemCount>0 判斷的解釋:
"當前列表時第一次顯示,並且listview的item已經展示出來",然后mFirstIn =false ,保證此段代碼只有listview第一次顯示的時候才會執行,之后滾動過程中不再執行

這里為什么要判斷visibleItemCount>0 呢?
其實 oncroll會被多次回調的, 但是初次調用 
visibleItemCount 是 等於0的,也就是說此時item還未被加載
所以我們要判斷 >0 跳過==0的情況,因為==0 item未被加載,當然 也就不會顯示網絡圖片了
分割線--------------------------------------------------------------------------------------------------------------------------------------

最后是 MainActivity代碼,比較簡單不再分析
public class MainActivity extends AppCompatActivity {

    private static String URL = "http://www.imooc.com/api/teacher?type=4&num=30";
    private ListView mListView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mListView = (ListView) findViewById(R.id.list_main);
        new LoadImageAsync().execute(URL);
    }

    //異步加載所有的網絡數據
    class LoadImageAsync extends AsyncTask<String, Void, List<NewsBeans>> {

        @Override
        protected List<NewsBeans> doInBackground(String... params) {
            return getJsonData(params[0]);
        }

        @Override
        protected void onPostExecute(List<NewsBeans> newsBeans) {
            super.onPostExecute(newsBeans);
            NewsAdapter adapter = new NewsAdapter(MainActivity.this, newsBeans,mListView);
            mListView.setAdapter(adapter);
        }
    }

    //得到JSON數據
    private List<NewsBeans> getJsonData(String url) {
        List<NewsBeans> data = new ArrayList<>();
        try {
            //讀取流得到json數據
            String jsonList = readStream(new URL(url).openStream());
            JSONObject jsonObject;
            NewsBeans newsBeans;
            try {
                //解析JSON數據
                jsonObject = new JSONObject(jsonList);
                JSONArray jsonArray = jsonObject.getJSONArray("data");
                for (int i = 0; i <= jsonArray.length(); i++) {
                    jsonObject = jsonArray.getJSONObject(i);
                    newsBeans = new NewsBeans();
                    newsBeans.iv_title = jsonObject.getString("picSmall");
                    newsBeans.tv_title = jsonObject.getString("name");
                    newsBeans.tv_content = jsonObject.getString("description");
                    data.add(newsBeans);
                }
            } catch (JSONException e) {
                e.printStackTrace();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return data;
    }

    //讀取輸入流
    private String readStream(InputStream is) {
        InputStreamReader isr;
        String result = "";
        try {
            String line;
            //讀取輸入流
            isr = new InputStreamReader(is, "utf-8");
            //輸入流轉換成字節流
            BufferedReader br = new BufferedReader(isr);
            //逐行讀取
            while ((line = br.readLine()) != null) {
                result += line;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return result;
    }
}

以上代碼只是做了內存緩存,如果想做二級緩存,可以用
DiskLruCache 硬盤緩存.最后來張圖吧

 
        



免責聲明!

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



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