ListView的工做原理php
首先來了解一下ListView的工做原理(可參見http://mobile.51cto.com/abased-410889.htm),如圖:android
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加載數據的工做原理(原理圖看不清楚的點擊後看大圖):算法
一、若是你有幾千幾萬甚至更多的選項(item)時,其中只有可見的項目存在內存(內存內存哦,說的優化就是說在內存中的優化!!!)中,其餘的在Recycler中數據庫
二、ListView先請求一個type1視圖(getView)而後請求其餘可見的項目。convertView在getView中是空(null)的緩存
三、當item1滾出屏幕,而且一個新的項目從屏幕低端上來時,ListView再請求一個type1視圖。convertView此時不是空值了,它的值是item1。你只需設定新的數據而後返回convertView,沒必要從新建立一個視圖安全
1、複用convertView,減小findViewById的次數網絡
一、優化一:複用convertView多線程
Android系統自己爲咱們考慮了ListView的優化問題,在複寫的Adapter的類中,比較重要的兩個方法是getCount()和getView()。界面上有多少個條顯示,就會調用多少次的getView()方法;所以若是在每次調用的時候,若是不進行優化,每次都會使用View.inflate(….)的方法,都要將xml文件解析,並顯示到界面上,這是很是消耗資源的:由於有新的內容產生就會有舊的內容銷燬,因此,能夠複用舊的內容。異步
優化:函數
在getView()方法中,系統就爲咱們提供了一個複用view的歷史緩存對象convertView,當顯示第一屏的時候,每個item都會新建立一個view對象,這些view都是能夠被複用的;若是每次顯示一個view都要建立一個,是很是耗費內存的;因此爲了節約內存,能夠在convertView不爲null的時候,對其進行復用
二、優化二:緩存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)獲取引用(須要強轉)
2、ListView中數據的分批及分頁加載:
需求:ListView有一萬條數據,如何顯示;若是將十萬條數據加載到內存,很消耗內存
解決辦法:
優化查詢的數據:先獲取幾條數據顯示到界面上
進行分批處理---à優化了用戶體驗
進行分頁處理---à優化了內存空間
說明:
通常數據都是從數據庫中獲取的,實現分批(分頁)加載數據,就須要在對應的DAO中有相應的分批(分頁)獲取數據的方法,如findPartDatas ()
一、準備數據:
在dao中添加分批加載數據的方法:findPartDatas ()
在適配數據的時候,先加載第一批的數據,須要加載第二批的時候,設置監聽檢測什麼時候加載第二批
二、設置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
以安全衛士中的進程管理的功能爲例。效果如圖:
一、定義兩個(或多個)集合
每一個集合中存入的是對應不一樣類型的內容(這裏爲:用戶程序(userAppinfos)和系統程序的集合(systemAppinfos))
二、在初始化數據(填充數據)中初始化兩個集合
如,此處是在fillData方法中初始化
三、在數據適配器中,複寫對應的方法
getCount():計算全部須要顯示的條目個數,這裏包括listView和textView
getView():對顯示在不一樣位置的條目進行if處理
四、數據類型的判斷
須要注意的是,在複用view的時候,須要對convertView進行類型判斷,是由於這裏含有各類不一樣類型的view,在view滾動顯示的時候,對於不一樣類型的view不能複用,全部須要判斷
4、ListView中圖片的優化:詳看OOM異常中圖片的優化
一、處理圖片的方式:
若是自定義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集,在顯示前判斷處理一下便可。
三、內存緩衝機制:
首先限制內存圖片緩衝的堆內存大小,每次有圖片往緩存里加時判斷是否超過限制大小,超過的話就從中取出最少使用的圖片並將其移除。
固然這裏若是不採用這種方式,換作軟引用也是可行的,兩者目的皆是最大程度的利用已存在於內存中的圖片緩存,避免重複製造垃圾增長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();
}
}
5、ListView的其餘優化:
一、儘可能避免在BaseAdapter中使用static 來定義全局靜態變量:
static是Java中的一個關鍵字,當用它來修飾成員變量時,那麼該變量就屬於該類,而不是該類的實例。因此用static修飾的變量,它的生命週期是很長的,若是用它來引用一些資源耗費過多的實例(好比Context的狀況最多),這時就要儘可能避免使用了。
二、儘可能使用getApplicationContext:
若是爲了知足需求下必須使用Context的話:Context儘可能使用Application Context,由於Application的Context的生命週期比較長,引用它不會出現內存泄露的問題
三、儘可能避免在ListView適配器中使用線程:
由於線程產生內存泄露的主要緣由在於線程生命週期的不可控制。以前使用的自定義ListView中適配數據時使用AsyncTask自行開啓線程的,這個比用Thread更危險,由於Thread只有在run函數不結束時纔出現這種內存泄露問題,然而AsyncTask內部的實現機制是運用了線程執行池(ThreadPoolExcutor),這個類產生的Thread對象的生命週期是不肯定的,是應用程序沒法控制的,所以若是AsyncTask做爲Activity的內部類,就更容易出現內存泄露的問題。解決辦法以下:
①、將線程的內部類,改成靜態內部類。
②、在線程內部採用弱引用保存Context引用