Android:跟著實戰專案學快取策略之LruCache詳談

iamxiarui_發表於2018-07-02

##寫在前面

前幾天更新了一篇非同步任務AsyncTask的文章,用了兩個小小的例子,總體來說比較簡單。今天我就通過一個比較完整的新聞小專案來繼續說下AsyncTask在專案中的使用方法。因為不再是一個簡單的例子所以考慮的情況要比之前多得多,也複雜許多。

同時由於專案中用到了最常用的ListView,所以ListView的優化也在本文的重點範圍中。而優化的兩個主要方面就是使用非同步任務和控制非同步任務執行的頻率,也就是監聽ListView滾動狀態,只有當其靜止的時候才非同步載入網路圖片資料。這樣大大提高了ListView的效能及使用體驗。

此外在專案中用到了通過URL載入網路圖片,因為載入圖片需要聯網,如果每次都載入的話不僅耗費大量流量,而且使用者體驗極差,所以我們必須知道Android中的基本快取策略。比如常見的LruCache與DiskLruCache。

所以本文總體來說就是通過一個小小的專案來學習三方面的知識:

  1. AsyncTask如何在專案中運用自如;
  2. 如何高效優化ListView
  3. LruCache的概念與基本用法

儘管具體的實現比較複雜,但是清楚原理和基本流程後,大體還是比較清晰簡單的。下面是本文的目錄:

  • 專案介紹
  • LruCache用法詳解
  • 進一步優化ListView
  • 結語
  • 專案原始碼

##專案介紹

專案本身很簡單,就是一個通過解析JSON得到相關資料顯示在ListView上,在這裡我們先不採用快取策略,直接載入圖片,所以前期效果圖如下:

執行緒載入

可以看到,圖中第一次開啟是需要載入網路圖片資料的,每次滑動的時候也需要載入網路資料,而且是邊滑動邊載入。很明顯的看出,滑動過程是很卡頓的,所以這樣的體驗是不友好的,所以我們必須優化它。

那麼在優化之前,我先簡單說一下這個專案。佈局就不提了,每個人都會。這是專案結構:

專案結構

  • MainActivity:主頁面
  • NewsAdapter:新聞列表介面卡
  • NewsBean:封裝的新聞模型
  • GetJsonUtil:得到Json資料工具類
  • JsonToStringUtil:Json轉字串工具類
  • ThreadUtil:普通子執行緒載入URL圖片工具類
  • LruCacheUtil:記憶體快取載入URL圖片工具類

這裡面NewsBean、GetJsonUtil、JsonToStringUtil三個檔案我不詳細說,因為對大家來說很簡單。專案也原始碼附在文末,可以自行檢視。

我們主要來看一下非同步任務處理和快取處理這一塊。

首先來看主Activity,這裡面自定義了一個 GetJsonTask 非同步任務,並在非同步任務的 doInBackGround 方法中進行Json資料的解析。最後將解析結果在 onPostExecute 方法中展示在ListView中。當然不要忘記開啟非同步任務。

/**
 * 新聞案例:非同步任務與非同步載入圖片的使用
 */
public class MainActivity extends AppCompatActivity {

    private ListView mainListView;
    private Context mainContext = MainActivity.this;
    private String url = "http://www.imooc.com/api/teacher?type=4&num=30";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initView();

        //開啟非同步任務
        GetJsonTask getJsonTask = new GetJsonTask();
        getJsonTask.execute(url);
    }

    /**
     * 初始化View
     */
    private void initView() {
        mainListView = (ListView) findViewById(R.id.lv_main);
    }

    /**
     * 自定義非同步任務解析JSON資料
     */
    class GetJsonTask extends AsyncTask<String, Void, List<NewsBean>> {

        @Override
        protected List<NewsBean> doInBackground(String... params) {
            return GetJsonUtil.getJson(params[0]);
        }

        @Override
        protected void onPostExecute(List<NewsBean> newsBeen) {
            super.onPostExecute(newsBeen);
            NewsAdapter newsAdapter = new NewsAdapter(mainContext, newsBeen,mainListView);
            mainListView.setAdapter(newsAdapter);
        }
    }
}
複製程式碼

由於後面才講快取策略,所以我把通過子執行緒且沒有快取策略的載入URL圖片的方法剝離成ThreadUtil,先來看一看效果。類中無非就是開啟子執行緒通過URL獲取圖片的Bitmap物件,然後通過Handler設定給ListView。比較簡單,直接上程式碼,不多解釋。

