標籤:
隔了很久沒寫部落格,現在必須快速脈動回來。今天我還是接著上一個多線程中的非同步載入系列中的最後一個使用非同步載入實現ListView中的圖片緩衝及其最佳化。具體來說這次是一個綜合Demo.但是個人覺得裡面還算有點價值的就是裡面的圖片的緩衝的實現。因為老實說它確實能在實際的項目中得到很好的應用。主要學習來源於慕課網中的非同步載入學習,來自徐宜生大神的靈感。本次也就是對大神所講知識的一個總結及一些個人的感受吧。
這次是一個綜合的Demo,主要裡面涉及到的知識主要有:網路編程、非同步載入、JSON解析、圖片緩衝、通用ListAdapter的使用。最後實現一個載入網路資料的圖文混排listView的效果。當然這裡面涉及到的知識比較多,但是本次的重點就是圖片緩衝和非同步載入,當然類似網路編程中的HttpURLConnection,JSON解析、打造通用適配器等知識將會在後續部落格中給出,這裡也就是使用我以前自己封裝好的,因為為了簡化開發。
這次的重點是非同步載入和圖片緩衝,至於非同步載入因為在前兩個部落格中已經寫得很清楚了,這次主要是用一下非同步載入,看看非同步載入在實際項目是怎麼使用的。主要是使用非同步載入進行耗時網路請求,並且自訂一個監聽器用於當獲得資料後,立即將獲得的資料回調出去。然後重點介紹的就是圖片緩衝。
說到圖片緩衝下面將通過以下幾個方面認識一片緩衝:
1、為什麼要使用圖片緩衝?
很簡單“消耗流量特別大” , 這個相信很多人都感同深受吧,因為我們可能都寫過一個類似網路請求資料的ListView的圖文混排的Demo,但是如果我們直接通過網路請求圖片,然後拿到的圖片顯示在ListView上,當滑動ListView,下次將已經滑過Item,會發現圖片重新請求一個網路資料,重新載入一次,也就是滑到哪就請求一次網路,不管是否重複。可想而知這流量消耗太大,估計這樣滑一晚上,第二天早上醒來,發現自己的房子都成中國移動的了。還有一個弊端就是每請求一次網路都是一次非同步和耗時過程,所以你會發現在滑動ListView會有卡頓情況出現。
2、圖片緩衝原理是什嗎?
圖片緩衝是基於LRU演算法來實現的,LRU即Least Recently Used,中文意思是最近最少未使用演算法,學過作業系統原理就知道這是作業系統中頁面置換演算法之一。
說到這,不妨來看看LruCache源碼是怎麼介紹的。
/** * A cache that holds strong references to a limited number of values. Each time * a value is accessed, it is moved to the head of a queue. When a value is * added to a full cache, the value at the end of that queue is evicted and may * become eligible for garbage collection. * * <p>If your cached values hold resources that need to be explicitly released, * override {@link #entryRemoved}. * * <p>If a cache miss should be computed on demand for the corresponding keys, * override {@link #create}. This simplifies the calling code, allowing it to * assume a value will always be returned, even when there's a cache miss. * * <p>By default, the cache size is measured in the number of entries. Override * {@link #sizeOf} to size the cache in different units. For example, this cache * is limited to 4MiB of bitmaps: * <pre> {@code * int cacheSize = 4 * 1024 * 1024; // 4MiB * LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) { * protected int sizeOf(String key, Bitmap value) { * return value.getByteCount(); * } * }}</pre> * * <p>This class is thread-safe. Perform multiple cache operations atomically by * synchronizing on the cache: <pre> {@code * synchronized (cache) { * if (cache.get(key) == null) { * cache.put(key, value); * } * }}</pre> * * <p>This class does not allow null to be used as a key or value. A return * value of null from {@link #get}, {@link #put} or {@link #remove} is * unambiguous: the key was not in the cache. * * <p>This class appeared in Android 3.1 (Honeycomb MR1); it's available as part * of <a href="http://developer.android.com/sdk/compatibility-library.html">Android's * Support Package</a> for earlier releases. */
LruCache主要原理:緩衝是限制了緩衝的數目的,也就是說緩衝的容量是有限的,可以把緩衝的邏輯記憶體結構想象一個隊列,當緩衝中一個緩衝值被訪問後,它將會被置換到隊列的隊頭,當一個緩衝值需要加到隊尾時,但是此時隊列已滿了,也即此時緩衝空間已滿,那麼就需要將處於隊列隊尾一個緩衝值出隊列,也即是釋放隊列隊尾一部分緩衝空間,因為基於LRU演算法處於隊尾的,肯定最近最少未使用。也就是因為緩衝空間是有限的,才會基於這樣演算法,及時併合適地將一些資料空間釋放。
LruCache類是安全執行緒的,它支援多個快取作業自動通過非同步來實現,並且這個類不允許用空值去作為key或者value,並且注意LruCache的key不是儲存在緩衝中的。
LurCache類出現在Android3.1版本。
3、LruCache如何建立:
LruCache實際在操作上很類似於Map的操作,初學者實際上就可以把它當做一個Map,因為它是key-value成對的,並且有put(),get()方法非常類似Map
個人覺得使用圖片緩衝使用率很高,為了下次方便使用,索性直接將它封裝成一個工具類。
package com.mikyou.utils;import android.graphics.Bitmap;import android.util.LruCache;public class LruCacheUtils {//建立Cache緩衝,第一個泛型表示緩衝的標識key,第二個泛型表示需要緩衝的對象private LruCache<String, Bitmap> mCaches; public LruCacheUtils() { int maxMemory=(int) Runtime.getRuntime().maxMemory();//擷取最大的應用運行時的最大記憶體//通過獲得最大的運行時候的記憶體,合理分配緩衝的記憶體空間大小int cacheSize=maxMemory/4;//取最大運行記憶體的1/4;mCaches=new LruCache<String, Bitmap>(cacheSize){@Overrideprotected int sizeOf(String key, Bitmap value) {//載入正確的記憶體大小return value.getByteCount();//在每次存入緩衝的時候調用}}; }//將圖片儲存在LruCache中public void addBitmapToCache(String url,Bitmap bitmap){if (getBitmapFromCache(url)==null) {//判斷當前的Url對應的Bitmap是否在Lru緩衝中,如果不在緩衝中,就把當前url對應的Bitmap對象加入Lru緩衝mCaches.put(url, bitmap);}}//將圖片從LruCache中讀取出來public Bitmap getBitmapFromCache(String url){Bitmap bitmap=mCaches.get(url);//實際上LruCache就是一個Map,底層是通過HashMap來實現的return bitmap;}}
通過以上知識的講解,相信已經對LruCache有了一定的瞭解了,那麼接下來我們就開始我們的Demo吧。
1、首先、我們既然是載入網路資料,所以得解決網路資料來源問題,主要來自於慕課網的一個課程列表的API的地址,返回的資料是JSON格式的資料。
地址是:http://www.imooc.com/api/teacher?type=4&num=60。可以先用瀏覽器來測試一下資料,測試結果如下:
注意:大家可能看到這裡面中文全部都亂碼了,這是因為Unicode編碼,我會在代碼中使用一個工具類將這些轉化成中文。
2、資料解決後,那麼接著就是布局,布局很簡單,主布局就是一個ListView,listItem布局也很簡單。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" > <ListView android:id="@+id/listview" android:layout_width="match_parent" android:layout_height="wrap_content" android:divider="#22000000" android:dividerHeight="0.2dp" > </ListView></RelativeLayout>
<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="5dp" > <ImageView android:id="@+id/c_img" android:layout_width="140dp" android:layout_height="90dp" android:src="@drawable/left_img" /><TextView android:id="@+id/c_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Android百度地圖之導航" android:textSize="16sp" android:layout_marginLeft="5dp" android:layout_toRightOf="@id/c_img" android:layout_marginTop="10dp" /><TextView android:id="@+id/c_learner" android:layout_width="wrap_content" android:layout_height="wrap_content" android:drawableLeft="@drawable/learner" android:text="7897" android:textColor="#a9b7b7" android:layout_alignBottom="@id/c_img" android:layout_alignLeft="@id/c_name" android:drawablePadding="5dp" android:layout_marginBottom="5dp" /></RelativeLayout>
3、自己封裝的HttpURLConnection網路請求架構,返回的是整個JSON資料
package com.mikyou.utils;import java.io.BufferedReader;import java.io.InputStream;import java.io.InputStreamReader;import java.net.HttpURLConnection;import java.net.MalformedURLException;import java.net.URL;import java.util.List;import org.json.JSONObject;import android.R.interpolator;public class MikyouHttpUrlConnectionUtils {private static StringBuffer buffer;public static String getData(String urlString,String apiKeyValue,List<String> stringList){buffer=new StringBuffer();String jsonOrXmlString=null;if (stringList!=null) {for (int i = 0; i <stringList.size(); i++) {urlString+=stringList.get(i);}}try {System.out.println("URL---->"+urlString);URL url=new URL(urlString);HttpURLConnection conn=(HttpURLConnection) url.openConnection();conn.setRequestMethod("GET");if (apiKeyValue!=null) {conn.setRequestProperty("apikey", apiKeyValue);}conn.setConnectTimeout(8000);conn.setReadTimeout(8000);conn.connect();if (conn.getResponseCode()==200) {InputStream is=conn.getInputStream();BufferedReader reader=new BufferedReader(new InputStreamReader(is, "UTF-8"));while ((jsonOrXmlString=reader.readLine())!=null) {buffer.append(jsonOrXmlString+"\n");}reader.close();is.close();}} catch (Exception e) {e.printStackTrace();}String string=UnicodeUtils.decodeUnicode(buffer.toString());//使用UnicodeUtils工具類將Unicode編碼轉換成UTF-8顯示的中文return string;}}
4、封裝課程對象的javaBean類對象即每個Item為一個對象
package com.mikyou.bean;import java.io.Serializable;import android.R.id;public class Course implements Serializable{private String cName;private String cImgURl;private String cDescriptor;private String cLearner;public String getcName() {return cName;}public void setcName(String cName) {this.cName = cName;}public String getcImgURl() {return cImgURl;}public void setcImgURl(String cImgURl) {this.cImgURl = cImgURl;}public String getcDescriptor() {return cDescriptor;}public void setcDescriptor(String cDescriptor) {this.cDescriptor = cDescriptor;}public String getcLearner() {return cLearner;}public void setcLearner(String cLearner) {this.cLearner = cLearner;}}
5、通用適配器實現的子類
package com.mikyou.adapter;import java.util.List;import com.lidroid.xutils.BitmapUtils;import com.mikyou.async.ImageLoader;import com.mikyou.bean.Course;import com.mikyou.cache.R;import com.mikyou.tools.ViewHolder;import android.content.Context;import android.widget.ImageView;public class MyListAdapter extends CommonAdapter<Course>{private ImageLoader loader;public MyListAdapter(Context context, List<Course> listBeans, int layoutId) {super(context, listBeans, layoutId); loader=new ImageLoader();}@Overridepublic void convert(ViewHolder holder, Course course) {holder.setText(R.id.c_name, course.getcName()).setText(R.id.c_learner, course.getcLearner()); ImageView iv= holder.getView(R.id.c_img); iv.setTag(course.getcImgURl());//首先、需要將相應的url和相應的iv綁定在一起,為了防止圖片和請求URL不對應 loader.showImageByAsyncTask(iv, course.getcImgURl());}}
7、核心實現代碼:
package com.mikyou.async;import java.io.IOException;import java.net.HttpURLConnection;import java.net.MalformedURLException;import java.net.URL;import java.util.List;import com.mikyou.bean.Course;import com.mikyou.utils.LruCacheUtils;import android.graphics.Bitmap;import android.graphics.BitmapFactory;import android.os.AsyncTask;import android.os.Handler;import android.os.Message;import android.util.Log;import android.util.LruCache;import android.widget.ImageView;public class ImageLoader {private ImageView iv;private String url;private LruCacheUtils mCacheUtils;public ImageLoader() {mCacheUtils=new LruCacheUtils();}/** * @author mikyou * 實現的主要思路: * 首先、載入圖片的時候,先去LruCache緩衝中根據傳入的url作為key去取相應的Bitmap對象 * ,如果緩衝中存在相應的key對應的value,那麼就直接取出key對應緩衝中的Bitmap對象 * 並設定給ImageView,如果緩衝中沒有,那麼就需要通過非同步載入請求網路中的資料和圖片資訊, * 然後通過監聽器中的asyncImgListener回調方法將網路請求得到的Bitmap對象,首先得通過iv.getTag() * 比較url如果對應就將該Bitmap對象設定給iv,並且還需要將這個Bitmap對象和相應的url以key-value形式 * 通過put方法,加入LruCache緩衝中。 * */public void showImageByAsyncTask(final ImageView iv,final String url){//首先,從緩衝中讀取圖片,如果有就直接使用緩衝,如果沒有就直接載入網狀圖片Bitmap bitmap=mCacheUtils.getBitmapFromCache(url);Log.d("url", url);if (bitmap==null) {//表示緩衝中沒有,就去訪問網路下載圖片,並記住將下載到的圖片放入緩衝中ImageAsyncTask imageAsyncTask=new ImageAsyncTask();imageAsyncTask.execute(url);imageAsyncTask.setOnImgAsyncTaskListener(new OnAsyncListener() {@Overridepublic void asyncListener(List<Course> mCourseList) {}@Overridepublic void asyncImgListener(Bitmap bitmap) {//圖片請求網路資料的回調方法if (iv.getTag().equals(url)) {//判斷url和iv是否對應iv.setImageBitmap(bitmap);Log.d("addLru", "網路載入並加入緩衝--->"+url);mCacheUtils.addBitmapToCache(url, bitmap);//由於是網路請求得到的資料,所以緩衝中肯定沒有,所以還需要將該Bitmap對象加入到緩衝中}}});}else{//否則就直接從緩衝中擷取iv.setImageBitmap(mCacheUtils.getBitmapFromCache(url));//直接讀取緩衝中的Bitmap對象Log.d("getLru", "url讀出緩衝--->"+url);}}//HttpURLConnection網路請求方式來得到網狀圖片輸入資料流,並且將輸入資料流轉換成一個Bitmap對象public Bitmap getBitmapFromURL(String url){Bitmap bitmap = null;try {URL mURL=new URL(url);HttpURLConnection conn=(HttpURLConnection) mURL.openConnection();bitmap = BitmapFactory.decodeStream(conn.getInputStream());conn.disconnect();} catch (Exception e) {e.printStackTrace();} return bitmap;}}
8、非同步載入類實現,這裡主要有兩個:一個是請求整個網路的JSON資料,另一個就是請求載入網狀圖片,並且自訂一個監聽器介面。
package com.mikyou.async;import java.util.ArrayList;import java.util.List;import org.json.JSONArray;import org.json.JSONException;import org.json.JSONObject;import com.mikyou.bean.Course;import com.mikyou.utils.MikyouHttpUrlConnectionUtils;import android.os.AsyncTask;import android.util.Log;public class MikyouAsyncTask extends AsyncTask<String, Void, String>{private List<Course> mCourseList;private OnAsyncListener listener;//自訂監聽器介面對象引用@Overrideprotected void onPreExecute() {mCourseList=new ArrayList<Course>();super.onPreExecute();}@Overrideprotected String doInBackground(String... params) {String data=MikyouHttpUrlConnectionUtils.getData(params[0], null, null);//網路請求JSON資料return data;}@Overrideprotected void onPostExecute(String result) {//解析JSON資料Log.d("info", result);try {JSONObject object=new JSONObject(result);JSONArray array=object.getJSONArray("data");for (int i = 0; i < array.length(); i++) {Course mCourse=new Course();JSONObject object2=array.getJSONObject(i);mCourse.setcName(object2.getString("name"));mCourse.setcImgURl(object2.getString("picSmall"));mCourse.setcLearner(object2.getInt("learner")+"");mCourse.setcDescriptor(object2.getString("description"));mCourseList.add(mCourse);}if (listener!=null) {//判斷是否註冊了監聽器listener.asyncListener(mCourseList);//通過監聽器中的回調方法將非同步載入得到的資料後經過解析、封裝的對象集合回調出去}} catch (JSONException e) {e.printStackTrace();}super.onPostExecute(result);}public void setOnAsyncTaskListener(OnAsyncListener listener){//公布一個註冊監聽器的方法this.listener=listener;}}
ImgAsyncTask非同步載入類:
package com.mikyou.async;import java.net.HttpURLConnection;import java.net.URL;import android.graphics.Bitmap;import android.graphics.BitmapFactory;import android.media.Image;import android.os.AsyncTask;import android.text.GetChars;import android.util.LruCache;import android.widget.ImageView;public class ImageAsyncTask extends AsyncTask<String, Void, Bitmap>{private OnAsyncListener listener;@Overrideprotected Bitmap doInBackground(String... params) {return getBitmapFromURL(params[0]);}@Overrideprotected void onPostExecute(Bitmap result) {if (listener!=null) {listener.asyncImgListener(result);}super.onPostExecute(result);}public Bitmap getBitmapFromURL(String url){Bitmap bitmap = null;try {URL mURL=new URL(url);HttpURLConnection conn=(HttpURLConnection) mURL.openConnection();bitmap = BitmapFactory.decodeStream(conn.getInputStream());conn.disconnect();} catch (Exception e) {e.printStackTrace();} return bitmap;}public void setOnImgAsyncTaskListener(OnAsyncListener listener){this.listener=listener;}}
自訂監聽器:
監聽器介面:
package com.mikyou.async;import java.util.List;import com.mikyou.bean.Course;import android.graphics.Bitmap;public interface OnAsyncListener {public void asyncListener(List<Course> mCourseList);public void asyncImgListener(Bitmap bitmap);}
運行結果:
沒有加入圖片緩衝的運行結果會發現無論什麼時候滑動都會請求網路,會發現圖片載入有個延遲時間:
加入圖片緩衝後的運行結果會發現,非常流暢,並且直接讀緩衝的圖片時沒有圖片載入的延遲
最後,圖片緩衝LruCache實際上運用很流行,並且運用在很多流行網路架構中,我們都知道很流行的Xutils架構,其中就有一個BitmapUtils,它裡面實現緩衝原理也就是基於LruCache來實現的。
淺談Android中的非同步載入之ListView中圖片的緩衝及最佳化三