淺談Android中的非同步載入之ListView中圖片的快取及優化三
隔了很久沒寫部落格,現在必須快速脈動回來。今天我還是接著上一個多執行緒中的非同步載入系列中的最後一個使用非同步載入實現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){
@Override
protected 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格式的資料。
注意:大家可能看到這裡面中文全部都亂碼了,這是因為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();
}
@Override
public 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() {
@Override
public void asyncListener(List<Course> mCourseList) {
}
@Override
public 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;//自定義監聽器介面物件引用
@Override
protected void onPreExecute() {
mCourseList=new ArrayList<Course>();
super.onPreExecute();
}
@Override
protected String doInBackground(String... params) {
String data=MikyouHttpUrlConnectionUtils.getData(params[0], null, null);//網路請求JSON資料
return data;
}
@Override
protected 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;
@Override
protected Bitmap doInBackground(String... params) {
return getBitmapFromURL(params[0]);
}
@Override
protected 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來實現的。
Demo下載連結