/**
 * 普通執行緒載入URL圖片類
 */
public class ThreadUtil {

    private ImageView mImageView;
    private String mIconUrl;

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            //只有當前的ImageView所對應的URL的圖片是一致的,才會設定圖片
            if (mImageView.getTag().equals(mIconUrl)) {
                mImageView.setImageBitmap((Bitmap) msg.obj);
            }
        }
    };

    /**
     * 通過子執行緒的方式展示圖片
     *
     * @param iv  圖片的控制元件
     * @param url 圖片的URL
     */
    public void showImageByThread(ImageView iv, final String url) {
        mImageView = iv;
        mIconUrl = url;
        //非同步解析圖片
        new Thread(new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap = getBitmapFromURL(url);
                //傳送到主執行緒
                Message msg = Message.obtain();
                msg.obj = bitmap;
                mHandler.sendMessage(msg);
            }
        }).start();
    }

    /**
     * 將一個URL轉換成bitmap物件
     *
     * @param urlStr 圖片的URL
     * @return
     */
    public Bitmap getBitmapFromURL(String urlStr) {
        Bitmap bitmap;
        InputStream is = null;

        try {
            URL url = new URL(urlStr);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            is = new BufferedInputStream(connection.getInputStream());
            bitmap = BitmapFactory.decodeStream(is);
            connection.disconnect();
            return bitmap;
        } catch (java.io.IOException e) {
            e.printStackTrace();
        } finally {
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}
複製程式碼

如果你看了上面的程式碼,你可能對下面這段判斷程式碼有疑問:

//只有當前的ImageView所對應的URL的圖片是一致的,才會設定圖片
if (mImageView.getTag().equals(mIconUrl)) {
	mImageView.setImageBitmap((Bitmap) msg.obj);
}
複製程式碼

沒關係,因為我還沒說Adapter,現在來說。先看程式碼,就是簡單的繼承BaseAdapter,還有個ViewHolder,其他都是常規的東西。

/**
 * 新聞列表介面卡
 */
public class NewsAdapter extends BaseAdapter {

    private Context context;
    private List<NewsBean> list;

    public NewsAdapter(Context context, List<NewsBean> list, ListView lv) {
        this.context = context;
        this.list = list;
    }
複製程式碼

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

    @Override
    public Object getItem(int position) {
        return list.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) {
            convertView = View.inflate(context, R.layout.item_news, null);
        }
        // 得到一個ViewHolder
        viewHolder = ViewHolder.getViewHolder(convertView);
        //先載入預設圖片 防止有的沒有圖
        viewHolder.iconImage.setImageResource(R.mipmap.ic_launcher);

        String iconUrl = list.get(position).newsIconUrl;
        //當前位置的ImageView與圖片的URL繫結
        viewHolder.iconImage.setTag(iconUrl);
        //再載入聯網圖

        //第一種方式 通過子執行緒設定
        new ThreadUtil().showImageByThread(viewHolder.iconImage, iconUrl);

        viewHolder.titleText.setText(list.get(position).newsTitle);
        viewHolder.contentText.setText(list.get(position).newsContent);

        return convertView;
    }

    static class ViewHolder {
        ImageView iconImage;
        TextView titleText;
        TextView contentText;

        // 建構函式中就初始化View
        public ViewHolder(View convertView) {
            iconImage = (ImageView) convertView.findViewById(R.id.iv_icon);
            titleText = (TextView) convertView.findViewById(R.id.tv_title);
            contentText = (TextView) convertView.findViewById(R.id.tv_content);
        }

        // 得到一個ViewHolder
        public static ViewHolder getViewHolder(View convertView) {
            ViewHolder viewHolder = (ViewHolder) convertView.getTag();
            if (viewHolder == null) {
                viewHolder = new ViewHolder(convertView);
                convertView.setTag(viewHolder);
            }
            return viewHolder;
        }
    }
}
複製程式碼

值得注意的是,在得到圖片的Url的時候,給當前item的ImageView與圖片的URL進行繫結。

String iconUrl = list.get(position).newsIconUrl;
//當前位置的ImageView與圖片的URL繫結
viewHolder.iconImage.setTag(iconUrl);
複製程式碼

