Android-Adapter-View復用機制


前言

相信Android開發者對ListView不會陌生,使用ListView需要設置相應的Adapter才能展示數據。Adapter到底是什么東西?讓我們來一探究竟。

Adapter

圖1

p1.png

通過圖1我們可以看出 Adapter是View和數據之間的橋梁,並為每一個數據項生成相應的View。Adapter是個接口,定義了子類需要實現的方法,最常見的
方法有:

  • getCount(),總共有多少數據項

  • getItem(),獲取對應position 中的item

  • getView(),返回需要展示在屏幕中的View
    一般在自定義Adapter中,只需要實現上述三個方法即可。

Adapter優化前

class ImageAdapter extends BaseAdapter {
        private Context mContext ;
        private String[] mList ;

        public ImageAdapter(Context context, String[] list) {
            mList = list ;
            mContext = context ;
        }

        @Override
        public int getCount() {
            return mList.length;
        }

        @Override
        public Object getItem(int position) {
            return mList[position];
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
               View view = LayoutInflater.from(mContext).inflate(R.layout.gridview_item, parent, false) ;
               TextView tv = view.findViewById(R.id.text) ;
               tv.setText(mList[position]);
            return view;
        }
    }

上述大概就是一個最簡單的Adapter的實現了吧。通過在getView函數中inflate一個新布局,給相應position設置data,然后將view返回給父控件。咋一看沒啥問題,運行也不會有錯。但是會有嚴重的性能問題。想象一下,如果有mList中有100萬條數據,我們有必要每次都重新inflate一個新layout,生成一個新view嗎?顯然是沒有必要的,我們的手機屏幕就那么小,可見的View其實也就那幾個。那些看不見的View其實是沒必要保存的。

View復用原理

圖2

p2.jpg

圖片來源於此 博客。上圖清晰展現了View的復用原理。手機屏幕一共能夠展示七個item,繼續往上滑動,item 1從我們視線消失,此時ListView 會調用Adaper中的getView函數來生成第八個item。此時getView函數中參數convertView就是item 1,我們只需把convertView(item 1)上的數據項全部設置成item 8的數據項即可,這樣就不用再重新inflate一個新View出來了。不管是有mList有多大,內存中保存的View的個數永遠只是可見的幾個。這對程序性能有很大提升。

Adapter優化后

class ImageAdapter extends BaseAdapter {
        private Context mContext ;
        private String[] mList ;

        public ImageAdapter(Context context, String[] list) {
            mList = list ;
            mContext = context ;
        }

        @Override
        public int getCount() {
            return mList.length;
        }

        @Override
        public Object getItem(int position) {
            return mList[position];
        }

        @Override
        public long getItemId(int position) {
            return position;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ViewHolder viewHolder = null ;
            if (convertView == null) {
                viewHolder = new ViewHolder();
                convertView = LayoutInflater.from(mContext).inflate(R.layout.gridview_item, parent, false) ;
                viewHolder.textview = (TextView) convertView.findViewById(R.id.text);
                convertView.setTag(viewHolder);
            } else {
                viewHolder = (ViewHolder) convertView.getTag();
            }
            // 綁定對應position數據
            bindViewData(position, viewHolder) ;

            return convertView;
        }
        
        private void bindViewData(int position, ViewHolder viewHolder) {
            viewHolder.textview.setText(mList[position]);
        }

        private static class ViewHolder {
                public TextView textview ;
        }
    }

以圖2為例,每個item都是由View(視圖)和Data(數據)組成的。前7個item的convertView都是為空的,因此inflate出了7個新View,當第8個item變成可見時此時convertView 為item 1,再通過bindViewData函數把第八個mList中的數據設置給item 1對應的View,然后直接返回convertView。此時我們看到的item 8其實是由這兩個東西組成的:

  • View ------ item 1 的View

  • Data ------ mList 中的第八項

因為item 8和View 和item 1的View結構是一樣的(使用同樣layout),所以不會造成錯誤。

View復用導致的問題

問題1:item的狀態錯亂

圖3

p3.png

如圖3所示,6跟帖這個View不是所有item都有的,還是拿圖2來舉例。假設item 1是圖3的第一項,item8是圖3的第二項。item 1中跟帖這個View是顯示的,因為item 8使用的是item 1的View,所以item 8中跟帖的View的狀態也是顯示的。但是在item 8中跟帖這個View是不應該顯示的。這就是復用View導致item狀態錯亂的問題。解決方法之一就是在bindViewData函數中給layout中的View先全部還原成默認狀態即可。

問題2:多線程導致圖片加載位置錯亂

圖片加載往往涉及到網絡操作,因此ListView中加載圖片時一般都會開啟新線程去加載圖片。如果不使用View復用方法,直接使用優化前的Adapter,是不會有任何問題的。但如果使用的View復用,就不一定了。

 @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ViewHolder viewHolder = null ;
            if (convertView == null) {
                viewHolder = new ViewHolder();
                convertView = LayoutInflater.from(mContext).inflate(R.layout.gridview_item, parent, false) ;
                viewHolder.imageView = (ImageView) convertView.findViewById(R.id.square_image_view);
                convertView.setTag(viewHolder);
            } else {
                viewHolder = (ViewHolder) convertView.getTag();
            }
            String url = mList[position] ;
            ImageView imageView = viewHolder.imageView ;
            ImageLoader.loadBitmap(imageView, url);
            
            return convertView;
        }

還是拿圖2為例。如上述代碼所示,我們使用了復用View方法,並且異步加載圖片。由於網絡比較慢,item 1到item 7的圖片還沒加載出來,我們滑動到了item 8(復用了item 1的ImageView),突然網絡變好了,item 8 的圖片加載完成,我們給Item 8設置了圖片。過了一會,item 1的圖片也下載完成了,我們又給item 1設置了圖片,由於item 1和item 8共用同一個ImageView,因此item 8的圖片立馬變成了item 1的圖片。此時item 8上顯示的竟然是item 1對應的圖片!解決辦法之一就是給ImageView setTag。代碼如下:

 ImageView imageView = viewHolder.imageView ;
 String tag = (String) imageView.getTag();
 String url = mList[position];
 if ( !url.equals(tag) ) {
       //default drawable
        imageView.setImageDrawable(null);
   }
  // set tag to image view
  imageView.setTag(url);
  // load bitmap
  ImageLoader.loadBitmap(imageView, url);

延伸閱讀

關於 ScrapView 和 ActiveView

Performance Tips for Android’s ListView
Handling ListView Recycle on Android


免責聲明!

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



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