一.概述
本文來自"慕課網" 的學習,只是對代碼做一下分析
圖片異步加載有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 硬盤緩存.最后來張圖吧