為什麼呢?因為我們知道,ListView中的Item是屬於複用機制的,所以在滑動過程中,滑動很快的話,有可能出現一個item中ImageView剛剛載入好一張圖後突然載入另一張圖,因為前面那張圖還是上一個item所對應的圖,由於滑動太快,剛載入就被複用了。也就是可能出現圖片閃爍的現象,體驗十分差,所以就將當然位置與URL繫結,在Hanlder中設定的時候判斷標記,防止出現這種現象。這也算ListView的一種優化吧。

好了到此,就是一個很小的新聞專案,沒有任何難度。下面我們就對這個專案不斷進行優化。主要就是通過快取策略,所以先來看快取策略的介紹。

##LruCache用法詳解

###初識Cache

對於快取我想大家都應該瞭解,通俗的說就是把一些經常使用但需要聯網獲取檔案,通過一種策略持久的儲存在記憶體或者儲存裝置中,當下一次需要用到這些檔案的時候,不需要聯網,直接從記憶體或儲存裝置中獲取就可以了。這種策略就是快取策略。

快取策略一般來說包含快取的新增、獲取、刪除。至於刪除,其實是指快取的大小已經超過定義的快取的大小後移除已有的一部分快取。比如LRU演算法,最近最少使用演算法,會移除最近最少使用的那一部分快取,以此來新增新的快取。

關於快取的好處,開篇已經說過了,無非就是兩點:

  • 節省流量
  • 提高使用者體驗

為什麼流量又沒了

而接下來要說的LruCache和DiskLruCache就是基於LRU演算法的快取策略。LruCache是用於實現記憶體快取的,而DiskLruCache實現儲存裝置快取,也就是直接快取到本地。其中LruCache在Android中已經封裝成了類,直接用就可以了。而DiskLruCache需要下載對應的檔案才能用,本專案中也有整合好的。如果需要可以直接拷貝來用。

###LruCache介紹

關於什麼是LruCache,在開發藝術探索上任老師說的不能再清楚了:

LruCache是一個泛型類,它內部採用一個LinkedHashMap以強引用的方式儲存外界的快取物件,其提供了get與set方法來完成快取的新增與獲取操作。當快取滿時,LruCache會移除較早使用的快取物件,然後再新增新的快取物件。

這裡面提到了一個概念——強引用。也就是Java中的四種引用,不久前電話面試中面試官也問到了這個,可惜當時答的太爛。由於不作為重點,這裡僅僅給出定義,點到為止:

  • 強引用:直接的物件引用,gc絕不會回收它
  • 軟引用:當物件只具有軟引用時,系統記憶體不足時才會被gc回收
  • 弱引用:當物件只具有弱引用時,物件隨時會被gc
  • 虛引用:當物件只具有虛引用時,物件隨時會被gc,但是必須與引用佇列一起使用

那麼如何使用LruCache呢?首先需要直接定義一個LruCache,注意內部實現是Map,所以要設定key和value的型別:

//LRU快取
private LruCache<String, Bitmap> mCache;
複製程式碼

然後就是初始化LruCache,來看下面這段程式碼:

//返回Java虛擬機器將嘗試使用的最大記憶體
int maxMemory = (int) Runtime.getRuntime().maxMemory();
//指定快取大小
int cacheSize = maxMemory / 4;
mCache = new LruCache<String, Bitmap>(cacheSize) {
	@Override
    protected int sizeOf(String key, Bitmap value) {
    	//Bitmap的實際大小 注意單位與maxMemory一致
        return value.getByteCount();

		//也可以這樣返回 結果是一樣的
		//return value.getRowBytes()*value.getHeight();
	}
};
複製程式碼

可以看到上面這段程式碼規定了LruCache的快取大小,它是通過返回Java虛擬機器將嘗試使用的最大記憶體來確定的。這就初始化了一個LruCache,現在就是簡單的新增獲取了。因為是Map機制,所以與Map的新增獲取是一樣的道理。

//新增到快取
mCache.get(key);
//從快取中獲取
mCache.put(key,value);
複製程式碼

這也就是LruCache的基本使用了,當然還有其他方法,這裡暫且不考慮。而且上面的新增與獲取在專案中可以封裝成相關的方法。接下來,我們就對上一個專案進行優化,來看看如何將聯網獲取的圖片快取到記憶體。

###LruCache實戰運用

為了與之前的ThreadUtil對比,這裡講LruCache方法剝離成LruCacheUtil。先看效果圖,第一次開啟需要載入圖片,全部載入完成後,再滑動的時候,已經不需要載入圖片了。

LruCache載入

