天天看點

listview 記憶體優化

在整理前幾篇文章的時候有朋友提出寫一下ListView的性能優化方面的東西,這個問題也是小馬在面試過程中被别人問到的…..今天小馬就借此機會來整理下,網上類似的資料蠻多的,倒不如自己寫一篇,記錄在這個地方,供自己以後使用,不用再翻來翻去的找了,用自己寫的…呵呵,不多講其它了,說起優化我想大家第一反應跟小馬一樣吧?想到利用ViewHolder來優化ListView資料加載,僅僅就此一條嗎?其實不是的,首先,想要優化ListView就得先了解ListView加載資料原理,這是前提,但是小馬在這個地方先做一些簡單的補充,大家一定仔細看下,保證會有收獲的:

   清單的顯示需要三個元素:

  1. ListVeiw:  用來展示清單的View。
  2. 擴充卡 : 用來把資料映射到ListView上
  3. 資料:    具體的将被映射的字元串,圖檔,或者基本元件。 

           根據清單的擴充卡類型,清單分為三種,ArrayAdapter,SimpleAdapter和SimpleCursorAdapter,這三種擴充卡的使用大家可學習下官網上面的使用或者自行百度谷歌,一堆DEMO!!!其中以ArrayAdapter最為簡單,隻能展示一行字。SimpleAdapter有最好的擴充性,可以自定義出各種效果。SimpleCursorAdapter可以認為是SimpleAdapter對資料庫的簡單結合,可以友善的把資料庫的内容以清單的形式展示出來。

           系統要繪制ListView了,他首先用getCount()函數得到要繪制的這個清單的長度,然後開始繪制第一行,怎麼繪制呢?調用getView()函數。在這個函數裡面首先獲得一個View(這個看實際情況,如果是一個簡單的顯示則是View,如果是一個自定義的裡面包含很多控件的時候它其實是一個ViewGroup),然後再執行個體化并設定各個元件及其資料内容并顯示它。好了,繪制完這一行了。那 再繪制下一行,直到繪完為止,前面這些東西做下鋪墊,繼續…….

           現在我們再來了解ListView加載資料的原理,有了這方面的了解後再說優化才行,下面先跟大家一起來看下ListView加載資料的基本原理小馬就直接寫了:

ListView的工作原理如下:

                 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加載資料的工作原理(原理圖看不清楚的點選後看大圖):

listview 記憶體優化

下面簡單說下上圖的原理:

  1. 如果你有幾千幾萬甚至更多的選項(item)時,其中隻有可見的項目存在記憶體(記憶體記憶體哦,說的優化就是說在記憶體中的優化!!!)中,其他的在Recycler中
  2. ListView先請求一個type1視圖(getView)然後請求其他可見的項目。convertView在getView中是空(null)的
  3. 當item1滾出螢幕,并且一個新的項目從螢幕低端上來時,ListView再請求一個type1視圖。convertView此時不是空值了,它的值是item1。你隻需設定新的資料然後傳回convertView,不必重新建立一個視圖

             下面來看下小馬從網上找來的示例代碼,網址搞丢了,隻有一個word文檔,隻能 copy過來,不然直接貼網址,結合上面的原理圖一起加深了解,如下:

public class MultipleItemsList extends ListActivity {      private MyCustomAdapter mAdapter;      @Override     public void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);         mAdapter = new MyCustomAdapter();         for (int i = 0; i < 50; i++) {             mAdapter.addItem("item " + i);         }         setListAdapter(mAdapter);     }      private class MyCustomAdapter extends BaseAdapter {          private ArrayList mData = new ArrayList();         private LayoutInflater mInflater;          public MyCustomAdapter() {             mInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);         }          public void addItem(final String item) {             mData.add(item);             notifyDataSetChanged();         }          @Override         public int getCount() {             return mData.size();         }          @Override         public String getItem(int position) {             return mData.get(position);         }          @Override         public long getItemId(int position) {             return position;         }          @Override         public View getView(int position, View convertView, ViewGroup parent) {             System.out.println("getView " + position + " " + convertView);             ViewHolder holder = null;             if (convertView == null) {                 convertView = mInflater.inflate(R.layout.item1, null);                 holder = new ViewHolder();                 holder.textView = (TextView)convertView.findViewById(R.id.text);                 convertView.setTag(holder);             } else {                 holder = (ViewHolder)convertView.getTag();             }             holder.textView.setText(mData.get(position));             return convertView;         }      }      public static class ViewHolder {         public TextView textView;     } }      

執行程式,檢視日志:

listview 記憶體優化

getView 被調用 9 次 ,convertView 對于所有的可見項目是空值(如下):

listview 記憶體優化

然後稍微向下滾動List,直到item10出現:

listview 記憶體優化

       convertView仍然是空值,因為recycler中沒有視圖(item1的邊緣仍然可見,在頂端)再滾動清單,繼續滾動:

listview 記憶體優化

      convertView不是空值了!item1離開螢幕到Recycler中去了,然後item11被建立,再滾動下:

listview 記憶體優化

