Android非同步載入全解析之引入一級緩衝,android非同步
Android非同步載入全解析之引入緩衝
為啥要緩衝通過對映像的縮放,我們做到了對大圖的非同步載入最佳化,但是現在的App不僅是高清大圖,更是高清多圖,動不動就是圖文混排,以圖代文,如果這些圖片都載入到記憶體中,必定會OOM。因此,在使用者瀏覽完映像後,應當立即將這些廢棄的映像回收,但是,這又帶來了另一個問題,也就是當使用者在瀏覽完一次圖片後,如果還要返回去再進行重新瀏覽,那麼這些回收掉的映像又要重新進行載入,保不準就要那些無聊到蛋疼的人在那一邊看你回收GC,一邊看你重新載入。這兩件事情,肯定是互相矛盾的,也是影響效能的一個很重要的原因。
記憶體緩衝針對這樣一個非常需要找到一個彼此平衡點的問題,Google提供了一套記憶體緩衝技術。記憶體緩衝技術對那些大量佔用應用程式寶貴記憶體的圖片提供了快速存取的方法。其中最核心的類是LruCache 。這個類非常適合用來緩衝圖片,它的主要演算法原理是把最近使用的對象用強引用儲存在 LinkedHashMap 中,並且把最近最少使用的對象在緩衝值達到預設定值之前從記憶體中移除。LruCache 是在support-v4中才引入的,在引入LruCache 之前,Google建議的是使用軟引用或弱引用 (SoftReference or WeakReference)來進行記憶體緩衝。但是從Android 2.3開始,GC演算法修改,軟引用與弱引用同樣會優先被GC回收,所以這種方法也就沒有太高的使用價值了,現在網上很多還在繼續使用SoftReference 和WeakReference的文章,大多都是過時的文章,建議大家跟上黨的步伐,與時俱進。
LruCache使用記憶體緩衝LruCache所使用的記憶體緩衝大小是由開發人員決定的,開發人員需要根據映像的使用率、解析度、訪問頻率、裝置效能等很多因素進行考慮。這個平衡點經常需要很多經驗和測試來決定。使用LruCache非常簡單:
private LruCache<String, Bitmap> mMemoryCaches;// 擷取應用記憶體int maxMemory = (int) Runtime.getRuntime().maxMemory();// 分配cacheint cacheSize = maxMemory / 10;mMemoryCaches = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap value) { return value.getByteCount(); } };// 從LruCache擷取中擷取緩衝對象public Bitmap getBitmapFromMemoryCaches(String url) { return mMemoryCaches.get(url);}// 增加緩衝對象到LruCachepublic void addBitmapToMemoryCaches(String url,Bitmap bitmap) { if (getBitmapFromMemoryCaches(url) == null) { mMemoryCaches.put(url, bitmap); }}
首先,我們需要聲明LruCache,接著,通過LruCache的構造方法建立緩衝對象,並為其分配cacheSize,這個cacheSize通常我們需要通過Runtime來擷取,擷取當前系統分給App的可用記憶體,並將這些記憶體的一部分用做LruCache緩衝。LruCache中必須重寫sizeOf方法,通過這個方法,LruCache可以擷取每個緩衝對象的大小,子類必須重寫,因為預設的LruCache擷取的是緩衝的個數。。。尼瑪。最後,我們提供兩個方法getBitmapFromMemoryCaches和addBitmapToMemoryCaches分別用來擷取和增加記憶體緩衝到LruCache。等等,我們好像還沒寫釋放記憶體的方法,對,不用你寫了,Lru演算法可以保證cacheSize不會OOM,一旦超過這個大小,GC就會回收時間最長的對象,釋放空間。
為非同步處理加入一級緩衝OK,在瞭解了關於緩衝的基礎資訊後,我們回到現在這個例子,想想怎麼利用緩衝來進行非同步處理的最佳化。首先,ListView、GridView這些嬌生慣養的玩意兒,碰不得摔不得,更不能在它滾的開心的時候,你還在後面拚命玩載入。所以,第一個重點,滾的時候就讓它開心的滾,滾完了再開始載入。
滾完再載入要實現這一點,我們可以通過給Adapter增加AbsListView.OnScrollListener介面來實現。當然,還有一點需要注意,第一次初始化的時候,一定要手動來載入圖片,不然系統判斷你沒滾,只能調用onScroll方法,不會調用onScrollStateChanged方法。而且我們也需要在onScroll方法中來不斷擷取可見的Item。特別要注意的是visibleItemCount,只要大於0的時候,才認為是開始顯示圖片了。
@Overridepublic void onScrollStateChanged(AbsListView view, int scrollState) { if (scrollState == SCROLL_STATE_IDLE) { mImageLoader.loadImages(mStart, mEnd); } else { mImageLoader.cancelAllTasks(); }}@Overridepublic void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { mStart = firstVisibleItem; mEnd = firstVisibleItem + visibleItemCount; if (mFirstFlag && visibleItemCount > 0) { mImageLoader.loadImages(mStart, mEnd); mFirstFlag = false; }}
載入顯示的項目載入資料的時候,擷取第一個能顯示的Item和最後一個可見的Item,只載入這一部分。所以我們建立一個方法——loadImages(int start, int end)。這個方法用來載入從start到end之間的Item資料。載入的時候,先從記憶體緩衝中去取,如果有,那說明最近已經載入過了,那直接載入就好了,如果沒有取到,那就開啟synctask去下載。
public void loadImages(int start, int end) { for (int i = start; i < end; i++) { String url = Images.IMAGE_URLS[i]; Bitmap bitmap = getBitmapFromMemoryCaches(url); if (bitmap == null) { ASyncDownloadImage task = new ASyncDownloadImage(url); mTasks.add(task); task.execute(url); } else { ImageView imageView = (ImageView) mListView.findViewWithTag(url); imageView.setImageBitmap(bitmap); } }}
這裡我們在設定圖片的時候,直接通過findViewWithTag,通過url來找到相應的Imageview,這裡與之前不同是因為我們這裡是按照start到end來進行載入,直接從ListView對象中擷取對應的Imageview比較簡單。
下載與Asynctask下載依然是使用老方法:
private static Bitmap getBitmapFromUrl(String urlString) { Bitmap bitmap; InputStream is = null; try { URL url = new URL(urlString); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); is = new BufferedInputStream(conn.getInputStream()); bitmap = BitmapFactory.decodeStream(is); conn.disconnect(); return bitmap; } catch (Exception e) { e.printStackTrace(); } finally { try { if (is != null) is.close(); } catch (IOException e) { } } return null;}
Asynctask也與之前基本類似:
class ASyncDownloadImage extends AsyncTask<String, Void, Bitmap> { private String url; public ASyncDownloadImage(String url) { this.url = url; } @Override protected Bitmap doInBackground(String... params) { url = params[0]; Bitmap bitmap = getBitmapFromUrl(url); if (bitmap != null) { addBitmapToMemoryCaches(url, bitmap); } return bitmap; } @Override protected void onPostExecute(Bitmap bitmap) { super.onPostExecute(bitmap); ImageView imageView = (ImageView) mListView.findViewWithTag(url); if (imageView != null && bitmap != null) { imageView.setImageBitmap(bitmap); } mTasks.remove(this); }}
唯一不同的是,我們在下載好映像之後,會將映像載入到Lrucache。
組裝OK,萬事具備,準備刷代碼。在刷之前,我們先來重新整理下思路,首先,在Adapter中,一載入ListView,就開始下載顯示範圍內的Item的映像,這時候緩衝中當然沒有,所以都去下載了,下完了就顯示在Item中,並緩衝起來,如果還沒下完,你就迫不及待的滾起來了,那麼立即取消所有task,讓ListView歡快的滾,滾完之後,繼續載入。OK,該講的都講了,下面我們開始刷代碼了,一切盡在不言中,只有代碼最懂你。
package com.imooc.listviewacyncloader;import android.graphics.Bitmap;import android.graphics.BitmapFactory;import android.os.AsyncTask;import android.util.LruCache;import android.widget.ImageView;import android.widget.ListView;import java.io.BufferedInputStream;import java.io.IOException;import java.io.InputStream;import java.net.HttpURLConnection;import java.net.URL;import java.util.HashSet;import java.util.Set;public class ImageLoaderWithCaches { private Set<ASyncDownloadImage> mTasks; private LruCache<String, Bitmap> mMemoryCaches; private ListView mListView; public ImageLoaderWithCaches(ListView listview) { this.mListView = listview; mTasks = new HashSet<>(); int maxMemory = (int) Runtime.getRuntime().maxMemory(); int cacheSize = maxMemory / 10; mMemoryCaches = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap value) { return value.getByteCount(); } }; } public void showImage(String url, ImageView imageView) { Bitmap bitmap = getBitmapFromMemoryCaches(url); if (bitmap == null) { imageView.setImageResource(R.drawable.ic_launcher); } else { imageView.setImageBitmap(bitmap); } } public Bitmap getBitmapFromMemoryCaches(String url) { return mMemoryCaches.get(url); } public void addBitmapToMemoryCaches(String url,Bitmap bitmap) { if (getBitmapFromMemoryCaches(url) == null) { mMemoryCaches.put(url, bitmap); } } public void loadImages(int start, int end) { for (int i = start; i < end; i++) { String url = Images.IMAGE_URLS[i]; Bitmap bitmap = getBitmapFromMemoryCaches(url); if (bitmap == null) { ASyncDownloadImage task = new ASyncDownloadImage(url); mTasks.add(task); task.execute(url); } else { ImageView imageView = (ImageView) mListView.findViewWithTag(url); imageView.setImageBitmap(bitmap); } } } private static Bitmap getBitmapFromUrl(String urlString) { Bitmap bitmap; InputStream is = null; try { URL url = new URL(urlString); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); is = new BufferedInputStream(conn.getInputStream()); bitmap = BitmapFactory.decodeStream(is); conn.disconnect(); return bitmap; } catch (Exception e) { e.printStackTrace(); } finally { try { if (is != null) is.close(); } catch (IOException e) { } } return null; } public void cancelAllTasks() { if (mTasks != null) { for (ASyncDownloadImage task : mTasks) { task.cancel(false); } } } class ASyncDownloadImage extends AsyncTask<String, Void, Bitmap> { private String url; public ASyncDownloadImage(String url) { this.url = url; } @Override protected Bitmap doInBackground(String... params) { url = params[0]; Bitmap bitmap = getBitmapFromUrl(url); if (bitmap != null) { addBitmapToMemoryCaches(url, bitmap); } return bitmap; } @Override protected void onPostExecute(Bitmap bitmap) { super.onPostExecute(bitmap); ImageView imageView = (ImageView) mListView.findViewWithTag(url); if (imageView != null && bitmap != null) { imageView.setImageBitmap(bitmap); } mTasks.remove(this); } }}
下面是Adapter的代碼:
package com.imooc.listviewacyncloader;import android.content.Context;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.widget.AbsListView;import android.widget.BaseAdapter;import android.widget.ImageView;import android.widget.ListView;import java.util.List;public class MyAdapterUseCaches extends BaseAdapter implements AbsListView.OnScrollListener { private LayoutInflater mInflater; private List<String> mData; private ImageLoaderWithCaches mImageLoader; private int mStart = 0, mEnd = 0; private boolean mFirstFlag; public MyAdapterUseCaches(Context context, List<String> data, ListView listView) { this.mData = data; mInflater = LayoutInflater.from(context); mImageLoader = new ImageLoaderWithCaches(listView); mImageLoader.loadImages(mStart, mEnd); mFirstFlag = true; listView.setOnScrollListener(this); } @Override public int getCount() { return mData.size(); } @Override public Object getItem(int position) { return mData.get(position); } @Override public long getItemId(int position) { return position; } @Override public View getView(int position, View convertView, ViewGroup parent) { String url = mData.get(position); ViewHolder viewHolder = null; if (convertView == null) { viewHolder = new ViewHolder(); convertView = mInflater.inflate(R.layout.listview_item, null); viewHolder.imageView = (ImageView) convertView.findViewById(R.id.iv_lv_item); convertView.setTag(viewHolder); } else { viewHolder = (ViewHolder) convertView.getTag(); } viewHolder.imageView.setTag(url); viewHolder.imageView.setImageResource(R.drawable.ic_launcher); mImageLoader.showImage(url, viewHolder.imageView); 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 (mFirstFlag && visibleItemCount > 0) { mImageLoader.loadImages(mStart, mEnd); mFirstFlag = false; } } public class ViewHolder { public ImageView imageView; }}
是不是非常簡單,現在引入緩衝了,下載過的圖片會暫時儲存在記憶體中,媽媽再也不用擔心你OOM啦。我們下拉試試,下載完的圖片再次出現也可以馬上載入了,除非滑動太多導致GC。
可以就看見,我們的這次利用緩衝進行載入有這樣幾個特點:1、初始化的時候載入2、滑動的時候才載入3、載入的內容暫存緩衝中4、只載入顯示的地區
後面我們將繼續最佳化緩衝,未完待續~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~我的Github
我的視訊 慕課網