張延年,米洪
(南京交通職業技術學院,南京 211188)
Android應用開發中ListView組件性能優化的研究
張延年,米洪
(南京交通職業技術學院,南京211188)
市場上很多Android應用存在忽視程序性能優化的問題,這些應用往往在用戶使用量達到一定規模時出現性能問題,例如操作卡頓、下載變慢、突然閃退。要解決這些問題,就要求產品設計師必須提早將程序性能方面的需求作詳細的分析并提出相應的解決方案。因此,性能優化問題的研究對提高程序的健壯性、可擴展性,降低后期的修改和維護成本具有重要的意義[1]。本文主要涉及Android應用程序代碼層次上的優化,首先分析了常規的代碼優化機制,然后通過一個典型案例闡述了ListView組件的具體優化方案,并詳細測試和分析了優化的效果,最后證實了優化方法的正確性和可行性。

(1)增加緩存
緩存就是在內存中開辟一塊區域作為臨時數據交換區域,其中包括對象緩存、I/O緩存、網絡緩存、DB緩存等[2]。由于在內存、文件、數據庫、網絡的讀寫速度中,內存都是最優的,且速度數量級差別,所以盡量將需要頻繁訪問或訪問一次消耗較大的數據存儲在緩存中。
Android中常使用緩存機制有:
①線程池。
②圖片緩存,包括圖片Sdcard緩存,數據預取緩存。
③消息緩存,通過handler.obtainMessage復用之前的message。
④ListView緩存。
⑤文件I/O緩存。
使用具有緩存策略的輸入流,如BufferedInput-Stream替代InputStream。
⑥布局緩存。
(2)數據存儲優化
①數據類型選擇
●字符串拼接用StringBuilder代替String,在非并發情況下用StringBuilder代替StringBuffer。
●64位類型如long double的處理比32位如int慢。
●使用SoftReference、WeakReference相對正常的強應用來說更有利于系統垃圾回收。
●final類型存儲在常量區中讀取效率更高。
●LocalBroadcastManager代替普通 BroadcastRe-ceiver,效率和安全性都更高。
②數據結構選擇
●ArrayList和LinkedList的選擇,ArrayList根據index取值更快,LinkedList更占內存、隨機插入刪除更快速、擴容效率更高,一般推薦ArrayList。
●ArrayList、HashMap、LinkedHashMap、HashSet的選擇,hash系列數據結構查詢速度更優,ArrayList存儲有序元素,HashMap為鍵值對數據結構,Linked-HashMap可以記住加入次序的hashMap,HashSet不允許重復元素。
●HashMap、WeakHashMap選擇,WeakHashMap中元素可在適當時候被系統垃圾回收器自動回收,所以適合在內存緊張型中使用。
●Collections.synchronizedMap和ConcurrentHash-Map的選擇,ConcurrentHashMap為細分鎖,鎖粒度更小,并發性能更優。Collections.synchronizedMap為對象鎖,自己添加函數進行鎖控制更方便。
●Android也提供了一些性能更優的數據類型,如SparseArray、SparseBooleanArray、SparseIntArray、Pair。Sparse系列的數據結構是為key為int情況的特殊處理,采用二分查找及簡單的數組存儲,不需要泛型轉換的開銷,相對Map來說性能更優。
(3)算法優化
例如:盡量不用O(n*n)時間復雜度以上的算法,必要時候可用空間換時間。查詢考慮hash和二分,盡量不用遞歸。
(4)邏輯優化
這個不同于算法,主要是理清程序邏輯,減少不必要的操作。
(5)需求優化
對于部分對產品使用性能產生重大影響的需求,必須進行需求簡化或變更。

Android應用開發過程中必須遵循單線程模型(Single Thread Model)的原則[3]。因為Android的UI操作并不是線程安全的,所以涉及UI的操作必須在UI線程中完成。但是并非所有的操作都能在主線程中進行,Android應用程序在設計上約定,在5s內無響應的話會導致ANR(Application Not Response),這就要求開發者必須遵循兩條法則:第一不能阻塞UI線程,第二確保只在UI線程中訪問Android UI工具包。因此,對于耗時操作必須在另外開啟的工作線程中完成,然后通過handler和主線程交互。

(1)延遲操作
不在Activity、Service、BroadcastReceiver的生命周期等對響應時間敏感函數中執行耗時操作,可適當延遲[4]。
Java中延遲操作可使用ScheduledExecutorService,不推薦使用Timer.schedule;
Android中除了支持ScheduledExecutorService之外,還有一些延遲操作,如:
handler.postDelayed,handler.postAtTime,handler. sendMessageDelayed,
View.postDelayed,AlarmManager定時等。
(2)提前操作
對于第一次調用較耗時操作,可統一放到初始化中,將耗時提前。如得到壁紙wallpaperManager.get-Drawable()。
1.4網絡優化
以下是網絡優化中需要遵守的準則[5]:
●圖片必須緩存,最好根據機型做圖片適配。
●所有http請求必須添加HttpTimeOut。
●API接口數據盡量以json格式返回,而不是xml 或html。
●根據http頭信息中的Cache-Control及expires域確定是否緩存請求結果。
●發送網絡請求前需確定connection是否keepalive。
●減少網絡請求次數,服務器端適當做請求合并。
●減少重定向次數。
●API接口服務器端響應時間不超過100ms。
ListView是Android中常用的組件之一,常用于新聞列表、產品列表、聯系人清單等內容的展現,要使用ListView主要用到以下幾個元素。
(1)ListVeiw組件,用來展示列表的View,一般通過編寫xml布局文件來獲取。
(2)Adapter適配器,用來把數據映射到ListView上的中介。Android SDK主要提供了BaseAdapter、ArrayAdapter<T>,SimpleAdapter等幾個主要的類。
(3)List集合或數組,提供要顯示的數據,其中的元素一般是自定義的對象,對象的屬性可以字符串,圖片等,這些屬性的一部分或全部最后要顯示在ListVeiw組件上。
下面將通過一個典型的新聞列表案例來說明如何對ListView進行優化。

本案例的主要功能是在一個ListView中顯示100條新聞信息,其中每條信息包含標題文本、內容文本和小圖標(圖片)三項內容。為了更清楚地說明解決編程優化問題的方法,在此對實際項目案例做了一些簡化,系統所用數據均在本地直接生成 (不考慮網絡傳輸時延),數據信息采用模擬數據“新聞標題0,新聞標題1,…,新聞標題99”和“新聞內容0,新聞內容1,…,新聞內容99”,100條新聞的圖片均采用同一圖片。關鍵代碼如下:

新聞實體類:News主界面布局文件:activity_main.xml,包含一個ListView組件。
列表子項布局文件listview_item.xml,包含兩個TextView組件和一個ImageView組件,分別用來顯示新聞條目的標題、內容和圖片。
數據提供類:DataProder

public static final int count=100;//新聞條數/*獲取所有新聞記錄*/

新聞列表主界面類:MainActivity

/*創建自定義適配器內部類*/

/*根據制定的列表項位置獲取一個包含要顯示的數據的列表項視圖 */

程序運行結果如下圖所示:

圖1 程序運行結果圖

(1)問題分析
在程序測試過程中,對ListView組件做滑動操作時會出現輕微的卡頓現象,這個案例中僅有100條簡單數據,而且還沒有通過網絡進行數據傳輸,可以想象在實際項目中數據量將大很多,且需要網絡進行傳輸,所以直接用以上代碼則卡頓現象會更嚴重,甚至出現程序閃退,因此對程序性能進行優化是十分必要的。在進行優化之前必須找出代碼運行性能瓶頸所在的位置,這里使用Android SDK所提供的性能測試工具traceview進行測試。
(2)性能測試
在華為8813Q(Android4.1)上運行以上程序,開啟Eclispe里的DDMS工具,單擊Start Mathod Profiling按鈕,開啟traceview工具[6],在5秒鐘之在手機上快速向下滑動ListView到列表的最底部,最后點擊 Stop Mathod Profiling按鈕,這時會自動彈出測試結果頁面xxx.trace文件,將文件保存到C盤根目錄下,使用traceview工具打開文件,在最下面的Find查找框輸入getview,將測試結果進行截圖并標明重要的數據,最終測試結果如下圖所示。優化后的程序將嚴格按照以上測試環境和步驟進行。
對以上優化過的程序進行性能測試,得到如下結果:

圖2 未優化前程序性能測試結果圖
性能分析:
在測試界面上會發現其中getView函數占用的cpu時間非常高,達到50.4%,而getView中又以inflate函數耗費的時間最多,居然占據整個getView中所有操作95%的時間,而這個inflate函數的操作在整個應用程序中占用的cpu時間也達到了48%。通過以上分析定位程序運行的瓶頸主要在getView函數。下面將對適配器中getView函數的代碼進行優化。

(1)優化方案一:減少getView中填充布局inflate函數操作,重復利用布局視圖對象。
在MyAdapter類中的getView方法中,每次需要一個View對象時,都是通過inflate方法生成。實際上對于ListView而言,只需要保留能夠顯示的最大個數的view即可,其他新的view可以通過復用的方式使用消失的條目的view。在getView方法里提供了一個參數:convertView,這個參數就代表著可以復用的view對象,當然這個對象也可能為空,當它為null的時候,表示該條目view第一次創建,所以系統需要inflate一個view出來,而當它不為null的時候,系統就可以復用。關鍵代碼如下:

對以上優化過的程序進行性能測試,得到如下結果:

圖3 經方案一優化后程序性能測試結果圖
性能分析:
從以上測試結果可以看出,getView函數占用的cpu時間有明顯減少,從原來的50.4%減少到12.8%,效率提升了1倍多,而inflate函數的調用所占時間減少更明顯,從原來的getView中所占比例的95%減少到5.6%。因此通過方法一的優化,ListView組件的運行效率有了較大的提升,若僅從其中getView函數占用的cpu的時間上比較從2371減少到605,效率提升了大約74%。
(2)優化方案二:使用靜態內部類ViewHolder,減少findeViewById方法的操作。
程序每一次獲取view對象都需要通過資源id,也就是使用findViewById函數進行操作。頻繁的find-ViewById方法調用將要耗費大量的內存和時間,如果可以讓view內的組件也隨著view的復用而復用則可以大大減少這部分操作。這里采用重新建一個內部靜態類的方法,里面的成員變量跟view中所包含的組件個數類型相同,根據本案例可以創建如下靜態類:

ViewHolder類復用的基本思路是在 convertView 為null的時候,系統不僅重新inflate出來一個view,并進行findviewbyId的查找工作,同時還需要獲取一個ViewHolder類的對象,并將findviewById的結果賦值給ViewHolder中對應的成員變量。最后將holder對象與該view對象“綁”在一塊。
當convertView不為null時,將converView賦值給view,同時取出這個view對應的holder對象,就獲得了這個view對象中的TextView組件,它就是holder中的成員變量,這樣在復用的時候,就不需要再去find-ViewById了,只需要在最開始的時候進行數次查找工作。這里的關鍵在于如何將view與holder對象進行綁定,那么就需要用到兩個方法:setTag和getTag方法了。關鍵代碼如下:

對以上優化過的程序進行測試,得到如下結果:

圖4 經方案二優化后程序性能測試結果圖
性能分析:
從以上測試結果可以看出,getView函數占用的cpu時間比例有明顯減少,從原來的 12.8%減少到9.1%,而其中findViewById函數的調用所占時間由從原來的getView中所占比例的4.4%減少到0.0%。這里從cpu占用比率無法準確判斷優化的效果,而從cpu占用的絕對時間上則可以看出明顯的效率提升,getView由605減少到230,效率提升62%,findView-ById由26減少到0.108,效率提升99%。
(3)優化方案三:ListView滑動時不加載數據,停下來時加載數據,提升滑動流暢度。
經過上面的優化操作,ListView組件的性能已經有了很大的提升,但是當用戶頻繁滑動操作時,特別是需要從服務器端下載大量圖片數據時將出現卡頓現象。具體解決方案如下。
首先,在 Adapter的 getView方法中,在給ViewHolder的屬性賦值前做個判斷,即當組件在滑動狀態時,由于用戶并不會關心列表的任何信息,所以系統可以加載本地的默認數據 (也可以稱為假數據),而當組件不在滑動狀態時,再加載真正的數據。為此系統需要在MyAdapter里設置一個滑動狀態的boolean類型的屬性scrollState,然后由MainAcitvity實現OnScrollListener接口,作為Listview滑動操作的監聽器,在監聽器方法onScrollStateChanged中設置屬性scrollState的值。
其次,由于無法直接調用getView方法,所以需要在onScrollStateChanged中 當 參 數scrollState== SCROLL_STATE_IDLE (停止滾動)時,通過對ViewHolder中的每個屬性的tag屬性值進行判斷,來決定ViewHolder的數據是否進行加載。即getView如果在滑動狀態時執行setTag(默認文本信息)或setTag(默認圖片文件名),在非滑動狀態時執行setTag(null)或set-Tag(“1”),這樣在onScrollStateChanged中就可以根據這個tag的值來判斷是否需要加載數據。關鍵代碼如下:

對以上優化過的程序進行測試,得到如下結果:

圖5 經方案二優化后程序性能測試結果圖
性能分析:
從以上測試結果可以看出,getView函數占用的cpu時間比例又有明顯減少,從原來的9.1%減少到2.2%,而findViewById函數的調用在getView中所占比例為0.3%,沒有顯著變化,說明增加的代碼并沒有影響減少findeViewById函數的操作的優化效果。
(4)其他優化方案
本案例中的ListView都是顯示的本地的List集合中的內容,而且List的長度也只有100個,數據量很小,系統可以一次性加載完成測試數據。但是實際應用中,系統往往會需要使用ListView來顯示網絡上的大量內容,一般有以下兩個問題需要考慮:
其一:假如網絡情況很好,使用的手機也許能夠一下子加載完所有新聞數據,然后顯示在ListView中,用戶可能感覺還好,假如說在網絡不太順暢的情況下,用戶加載完所有網絡的數據,可能這個list有上千條新聞,那么用戶可能需要面對一個空白的Activity好幾分鐘,這個顯然是不合適的。
其二:Android虛擬機給每個應用分配的運行時內存是一定的,一般性能不太好的機器只有16M,好一點的可能也就是64M的樣子,假如要瀏覽的新聞總數為上萬條,可能出現內存溢出,應用崩潰的情況。
如何解決上面所述兩個問題呢?由于這部分優化方法的使用一般都與實際項目的需求有關,是否有必要優化以及如何優化都要具體問題具體分析,優化方法不能通用,因此只能對以上問題的解決方法做如下簡單說明。
首先,采用數據分批加載,例如1000條新聞的List集合,系統一次加載20條,等到用戶翻頁到底部的時候,再添加下面的20條到List中,并使用Adapter刷新ListView,這樣用戶一次只需要等待20條數據的傳輸時間,并可以預防內存溢出,應用崩潰的情況。
其次,分批加載有時候也不能完全解決問題,因為雖然在分批中一次只增加20條數據到List集合中,但假如有10萬條數據,如果系統要順利讀到最后,這個List集合中還是會累積海量條數的數據,將可能會造成OOM的情況,這時候就需要用到分頁技術,每頁加載時都覆蓋掉上一頁中List集合中的內容。
本文主要對Android應用開發中ListView組件編程過程中的性能優化做了較為深入的研究,通過一個典型的新聞客戶端列表案例詳細闡述了三種通用的編程優化方案,包括性能瓶頸的分析、優化的思路和方法、優化的步驟、程序運行測試等,另外簡要概述了其他一些較為復雜的優化方法。
本文中所涉及的案例代碼都已通過實際上機運行測試,通過對優化前后測試結果的比較和分析,證明以上優化方法是切實可行的,對組件的運行效率有較大的提高,對程序的整體性能有較大的改善和提升。
[1]丁振凡,吳小元.Android系統ListView控件數據遞增顯示研究[J].智能計算機與應用,2014,4(2):49-52.
[2]王海峰.基于Android技術校園信息平臺客戶端的研究與設計[J].軟件工程師,2014,17(9):43-45.
[3]鮑曉.基于Android平臺的新聞資訊閱讀軟件的設計與實現[J].計算機應用,2013,33(S2):279-282.
[4]邱忠權,候雪莉,張德新.基于Android系統的列車移動信息服務平臺設計與訂餐系統的實現[J].交通運輸工程與學報,2015,13 (1):18-25.
[5](美)Ronan Schwarz,Phil Dutson,James Steele,Nelson To.Android開發秘籍(第2版)[M].錢昊譯.北京:人民郵電出版社,2014.8.
[6]武永亮.Android開發范例實戰寶典[M].北京:清華大學出版社,2014.9
Android;ListView;Performance Optimization;TraceView
Research on ListView Performance Optimization in Android Application Development
ZHANG Yan-nian,MI Hong
(Nanjing Communications Institute of Technology,Nanjing 211188)
1007-1423(2015)36-0058-07
10.3969/j.issn.1007-1423.2015.36.014
張延年(1977-),男,山東聊城人,碩士,講師,研究方向為移動互聯網、云計算等
2015-11-19
2015-12-19
概述Android應用開發中代碼層次上性能優化的常規機制,然后通過一個典型案例詳細闡述對ListView組件進行性能優化的三種解決方案,其中包括性能瓶頸的分析、具體的優化步驟和優化結果的測試與分析,最后對其他一些特定場景下ListView組件的優化方法作簡要的介紹。使用TraceView性能分析工具對優化前后的程序進行多次測試實驗,實驗結果表明優化方案的實施顯著提高程序的性能,提升程序的運行效率。
Android;ListView;性能優化;TraceView
米洪(1974-),男,山東泰安人,碩士,副教授,研究方向為移動互聯網、信息安全等
Outlines the general mechanism of code level performance optimization in Android application development,and then elaborates the three solutions to the performance optimization of ListView components,including the analysis of the performance bottleneck,the specific optimization steps and the test and analysis of the optimization results,and finally gives a brief introduction to the optimization method of ListView components in some specific scenarios.Uses TraceView performance analysis tool to test the program.The experimental results show that the optimized scheme can significantly improve the performance of the program and improve the operating efficiency of the program.