標籤:
ListView的工作原理
首先來瞭解一下ListView的工作原理(可參見http://mobile.51cto.com/abased-410889.htm),
ListView 針對每個item,要求 adapter “返回一個視圖” (getView),也就是說ListView在開始繪製的時候,系統首先調用getCount()函數,根據他的傳回值得到ListView的長度,然後根據這個長度,調用getView()一行一行的繪製ListView的每一項。如果你的getCount()傳回值是0的話,列表一行都不會顯示,如果返回1,就只顯示一行。返回幾則顯示幾行。如果我們有幾千幾萬甚至更多的item要顯示怎麼辦?為每個Item建立一個新的View?不可能!!!實際上Android早已經緩衝了這些視圖,大家可以看下下面這個來理解下,這個圖是解釋ListView工作原理的最經典的圖了大家可以收藏下,不懂的時候拿來看看,加深理解,其實Android中有個叫做Recycler的構件,順帶列舉下與Recycler相關的已經由Google做過N多最佳化過的東東比如:AbsListView.RecyclerListener、ViewDebug.RecyclerTraceType等等,要瞭解的朋友自己查下,不難理解,是ListView載入資料的工作原理(原理圖看不清楚的點擊後看大圖):
1、如果你有幾千幾萬甚至更多的選項(item)時,其中只有可見的項目存在記憶體(記憶體記憶體哦,說的最佳化就是說在記憶體中的最佳化!!!)中,其他的在Recycler中
2、ListView先請求一個type1視圖(getView)然後請求其他可見的項目。convertView在getView中是空(null)的
3、當item1滾出螢幕,並且一個新的項目從螢幕低端上來時,ListView再請求一個type1視圖。convertView此時不是空值了,它的值是item1。你只需設定新的資料然後返回convertView,不必重新建立一個視圖
一、複用convertView,減少findViewById的次數
1、最佳化一:複用convertView
Android系統本身為我們考慮了ListView的最佳化問題,在複寫的Adapter的類中,比較重要的兩個方法是getCount()和getView()。介面上有多少個條顯示,就會調用多少次的getView()方法;因此如果在每次調用的時候,如果不進行最佳化,每次都會使用View.inflate(….)的方法,都要將xml檔案解析,並顯示到介面上,這是非常消耗資源的:因為有新的內容產生就會有舊的內容銷毀,所以,可以複用舊的內容。
最佳化:
在getView()方法中,系統就為我們提供了一個複用view的曆史緩衝對象convertView,當顯示第一屏的時候,每一個item都會新建立一個view對象,這些view都是可以被複用的;如果每次顯示一個view都要建立一個,是非常耗費記憶體的;所以為了節約記憶體,可以在convertView不為null的時候,對其進行複用
2、最佳化二:緩衝item條目的引用——ViewHolder
findViewById()這個方法是比較耗效能的操作,因為這個方法要找到指定的布局檔案,進行不斷地解析每個節點:從最頂端的節點進行一層一層的解析查詢,找到後在一層一層的返回,如果在左邊沒找到,就會接著解析右邊,並進行相應的查詢,直到找到位置()。因此可以對findViewById進行最佳化處理,需要注意的是:
》》》》特點:xml檔案被解析的時候,只要被建立出來了,其孩子的id就不會改變了。根據這個特點,可以將孩子id存入到指定的集合中,每次就可以直接取出集合中對應的元素就可以了。
最佳化:
在建立view對象的時候,減少布局檔案轉化成view對象的次數;即在建立view對象的時候,把所有孩子全部找到,並把孩子的引用給存起來
①定義儲存控制項引用的類ViewHolder
這裡的ViewHolder類需要不需要定義成static,根據實際情況而定,如果item不是很多的話,可以使用,這樣在初始化的時候,只載入一次,可以稍微得到一些最佳化
不過,如果item過多的話,建議不要使用。因為static是Java中的一個關鍵字,當用它來修飾成員變數時,那麼該變數就屬於該類,而不是該類的執行個體。所以用static修飾的變數,它的生命週期是很長的,如果用它來引用一些資源耗費過多的執行個體(比如Context的情況最多),這時就要盡量避免使用了。
class ViewHolder{
//定義item中相應的控制項
}
②建立自訂的類:ViewHolder holder = null;
③將子view添加到holder中:
在建立新的listView的時候,建立新的ViewHolder,把所有孩子全部找到,並把孩子的引用給存起來
通過view.setTag(holder)將引用設定到view中
通過holder,將孩子view設定到此holder中,從而減少以後查詢的次數
④在複用listView中的條目的時候,通過view.getTag(),將view對象轉化為holder,即轉化成相應的引用,方便在下次使用的時候存入集合。
通過view.getTag(holder)擷取引用(需要強轉)
二、ListView中資料的分批及分頁載入:
需求:ListView有一萬條資料,如何顯示;如果將十萬條資料載入到記憶體,很消耗記憶體
解決辦法:
最佳化查詢的資料:先擷取幾條資料顯示到介面上
進行分批處理---à最佳化了使用者體驗
進行分頁處理---à最佳化了記憶體空間
說明:
一般資料都是從資料庫中擷取的,實現分批(分頁)載入資料,就需要在對應的DAO中有相應的分批(分頁)擷取資料的方法,如findPartDatas ()
1、準備資料:
在dao中添加分批載入資料的方法:findPartDatas ()
在適配資料的時候,先載入第一批的資料,需要載入第二批的時候,設定監聽檢測何時載入第二批
2、設定ListView的滾動監聽器:setOnScrollListener(new OnScrollListener{….})
①、在監聽器中有兩個方法:滾動狀態發生變化的方法(onScrollStateChanged)和listView被滾動時調用的方法(onScroll)
②、在滾動狀態發生改變的方法中,有三種狀態:
手指按下移動的狀態: SCROLL_STATE_TOUCH_SCROLL: // 觸摸滑動
慣性滾動(滑翔(flgin)狀態): SCROLL_STATE_FLING: // 滑翔
靜止狀態: SCROLL_STATE_IDLE: // 靜止
3、對不同的狀態進行處理:
分批載入資料,只關心靜止狀態:關心最後一個可見的條目,如果最後一個可見條目就是資料配接器(集合)裡的最後一個,此時可載入更多的資料。在每次載入的時候,計算出滾動的數量,當滾動的數量大於等於總數量的時候,可以提示使用者無更多資料了。
三、複雜ListView的處理:(待進一步總結)
說明:
listView的介面顯示是通過getCount和getView這兩個方法來控制的
getCount:返回有多少個條目
getView:返回每個位置條目顯示的內容
提供思路:
對於含有多個類型的item的最佳化處理:由於ListView只有一個Adapter的入口,可以定義一個總的Adapter入口,存放各種類型的Adapter
以安全衛士中的進程管理的功能為例。效果
1、定義兩個(或多個)集合
每個集合中存入的是對應不同類型的內容(這裡為:使用者程式(userAppinfos)和系統程式的集合(systemAppinfos))
2、在初始化資料(填充資料)中初始化兩個集合
如,此處是在fillData方法中初始化
3、在資料配接器中,複寫對應的方法
getCount():計算所有需要顯示的條目個數,這裡包括listView和textView
getView():對顯示在不同位置的條目進行if處理
4、資料類型的判斷
需要注意的是,在複用view的時候,需要對convertView進行類型判斷,是因為這裡含有各種不同類型的view,在view滾動顯示的時候,對於不同類型的view不能複用,所有需要判斷
四、ListView中圖片的最佳化:詳看OOM異常中圖片的最佳化
1、處理圖片的方式:
如果自訂Item中有涉及到圖片等等的,一定要狠狠的處理圖片,圖片占的記憶體是ListView項中最噁心的,處理圖片的方法大致有以下幾種:
①、不要直接拿路徑就去迴圈decodeFile();使用Option儲存圖片大小、不要載入圖片到記憶體去
②、拿到的圖片一定要經過邊界壓縮
③、在ListView中取圖片時也不要直接拿個路徑去取圖片,而是以WeakReference(使用WeakReference代替強引用。
比如可以使用WeakReference mContextRef)、SoftReference、WeakHashMap等的來儲存圖片資訊,是圖片資訊不是圖片哦!
④、在getView中做圖片轉換時,產生的中間變數一定及時釋放
2、非同步載入圖片基本思想:
1)、 先從記憶體緩衝中擷取圖片顯示(記憶體緩衝)
2)、擷取不到的話從SD卡裡擷取(SD卡緩衝)
3)、都擷取不到的話從網路下載圖片並儲存到SD卡同時加入記憶體並顯示(視情況看是否要顯示)
原理:
最佳化一:先從記憶體中載入,沒有則開啟線程從SD卡或網路中擷取,這裡注意從SD卡擷取圖片是放在子線程裡執行的,否則快速滑屏的話會不夠流暢。
最佳化二:與此同時,在adapter裡有個busy變數,表示listview是否處於滑動狀態,如果是滑動狀態則僅從記憶體中擷取圖片,沒有的話無需再開啟線程去外存或網路擷取圖片。
最佳化三:ImageLoader裡的線程使用了線程池,從而避免了過多線程頻繁建立和銷毀,有的童鞋每次總是new一個線程去執行這是非常不可取的,好一點的用的AsyncTask類,其實內部也是用到了線程池。在從網路擷取圖片時,先是將其儲存到sd卡,然後再載入到記憶體,這麼做的好處是在載入到記憶體時可以做個壓縮處理,以減少圖片所佔記憶體。
Tips:這裡可能出現圖片亂跳(錯位)的問題:
圖片錯位問題的本質源於我們的listview使用了緩衝convertView,假設一種情境,一個listview一屏顯示九個item,那麼在拉出第十個item的時候,事實上該item是重複使用了第一個item,也就是說在第一個item從網路中下載圖片並最終要顯示的時候,其實該item已經不在當前顯示地區內了,此時顯示的後果將可能在第十個item上輸出映像,這就導致了圖片錯位的問題。所以解決之道在於可見則顯示,不可見則不顯示。在ImageLoader裡有個imageViews的map對象,就是用於儲存當前顯示地區映像對應的url集,在顯示前判斷處理一下即可。
3、記憶體緩衝機制:
首先限制記憶體配置圖片緩衝的堆記憶體大小,每次有圖片往緩衝裡加時判斷是否超過限制大小,超過的話就從中取出最少使用的圖片並將其移除。
當然這裡如果不採用這種方式,換做軟引用也是可行的,二者目的皆是最大程度的利用已存在於記憶體中的圖片緩衝,避免重複製造垃圾增加GC負擔;OOM溢出往往皆因記憶體瞬時大量增加而記憶體回收不及時造成的。只不過二者區別在於LinkedHashMap裡的圖片緩衝在沒有移除出去之前是不會被GC回收的,而SoftReference裡的圖片緩衝在沒有其他引用儲存時隨時都會被GC回收。所以在使用LinkedHashMap這種LRU演算法緩衝更有利於圖片的有效命中,當然二者配合使用的話效果更佳,即從LinkedHashMap裡移除出的緩衝放到SoftReference裡,這就是記憶體的二級緩衝。
本例採用的是LRU演算法,先看看MemoryCache的實現
public class MemoryCache {
private static final String TAG = "MemoryCache";
// 放入緩衝時是個同步操作
// LinkedHashMap構造方法的最後一個參數true代表這個map裡的元素將按照最近使用次數由少到多排列,即LRU
// 這樣的好處是如果要將緩衝中的元素替換,則先遍曆出最近最少使用的元素來替換以提高效率
private Map<String, Bitmap> cache = Collections
.synchronizedMap(new LinkedHashMap<String, Bitmap>(10, 1.5f, true));
// 緩衝中圖片所佔用的位元組,初始0,將通過此變數嚴格控制緩衝所佔用的堆記憶體
private long size = 0;// current allocated size
// 緩衝只能佔用的最大堆記憶體
private long limit = 1000000;// max memory in bytes
public MemoryCache() {
// use 25% of available heap size
setLimit(Runtime.getRuntime().maxMemory() / 10);
}
public void setLimit(long new_limit) {
limit = new_limit;
Log.i(TAG, "MemoryCache will use up to " + limit / 1024. / 1024. + "MB");
}
public Bitmap get(String id) {
try {
if (!cache.containsKey(id))
return null;
return cache.get(id);
} catch (NullPointerException ex) {
return null;
}
}
public void put(String id, Bitmap bitmap) {
try {
if (cache.containsKey(id))
size -= getSizeInBytes(cache.get(id));
cache.put(id, bitmap);
size += getSizeInBytes(bitmap);
checkSize();
} catch (Throwable th) {
th.printStackTrace();
}
}
/**
* 嚴格控制堆記憶體,如果超過將首先替換最近最少使用的那個圖片緩衝
*
*/
private void checkSize() {
Log.i(TAG, "cache size=" + size + " length=" + cache.size());
if (size > limit) {
// 先遍曆最近最少使用的元素
Iterator<Entry<String, Bitmap>> iter = cache.entrySet().iterator();
while (iter.hasNext()) {
Entry<String, Bitmap> entry = iter.next();
size -= getSizeInBytes(entry.getValue());
iter.remove();
if (size <= limit)
break;
}
Log.i(TAG, "Clean cache. New size " + cache.size());
}
}
public void clear() {
cache.clear();
}
/**
* 圖片佔用的記憶體
* <a href="\"http://www.eoeandroid.com/home.php?mod=space&uid=2768922\"" target="\"_blank\"">@Param</a> bitmap
* @return
*/
long getSizeInBytes(Bitmap bitmap) {
if (bitmap == null)
return 0;
return bitmap.getRowBytes() * bitmap.getHeight();
}
}
五、ListView的其他最佳化:
1、盡量避免在BaseAdapter中使用static 來定義全域靜態變數:
static是Java中的一個關鍵字,當用它來修飾成員變數時,那麼該變數就屬於該類,而不是該類的執行個體。所以用static修飾的變數,它的生命週期是很長的,如果用它來引用一些資源耗費過多的執行個體(比如Context的情況最多),這時就要盡量避免使用了。
2、盡量使用getApplicationContext:
如果為了滿足需求下必須使用Context的話:Context盡量使用Application Context,因為Application的Context的生命週期比較長,引用它不會出現記憶體泄露的問題
3、盡量避免在ListView適配器中使用線程:
因為線程產生記憶體泄露的主要原因在於線程生命週期的不可控制。之前使用的自訂ListView中適配資料時使用AsyncTask自行開啟線程的,這個比用Thread更危險,因為Thread只有在run函數不結束時才出現這種記憶體泄露問題,然而AsyncTask內部的實現機制是運用了線程執行池(ThreadPoolExcutor),這個類產生的Thread對象的生命週期是不確定的,是應用程式無法控制的,因此如果AsyncTask作為Activity的內部類,就更容易出現記憶體泄露的問題。解決辦法如下:
①、將線程的內部類,改為靜態內部類。
②、線上程內部採用弱引用儲存Context引用
Android--Listview最佳化