來看LruCacheUtil,為了方便講解,我將各個部分分開來說明。完整程式碼在下一個部分 進一步優化ListView 中。

首先我們定義出相關需要的變數,然後在建構函式中,初始化LruCache:

//LRU快取
private LruCache<String, Bitmap> mCache;
複製程式碼
private ListView mListView;

public LruCacheUtil(ListView listView) {
    this.mListView = listView;
    //返回Java虛擬機器將嘗試使用的最大記憶體
    int maxMemory = (int) Runtime.getRuntime().maxMemory();
    //指定快取大小
    int cacheSize = maxMemory / 4;
    mCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap value) {
            //Bitmap的實際大小 注意單位與maxMemory一致
            return value.getByteCount();
        }
    };
}
複製程式碼

注意這個地方,LurCache的key是字串型別的圖片Url地址,value當然就是圖片的Bitmap物件,所以我們很輕易的封裝出快取的新增刪除方法:

/**
 * 將Bitmap存入快取
 *
 * @param url    Bitmap物件的key
 * @param bitmap 物件的key
 */
public void addBitmapToCache(String url, Bitmap bitmap) {
    //如果快取中沒有
    if (getBitmapFromCache(url) == null) {
        //儲存到快取中
        mCache.put(url, bitmap);
    }
}

/**
 * 從快取中獲取Bitmap物件
 *
 * @param url Bitmap物件的key
 * @return 快取中Bitmap物件
 */
public Bitmap getBitmapFromCache(String url) {
    return mCache.get(url);
}
複製程式碼

其次呢,由於前幾天說了非同步任務AsyncTask,所以我們這裡就改成非同步任務。所以我們先定義一個AsyncTask類,比較簡單,不多解釋。

/**
 * 非同步任務類
 */
private class NewsAsyncTask extends AsyncTask<String, Void, Bitmap> {
    private String url;

    public NewsAsyncTask(String url) {
        this.url = url;
    }

    @Override
    protected Bitmap doInBackground(String... params) {
        Bitmap bitmap = getBitmapFromURL(params[0]);
        //儲存到快取中
        if (bitmap != null) {
            addBitmapToCache(params[0], bitmap);
        }
        return bitmap;
    }

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        super.onPostExecute(bitmap);
        //只有當前的ImageView所對應的UR的圖片是一致的,才會設定圖片
        ImageView imageView = (ImageView) mListView.findViewWithTag(url);
        if (imageView != null && bitmap != null) {
            imageView.setImageBitmap(bitmap);
        }
    }
}
複製程式碼

剩下的當然就是展示了,先從快取中找,沒找到才聯網獲取,聯網獲取的方法getBitmapFromURL在之前已經介紹了,不再多說了。程式碼如下:

/**
 * 通過非同步任務的方式載入資料
 *
 * @param iv  圖片的控制元件
 * @param url 圖片的URL
 */
public void showImageByAsyncTask(ImageView iv, final String url) {
    //從快取中取出圖片
    Bitmap bitmap = getBitmapFromCache(url);
    //如果快取中沒有,先設為預設圖片
    if (bitmap == null) {
        iv.setImageResource(R.mipmap.ic_launcher);
    } else {
        //如果快取中有 直接設定
        iv.setImageBitmap(bitmap);
    }
}
複製程式碼

到此,這個類就算完成了,當然不要忘了,注意我們在自定義的AsyncTask中傳遞了一個型別為ListView的引數,所以要改一下自定義Adapter的引數,並在改一下圖片的載入方式:

private LruCacheUtil lruCacheUtil;

public NewsAdapter(Context context, List<NewsBean> list, ListView lv) {
	...
	//初始化
    lruCacheUtil = new LruCacheUtil(lv);
	...
}

...
複製程式碼
@Override
public View getView(int position, View convertView, ViewGroup parent) {

	...
	//第二種方式 通過非同步任務方式設定 且利用LruCache儲存到記憶體快取中
	lruCacheUtil.showImageByAsyncTask(viewHolder.iconImage, iconUrl);

...
}
複製程式碼

好了,到這我們就算實現了LruCache,再總結一下思路就是在載入圖片的時候,先從快取中找圖,如果沒有才從網路中獲取。而且在獲取後存入到快取中,以方便下一次載入。

##進一步優化ListView

不知道大家注意到沒有,上面一個動態圖中有一個細節。就是我在滑動ListView的時候,並沒有載入圖片,等到列表停止滑動的時候才載入圖片。對,這也是一個優化ListView的方式之一。