此時的convertView非空了,在item11離開螢幕之後,它的視圖(…0f8)作為convertView容納item12了,好啦,結合以上原理,下面來看看今天最主要的話題,主角ListView的優化:

             首先,這個地方先記兩個ListView優化的一個小點:

                       1. ExpandableListView 與 ListActivity 由官方提供的,裡面要使用到的ListView是已經經過優化的ListView,如果大家的需求可以用Google自帶的ListView滿足的的話盡量用官方的,絕對沒錯!

                       2.其次,像小馬前面講的,說ListView優化,其實并不是指其它的優化,就是記憶體是的優化,提到記憶體…(想到OOM,折騰了我不少時間),很多很多,先來寫下,如果我們的ListView中的選項僅僅是一些簡單的TextView的話,就好辦啦,消耗不了多少的,但如果你的Item是自定義的Item的話,例如你的自定義Item布局ViewGroup中包含:按鈕、圖檔、flash、CheckBox、RadioButton等一系列你能想到的控件的話, 你要在getView中單單使用文章開頭提到的ViewHolder是遠遠不夠的,如果資料過多,加載的圖檔過多過大,你BitmapFactory.decode的猛多的話,OOM搞死你,這個地方再警告下大家,是警告……….也提醒下自己:

                         小馬碰到的問題大家應該也都碰到過的,自定義的ListView項亂序問題,我很天真的在getView()中強制清除了下ListView的緩存資料convertView,也就是convertView = null了,雖然當時是解決了這個問題讓其它每次重繪,但是犯了大錯了,如果資料太多的話,出現最最惡心的錯,手機卡死或強制關機,關機啊哥哥們……O_O,客戶殺了我都有可能,但大家以後别犯這樣的錯了,單單使用清除緩存convertView是解決不了實際問題的,繼續……

下面是小記:圖檔用完了正确的釋放… 

if(!bmp.isRecycle() ){          bmp.recycle()   //回收圖檔所占的記憶體          system.gc()  //提醒系統及時回收 }      

下面來列舉下真正意義上的優化吧:

  1.  ViewHolder   Tag 必不可少,這個不多說!
  2. 如果自定義Item中有涉及到圖檔等等的,一定要狠狠的處理圖檔,圖檔占的記憶體是ListView項中最惡心的,處理圖檔的方法大緻有以下幾種:

    2.1:不要直接拿個路徑就去循環decodeFile();這是找死….用Option儲存圖檔大小、不要加載圖檔到記憶體去;

    2.2:  拿到的圖檔一定要經過邊界壓縮

    2.3:在ListView中取圖檔時也不要直接拿個路徑去取圖檔,而是以WeakReference(使用WeakReference代替強引用。比如可以使        用WeakReference<Context> mContextRef)、SoftReference、WeakHashMap等的來存儲圖檔資訊,是圖檔資訊不是圖檔哦!

    2.4:在getView中做圖檔轉換時,産生的中間變量一定及時釋放,用以下形式:

  3. 盡量避免在BaseAdapter中使用static 來定義全局靜态變量,我以為這個沒影響 ,這個影響很大,static是Java中的一個關鍵字,當用它來修飾成員變量時,那麼該變量就屬于該類,而不是該類的執行個體。是以用static修飾的變量,它的生命周期是很長的,如果用它來引用一些資源耗費過多的執行個體(比如Context的情況最多),這時就要盡量避免使用了..
  4. 如果為了滿足需求下必須使用Context的話:Context盡量使用Application Context,因為Application的Context的生命周期比較長,引用它不會出現記憶體洩露的問題
  5. 盡量避免在ListView擴充卡中使用線程,因為線程産生記憶體洩露的主要原因在于線程生命周期的不可控制
  6.  記下小馬自己的錯誤:

                 之前使用的自定義ListView中适配資料時使用AsyncTask自行開啟線程的,這個比用Thread更危險,因為Thread隻有在run函數不 結束時才出現這種記憶體洩露問題,然而AsyncTask内部的實作機制是運用了線程執行池(ThreadPoolExcutor,要想了解這個類的話大家加下我們的Android開發群五号,因為其它群的存儲空間快滿了,是以隻上傳到五群裡了,看下小馬上傳的Gallery源碼,你會對線程執行池、軟、弱、強引用有個更深入的認識),這個類産生的Thread對象的生命周期是不确定的,是應用程式無法控制的,是以如果AsyncTask作為Activity的内部類,就更容易出現記憶體洩露的問題。這個問題的解決辦法小馬當時網上查到了記在txt裡了,如下: 

    6.1:将線程的内部類,改為靜态内部類。

    6.2:線上程内部采用弱引用儲存Context引用

    示例代碼如下:

  1. public abstract class WeakAsyncTask<Params, Progress, Result, WeakTarget> extends 
  2.             AsyncTask<Params, Progress, Result> { 
  3.         protected WeakReference<WeakTarget> mTarget;   
  4.         public WeakAsyncTask(WeakTarget target) { 
  5.             mTarget = new WeakReference<WeakTarget>(target); 
  6.         }   
  7.         /** {@inheritDoc} */ 
  8.         @Override 
  9.         protected final void onPreExecute() { 
  10.             final WeakTarget target = mTarget.get(); 
  11.             if (target != null) { 
  12.                 this.onPreExecute(target); 
  13.             } 
  14.         protected final Result doInBackground(Params... params) { 
  15.                 return this.doInBackground(target, params); 
  16.             } else { 
  17.                 return null; 
  18.         protected final void onPostExecute(Result result) { 
  19.                 this.onPostExecute(target, result); 
  20.         protected void onPreExecute(WeakTarget target) { 
  21.             // No default action 
  22.         protected abstract Result doInBackground(WeakTarget target, Params... params);   
  23.         protected void onPostExecute(WeakTarget target, Result result) { 
  24.         } 
  1.     }