天天看點

ListView終極優化方法,絕對流暢

     listview可以說是Android開發中最常見的UI控件了,listview能夠以清單的方式顯示大量同類的資料,這樣問題就産生了,既然是大量資料,就會使用到很多布局,給布局綁定資料,listview将占用大量資源還可能會産生卡頓現象。      listview現在最常用也擁有很好的性能的優化方式是在Adapter中使用靜态的ViewHolder,具體代碼如下:        Activity       private  TestAdapter mAdapter;

     private  String[] mArrData;

     private  TextView mTV;

    @Override

     protected   void  onCreate(Bundle savedInstanceState) {

         super .onCreate(savedInstanceState);

        setContentView(R.layout.main);

        mTV  =  (TextView) findViewById(R.id.tvShow);

        mArrData  =   new  String[ 1000 ];

         for  ( int  i  =   0 ; i  <   1000 ; i ++ ) {

            mArrData[i]  =   " Google IO Adapter " + i ;

        }

        mAdapter  =   new  TestAdapter( this , mArrData);

        ((ListView) findViewById(android.R.id.list)).setAdapter(mAdapter);

    }          Adapter

       private   int  count  =   0 ;      private   long  sum  =   0L ;

        @Override

         public  View getView( int  position, View convertView, ViewGroup parent) {

             //  開始計時

             long  startTime  =  System.nanoTime();

            ViewHolder holder;

             if  (convertView  ==   null ) {

                convertView  =  mInflater.inflate(R.layout.list_item_icon_text,

                         null );

                holder  =   new  ViewHolder();

                holder.icon1  =  (ImageView) convertView.findViewById(R.id.icon1);

                holder.text1  =  (TextView) convertView.findViewById(R.id.text1);

                holder.icon2  =  (ImageView) convertView.findViewById(R.id.icon2);

                holder.text2  =  (TextView) convertView.findViewById(R.id.text2);

                convertView.setTag(holder);

            }

             else {

                holder  =  (ViewHolder)convertView.getTag();

            }

            holder.icon1.setImageResource(R.drawable.icon);

            holder.text1.setText(mData[position]);

            holder.icon2 .setImageResource(R.drawable.icon);

            holder.text2.setText(mData[position]);

             //  停止計時

             long  endTime  =  System.nanoTime();

             //  計算耗時

             long  val  =  (endTime  -  startTime)  /   1000L ;

            Log.e( " Test " ,  " Position: "   +  position  +   " : "   +  val);

             if  (count  <   100 ) {

                 if  (val  <   1000L ) {

                    sum  +=  val;

                    count ++ ;

                }

            }  else

                mTV.setText(String.valueOf(sum  /   100L )); //  顯示統計結果

             return  convertView;

        }

    }

     static   class  ViewHolder {

        TextView text1;

        ImageView icon1;

        TextView text2;

        ImageView icon2;

    }

        在Adapter的代碼中,在getView方法裡首先判斷convertView是否為空,若為空則加載相應布局,若不為空則 直接使用該布局,這能夠很有效的使用Android為listview提供的緩存機制:隻加載一屏的布局,之後滑動出來的item使用的是之前已經加載的布局的緩存;        而使用靜态的ViewHoulder的目的則是節省了findViewById的時間。如果不使用ViewHolder,每次getView的時候都需要得到一次子布局,而這也是很耗時并且耗資源的;如果使用了ViewHolder作為子布局的緩存,使用View的setTag方法将緩存與每個item綁定,則也可以省去了findViewById的事件;而将ViewHolder設定為static的目的是指在初始化Adapter時初始化一次這個内部類,否則将會在每次建立Adapter時都要初始化一次,而這是沒有必要的。

      上述方法能夠解決大部分listview消耗資源以及卡頓的問題,但對于不同的需求的listview來說還會存在其他讓listview卡頓的原因,比如listview的item每次加載時都需要獲得圖檔并設定到imageview中,item加載時需要進行大量的計算,item裡的TextView需要設定指定字型;這些耗時的操作都會讓listview滑動起來很卡,帶來不好的體驗;           這幾天我一直在研究Android4.0+系統聯系人的源碼,因為我發現,系統聯系人采用的也是listview布局,每一個item都有圖檔文字,而且使用了fastscroller模式,對聯系人以首字母進行了分來,同時Adapter還實作了SectionIndexer接口,能夠實作這種效果; 但是卻一點都不卡,而我自己做的一個類似的界面,卻十分卡頓,很影響使用者體驗;于是我找來了Android體統聯系人的源碼,自己建立起來了一個可以運作的程式,然後去研究它是如何實作這麼流暢的listview的;

       經過研究我發現系統聯系人的listview使用的是自定義的listview:PinnedHeaderListView 它的定義是這樣的:  

       它給listview加了一個header即顯示在聯系人界面最上方貼着螢幕頂部的那部分,同時它繼承自AutoScrollListView 而這個AutoScrollListView繼承自listview,對listview進行了一些優化,讓listview在互動到指定的item時更流暢;

       看到這裡,于是我使用了這個AutoScrollListView ,但效果不是很明顯,listview還是很卡;在系統聯系人使用的listview中沒有找到解決方法,我開始研究它的Adapter;研究Adapter發現,系統的Adapter進行了很多層的封裝,完全淡化了geiView方法;使用了很多AsyncTask來加載不同的資料,然後使用了CursorLoader來将加載好的資料添加到Adapter裡,同時還将圖檔的加載與資料的加載進行了分離,代碼邏輯十分複雜;但通過這個我得出了一個結論:不要将任何的耗時操作放在listview的getView方法裡。

       在系統聯系人的這些Adapter裡我發現getView方法中沒有任何的耗時操作,在設定圖檔時圖檔已經得到,對清單按照字母進行的分類也已經分類好了,存放在一個内部類裡;在得出這個結論之前我嘗試過很多系統聯系人中的代碼,但都沒有得到明顯的效果,經過大量的測試我得出了這個結論,并且在測試中得到了驗證。

        在我的項目中,listview的每一個item都有一個圖檔,和很多TextView,而且所有的TextView都要設定非系統的字型;Adapter使用的是ViewHolder優化,在getView中的代碼已經很少了,但是還是卡;我的listview中的資料是一個對象的List,在對象裡隻存放了item需要展示的圖檔的資源ID,或者是圖檔的路徑,需要通過一些操作才能獲得圖檔,而這些操作其實是很耗時的;于是我将原來的對象進行了更改,将圖檔對象直接存放在item對應的對象中,然後再Adapter初始化的時候将這些對象初始化,雖然listview展示所需的時間稍微長了一點,但是結果是listview滑動流暢了很多;接着我又将從assets中獲得字型TypeFace的操作放在了Adapter初始化的方法中,并且将字型通過靜态的變量都存起來,然後再getView中隻需為TextView設定一下taptface即可,不需要在從asset中擷取字型所花費的時間;通過上面兩步操作之後,我的listview一點都不卡了,十分流暢(将圖檔放在List的每個資料項裡雖然能解決問題,但不是一個很好的解決方案,後來我使用了記憶體緩存加上異步加載圖檔的方式也保證了清單的流暢)。

        我這裡提供我使用的記憶體緩存的一個實作:

public class MemoryCache {

    private static final String TAG = "MemoryCache";
    private Map<String, Bitmap> cache=Collections.synchronizedMap(
            new LinkedHashMap<String, Bitmap>(10,1.5f,true));//Last argument true for LRU ordering
    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()/4);
    }
    
    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;
            //NullPointerException sometimes happen here http://code.google.com/p/osmdroid/issues/detail?id=78 
            return cache.get(id);
        }catch(NullPointerException ex){
            ex.printStackTrace();
            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();//least recently accessed item will be the first one iterated  
            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() {
        try{
            //NullPointerException sometimes happen here http://code.google.com/p/osmdroid/issues/detail?id=78 
            cache.clear();
            size=0;
        }catch(NullPointerException ex){
            ex.printStackTrace();
        }
    }

    long getSizeInBytes(Bitmap bitmap) {
        if(bitmap==null)
            return 0;
        return bitmap.getRowBytes() * bitmap.getHeight();
    }
}
           

        綜上,listview的優化其實就是去找getView中的耗時操作,然後提取出來,要麼使用異步的方式為item的布局設定資料,要是實在需要同步,就隻能在Adapter初始化時将資料準備好,然後再getView中隻需綁定一下就行。