那它是怎麼實現的呢?其實就是實現一個列表滾動監聽,也就是 OnScrollListener ,它的核心方法是

  • public void onScrollStateChanged(AbsListView view, int scrollState)
  • public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)

其中引數的意思等會程式碼中有詳細解釋,先來說一下思路。

首先在滑動過程中需要記錄滑動的起止位置,根據起止位置判斷中間有多少個item,然後再載入圖片。那麼如何載入呢?有了起止位置,要得到起止位置之間的元素,這跟陣列不是很相似嘛,所以乾脆我們用資料把圖片的URL記錄起來,然後直接從陣列中取就好了。

思路大體就是這樣,但是實現起來還是比較複雜的,主要有很多細節問題。

先來改造一下自定義的Adapter:

/**
 * 新聞列表介面卡
 */
public class NewsAdapter extends BaseAdapter implements AbsListView.OnScrollListener {

    private Context context;
    private List<NewsBean> list;

    private LruCacheUtil lruCacheUtil;

    private int mStart, mEnd;//滑動的起始位置
    public static String[] urls; //用來儲存當前獲取到的所有圖片的Url地址

    //是否是第一次進入
    private boolean mFirstIn;

    public NewsAdapter(Context context, List<NewsBean> list, ListView lv) {
        this.context = context;
        this.list = list;

        lruCacheUtil = new LruCacheUtil(lv);

        //存入url地址
        urls = new String[list.size()];
        for (int i = 0; i < list.size(); i++) {
            urls[i] = list.get(i).newsIconUrl;
        }

        mFirstIn = true;

        //註冊監聽事件
        lv.setOnScrollListener(this);
    }
複製程式碼

    ...

    /**
     * 滑動狀態改變的時候才會去呼叫此方法
     *
     * @param view        滾動的View
     * @param scrollState 滾動的狀態
     */
    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        if (scrollState == SCROLL_STATE_IDLE) {
            //載入可見項
            lruCacheUtil.loadImages(mStart, mEnd);
        } else {
            //停止載入任務
            lruCacheUtil.cancelAllTask();
        }
    }

    /**
     * 滑動過程中 一直會呼叫此方法
     *
     * @param view             滾動的View
     * @param firstVisibleItem 第一個可見的item
     * @param visibleItemCount 可見的item的長度
     * @param totalItemCount   總共item的個數
     */
    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        mStart = firstVisibleItem;
        mEnd = firstVisibleItem + visibleItemCount;
        //如果是第一次進入 且可見item大於0 預載入
        if (mFirstIn && visibleItemCount > 0) {
            try {
                lruCacheUtil.loadImages(mStart, mEnd);
            } catch (IOException e) {
                e.printStackTrace();
            }
            mFirstIn = false;
        }
    }
}
複製程式碼

也許你有個疑問?因為LruCacheUtil中自定義的AsyncTask類是用來載入圖片的。但是你在滑動過程中,停止了這些Task,那麼在滑動停止的時候如何得到這些沒執行的Task呢?沒錯,解決辦法就是把這些滑動過程中產生的Task放在集合中。當滑動的時候,停止這些Task的執行;滑動停止的時候,再執行這些Task。也就是如下這樣定義:

private Set<NewsAsyncTask> mTaskSet

好了,現在回頭看滑動停止、靜止載入的主要兩個核心方法:

/**
 * 載入從start到end的所有的Image
 *
 * @param start
 * @param end
 */
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);
            mTaskSet.add(task);
        } else {
            //如果快取中有 直接設定
            ImageView imageView = (ImageView) mListView.findViewWithTag(url);
            imageView.setImageBitmap(bitmap);
        }
    }
}
複製程式碼
/**
 * 停止所有當前正在執行的任務
 */
public void cancelAllTask() {
    if (mTaskSet != null) {
        for (NewsAsyncTask task : mTaskSet) {
            task.cancel(false);
        }
    }
}
複製程式碼

說到這裡,現在來看整個LruCacheUtil類,是不是清晰很多呢?

/**
 * 非同步載入圖片的工具類
 */
public class LruCacheUtil {

    //LRU快取
    private LruCache<String, Bitmap> mCache;

    private ListView mListView;
    private Set<NewsAsyncTask> mTaskSet;
複製程式碼

    public LruCacheUtil(ListView listView) {
        this.mListView = listView;
        mTaskSet = new HashSet<>();
        //返回Java虛擬機器將嘗試使用的最大記憶體
        int maxMemory = (int) Runtime.getRuntime().maxMemory();
        //指定快取大小
        int cacheSize = maxMemory / 4;
        mCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                //Bitmap的實際大小 注意單位與maxMemory一致
                return value.getByteCount();

                //也可以這樣返回 結果是一樣的
                //return value.getRowBytes()*value.getHeight();
            }
        };
    }

    /**
     * 通過非同步任務的方式載入資料
     *
     * @param iv  圖片的控制元件
     * @param url 圖片的URL
     */
    public void showImageByAsyncTask(ImageView iv, final String url) {
        //從快取中取出圖片
        Bitmap bitmap = getBitmapFromCache(url);
        //如果快取中沒有,則需要從網路中下載
        if (bitmap == null) {
            bitmap = getBitmapFromURL(url);
            iv.setImageBitmap(bitmap);
        } else {
            //如果快取中有 直接設定
            iv.setImageBitmap(bitmap);
        }
    }

    /**
     * 將一個URL轉換成bitmap物件
     *
     * @param urlStr 圖片的URL
     * @return
     */
    public Bitmap getBitmapFromURL(String urlStr) {
        Bitmap bitmap;
        InputStream is = null;

        try {
            URL url = new URL(urlStr);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            is = new BufferedInputStream(connection.getInputStream());
            bitmap = BitmapFactory.decodeStream(is);
            connection.disconnect();
            return bitmap;
        } catch (java.io.IOException e) {
            e.printStackTrace();
        } finally {
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * 載入從start到end的所有的Image
     *
     * @param start
     * @param end
     */
    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);
                mTaskSet.add(task);
            } else {
                //如果快取中有 直接設定
                ImageView imageView = (ImageView) mListView.findViewWithTag(url);
                imageView.setImageBitmap(bitmap);
            }
        }
    }
複製程式碼

    /**
     * 停止所有當前正在執行的任務
     */
    public void cancelAllTask() {
        if (mTaskSet != null) {
            for (NewsAsyncTask task : mTaskSet) {
                task.cancel(false);
            }
        }
    }

    /*--------------------------------LruCaChe的實現-----------------------------------------*/

    /**
     * 將Bitmap存入快取
     *
     * @param url    Bitmap物件的key
     * @param bitmap 物件的key
     */
    public void addBitmapToCache(String url, Bitmap bitmap) {
        //如果快取中沒有
        if (getBitmapFromCache(url) == null) {
            //儲存到快取中
            mCache.put(url, bitmap);
        }
    }

    /**
     * 從快取中獲取Bitmap物件
     *
     * @param url Bitmap物件的key
     * @return 快取中Bitmap物件
     */
    public Bitmap getBitmapFromCache(String url) {
        return mCache.get(url);
    }

    /*--------------------------------LruCaChe的實現-----------------------------------------*/

    /**
     * 非同步任務類
     */
    private class NewsAsyncTask extends AsyncTask<String, Void, Bitmap> {
        private String url;

        public NewsAsyncTask(String url) {
            this.url = url;
        }

        @Override
        protected Bitmap doInBackground(String... params) {
            Bitmap bitmap = getBitmapFromURL(params[0]);
            //儲存到快取中
            if (bitmap != null) {
                addBitmapToCache(params[0], bitmap);
            }
            return bitmap;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
            //只有當前的ImageView所對應的UR的圖片是一致的,才會設定圖片
            ImageView imageView = (ImageView) mListView.findViewWithTag(url);
            if (imageView != null && bitmap != null) {
                imageView.setImageBitmap(bitmap);
            }
            //移除所有Task
            mTaskSet.remove(this);
        }
    }
}
複製程式碼

至此,有了快取與ListView滑動監聽,我們可以說專案已經有了良好的效能優化和體驗了。

##結語

其實一開始準備將DiskLruCache也加到本文中,後來發現本文的篇幅已經過長了,大家可能看的比較枯燥。加上DiskLruCache相較於LruCache來說複雜一點,所以就將快取策略分為兩篇了。

由於篇幅過長,專案也比較複雜,有些地方並沒有將所有細節說清楚。如果大家有不明白的地方,或者文中有錯誤的,可以與我交流。由於我還在學習階段,水平有限,希望大家多多支援。

##專案原始碼

IamXiaRui - MoocNewsDemo


個人部落格:www.iamxiarui.com 原文連結:http://www.iamxiarui.com/?p=710

相關文章