Android性能優化之------Listview優化


ListView的工作原理

發表於:2015/7/7  15:18:24

首先來了解一下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的實現

  1 public class MemoryCache {
  2 
  3         private static final String TAG = "MemoryCache";
  4 
  5         // 放入緩存時是個同步操作
  6 
  7         // LinkedHashMap構造方法的最后一個參數true代表這個map里的元素將按照最近使用次數由少到多排列,即LRU
  8 
  9         // 這樣的好處是如果要將緩存中的元素替換,則先遍歷出最近最少使用的元素來替換以提高效率
 10 
 11         private Map<String, Bitmap> cache = Collections
 12 
 13                         .synchronizedMap(new LinkedHashMap<String, Bitmap>(10, 1.5f, true));
 14 
 15         // 緩存中圖片所占用的字節,初始0,將通過此變量嚴格控制緩存所占用的堆內存
 16 
 17         private long size = 0;// current allocated size
 18 
 19         // 緩存只能占用的最大堆內存
 20 
 21         private long limit = 1000000;// max memory in bytes
 22 
 23          public MemoryCache() {
 24 
 25                 // use 25% of available heap size
 26 
 27                 setLimit(Runtime.getRuntime().maxMemory() / 10);
 28 
 29         }
 30 
 31         public void setLimit(long new_limit) {
 32 
 33                 limit = new_limit;
 34 
 35                 Log.i(TAG, "MemoryCache will use up to " + limit / 1024. / 1024. + "MB");
 36 
 37         }
 38 
 39         public Bitmap get(String id) {
 40 
 41                 try {
 42 
 43                         if (!cache.containsKey(id))
 44 
 45                                 return null;
 46 
 47                         return cache.get(id);
 48 
 49                 } catch (NullPointerException ex) {
 50 
 51                         return null;
 52 
 53                 }
 54 
 55         }
 56 
 57         public void put(String id, Bitmap bitmap) {
 58 
 59                 try {
 60 
 61                         if (cache.containsKey(id))
 62 
 63                                 size -= getSizeInBytes(cache.get(id));
 64 
 65                         cache.put(id, bitmap);
 66 
 67                         size += getSizeInBytes(bitmap);
 68 
 69                         checkSize();
 70 
 71                 } catch (Throwable th) {
 72 
 73                         th.printStackTrace();
 74 
 75                 }
 76 
 77         }
 78 
 79          /**
 80 
 81          * 嚴格控制堆內存,如果超過將首先替換最近最少使用的那個圖片緩存
 82 
 83          *
 84 
 85          */
 86 
 87         private void checkSize() {
 88 
 89                 Log.i(TAG, "cache size=" + size + " length=" + cache.size());
 90 
 91                 if (size > limit) {
 92 
 93                         // 先遍歷最近最少使用的元素
 94 
 95                         Iterator<Entry<String, Bitmap>> iter = cache.entrySet().iterator();
 96 
 97                         while (iter.hasNext()) {
 98 
 99                                 Entry<String, Bitmap> entry = iter.next();
100 
101                                 size -= getSizeInBytes(entry.getValue());
102 
103                                 iter.remove();
104 
105                                 if (size <= limit)
106 
107                                         break;
108 
109                         }
110 
111                         Log.i(TAG, "Clean cache. New size " + cache.size());
112 
113                 }
114 
115         }
116 
117         public void clear() {
118 
119           cache.clear();
120 
121         }
122 
123  
124 
125         /**
126 
127          * 圖片占用的內存
128 
129              * <a href="\"http://www.eoeandroid.com/home.php?mod=space&uid=2768922\"" target="\"_blank\"">@Param</a> bitmap
130 
131             * @return
132 
133          */
134 
135         long getSizeInBytes(Bitmap bitmap) {
136 
137                 if (bitmap == null)
138 
139                         return 0;
140 
141                 return bitmap.getRowBytes() * bitmap.getHeight();
142 
143         }
144 
145 }

 

 

五、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引用

 本文摘自鏈接:http://www.cnblogs.com/tonycheng93/p/4625513.html#top


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM