Android開發之ListView 優化之快取優化
通過平時對ListView的使用,目前我把ListView的優化分為以下幾個方面:
快取優化、資料優化、其他方面優化
0.未優化簡單程式碼
<span style="font-size:14px;">public class MainActivity extends Activity { private ListView lv_demo; private List<String> list; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); lv_demo = (ListView) findViewById(R.id.listView); //list為要載入的條目文字的集合,這裡總共是100條 list = new ArrayList<String>(); for (int i = 0; i < 1000; i++) { list.add("條目" + i); } lv_demo.setAdapter(new MyAdapter()); } private class MyAdapter extends BaseAdapter { @Override public int getCount() { return list.size(); } @Override public View getView(int position, View convertView, ViewGroup parent) { //listview_item裡只有一個textview View view = View.inflate(MainActivity.this, R.layout.listview_item, null); //使用每一次都findviewById的方法來獲得listview_item內部的元件 TextView tv_item = (TextView) view.findViewById(R.id.tv_item); tv_item.setText(list.get(position)); return view; } @Override public Object getItem(int position) { return null; } @Override public long getItemId(int position) { return 0; } } }</span>
1.快取優化(介面卡Adapter)
即Google I/O 2009 Romain Guy 所講的內容,利用ListView的自身優化機制優化——快取優化。
ListView的Adapter的作用如下圖所示
1.1 優化一:複用convertView
Android系統本身為我們考慮了ListView的優化問題,在複寫的Adapter的類中,比較重要的兩個方法是getCount()和getView()。介面上有多少個條顯示,就會呼叫多少次的getView()方法;因此如果在每次呼叫的時候,如果不進行優化,每次都會使用View.inflate(….)的方法,都要將xml檔案解析,並顯示到介面上,這是非常消耗資源的:因為有新的內容產生就會有舊的內容銷燬,所以,可以複用舊的內容。
優化:
在getView()方法中,系統就為我們提供了一個複用view的歷史快取物件convertView,當顯示第一屏的時候,每一個item都會新建立一個view物件,這些view都是可以被複用的;如果每次顯示一個view都要建立一個,是非常耗費記憶體的;所以為了節約記憶體,可以在convertView不為null的時候,對其進行復用。
示例:
<span style="font-size:14px;"> @Override public View getView(int position, View convertView, ViewGroup parent) { View view; // 判斷convertView的狀態,來達到複用效果 if(null == convertView){ //如果convertView為空,則表示第一次顯示該條目,需要建立一個view view = View.inflate(Main1Activity.this,R.layout.listview_item,null); }else{ //否則表示可以複用convertView view = convertView; } TextView textView =(TextView) view.findViewById(R.id.tv_item); textView.setText(list.get(position)); return view; }</span>
1.2 優化二:快取item條目的引用——ViewHolder
findViewById()這個方法是比較耗效能的操作,因為這個方法要找到指定的佈局檔案,進行不斷地解析每個節點:從最頂端的節點進行一層一層的解析查詢,找到後在一層一層的返回,如果在左邊沒找到,就會接著解析右邊,並進行相應的查詢,直到找到位置。因此可以對findViewById進行優化處理。
特點:xml檔案被解析的時候,只要被創建出來了,其孩子的id就不會改變了。根據這個特點,可以將孩子id存入到指定的集合中,每次就可以直接取出集合中對應的元素就
可以了。
優化:
在建立view物件的時候,減少佈局檔案轉化成view物件的次數;即在建立view物件的時候,把所有孩子全部找到,並把孩子的引用給存起來
①定義儲存控制元件引用的類ViewHolder
這裡的ViewHolder類需要不需要定義成static,根據實際情況而定,如果item不是很多的話,可以使用,這樣在初始化的時候,只加載一次,可以稍微得到一些優化
不過,如果item過多的話,建議不要使用。因為static是Java中的一個關鍵字,當用它來修飾成員變數時,那麼該變數就屬於該類,而不是該類的例項。所以用static修飾的變數,它的生命週期是很長的,如果用它來引用一些資源耗費過多的例項(比如Context的情況最多),這時就要儘量避免使用了。
static 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)獲取引用(需要強轉)
示例:
public class MyAdapter extends BaseAdapter{
@Override
public int getCount() {
return list.size();
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
if(null == convertView){
convertView = View.inflate(Main2Activity.this,R.layout.listview_item,null);
viewHolder = new ViewHolder();
viewHolder.textView = (TextView) convertView.findViewById(R.id.tv_item);
convertView.setTag(viewHolder);
}else{
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.textView.setText(list.get(position));
return convertView;
}
}
static class ViewHolder{
TextView textView;
}
3.資料優化
3.1 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、對不同的狀態進行處理:
分批載入資料,只關心靜止狀態:關心最後一個可見的條目,如果最後一個可見條目就是資料介面卡(集合)裡的最後一個,此時可載入更多的資料。在每次載入的
時候,計算出滾動的數量,當滾動的數量大於等於總數量的時候,可以提示使用者無更多資料了。
3.2 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集,在顯示前判斷處理一下即可。
4.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引用