Android AdapterView View的复用机制 分析

对于ListView、GridView相信大家都不陌生,重写个BaseView,实现对于的几个方法,然后就完成了我们的界面展示,并且在大部分情况下,我们加载特别多的Item也不会发生OOM,大家也都明白内部有缓存机制,都遇到过ItemView复用带来的一些问题,比如异步加载图片,最终造成界面显示的混乱,我们一般会使用setTag,然后回调显示时,避免造成混乱。

设想1:拿ListView为例,如果ListView的ItemView复用机制,所有的ItemView复用同一个,如果在多线程下载图片的情况下,可能最终只有最后一个View显示图片吧,因为你前面的设置setTag(url),后面马上就会将你的Tag的值覆盖掉,最终findViewByTag找到的都是最后一个。由此可见ListView缓存的不是一个,至少是一屏幕可显示的数量。也就是说ListView维护着一个ItemView的池子。

跟大家解释下,为啥缓存了一个屏幕的可显示最大的ItemView数量的池子,我们可能上千个ItemView,仅依靠Tag就能实现不混乱呢。

情景:屏幕每次显示7个Item,ListView一共1000个Item,每个Item上显示一张从网络下载的图片。

getView的代码大概是这样的:

[java]  view plain copy
  1. @Override  
  2.     public View getView(int position, View convertView, ViewGroup parent)  
  3.     {  
  4.         final String url = getItem(position);  
  5.         View view;  
  6.         if (convertView == null)  
  7.         {  
  8.             view = LayoutInflater.from(getContext()).inflate(R.layout.photo_layout, null);  
  9.         } else  
  10.         {  
  11.             view = convertView;  
  12.         }  
  13.         final ImageView photo = (ImageView) view.findViewById(R.id.photo);  
  14.         // 给ImageView设置一个Tag,保证异步加载图片时不会乱序  
  15.         photo.setTag(url);  
  16.         new LoadImgTask(photo).execute(url);  
  17.         return view;  
  18.     }  
下载完成图片,进行photo.getTag().equals(url)来防止图片显示的混乱。

如果我们打开界面,开启了7个线程去下载,此时缓存了这7个ItemView,现在滑动屏幕显示另外下一屏,此时7个ItemView都会复用,会把第一屏设置的Tag全部覆盖掉,没错就是覆盖掉了,又开启7个线程去下载图片,当第一屏的ItemView的图片下载完成后,如果直接findViewByTag然后设置图片会显示在第二屏上,就混乱了,所以一般在显示前都会判断photo.getTag().equals(url);确定了再显示,也就是说第一屏的ItemView图片下载完了,但是Tag被覆盖了,所以即使下载完成了,也不会有任何显示。这就解释了为什么我们防止混乱的代码需要那样去写。

好了,下面从源码角度看一眼ListView内部到底是如何进行缓存的:

跟着ListView,进入父类AbsList,会发现这样一个变量:

[java]  view plain copy
  1. /** 
  2.     * The data set used to store unused views that should be reused during the next layout 
  3.     * to avoid creating new ones 
  4.     */  
  5.    final RecycleBin mRecycler = new RecycleBin();  

注释的意思上用一个数据集来存储应当在下一个布局重用的View,避免重新创建新的布局。这个对象应该就是对我们缓存管理的核心类了。继续看这个类,这是一个内部类:

[java]  view plain copy
  1. /** 
  2.     * The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of 
  3.     * storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the 
  4.     * start of a layout. By construction, they are displaying current information. At the end of 
  5.     * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that 
  6.     * could potentially be used by the adapter to avoid allocating views unnecessarily. 
  7.     * 
  8.     * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener) 
  9.     * @see android.widget.AbsListView.RecyclerListener 
  10.     */  
  11.    class RecycleBin {    private View[] mActiveViews = new View[0];                                                                                                                private ArrayList<View>[] mScrapViews;                                                                                                               <span style="white-space:pre">    </span>  ....                                                                                                                                                 }     
大概意思:这个类是用来帮助在滑动布局时重用View的,RecycleBin包含了两个级别的存储,ActiveViews和ScrapViews,ActiveViews存储的是第一次显示在屏幕上的View;所有的ActiveViews最终都会被移到ScrapViews,ScrapViews存储的是有可能被adapter复用的View。
现在很明确了AbsListView缓存依赖于两个数组,一个数组存储屏幕上当前现实的ItemView,一个显示从屏幕下移除的且可能会被复用的ItemView。下面看ListView里面的代码:

[java]  view plain copy
  1. @Override  
  2.     protected void layoutChildren()   
  3.     {  
  4.         if (dataChanged)   
  5.         {  
  6.             for (int i = 0; i < childCount; i++)   
  7.             {  
  8.                 recycleBin.addScrapView(getChildAt(i));     
  9.             }  
  10.         } else   
  11.         {  
  12.             recycleBin.fillActiveViews(childCount, firstPosition);  
  13.         }  
  14.         ....  
  15.     }  

[java]  view plain copy
  1. /** 
  2. * Fill ActiveViews with all of the children of the AbsListView. 
  3. * 
  4. * @param childCount The minimum number of views mActiveViews should hold 
  5. * @param firstActivePosition The position of the first view that will be stored in 
  6. *        mActiveViews 
  7. */  
  8. void fillActiveViews(int childCount, int firstActivePosition)   
  9. {  
  10.     if (mActiveViews.length < childCount)   
  11.     {  
  12.         mActiveViews = new View[childCount];  
  13.     }  
  14.     mFirstActivePosition = firstActivePosition;  
  15.   
  16.     final View[] activeViews = mActiveViews;  
  17.     for (int i = 0; i < childCount; i++)   
  18.     {  
  19.         View child = getChildAt(i);  
  20.         activeViews[i] = child;   
  21.     }  
  22. }  

可以看出,如果数据发生变化则把当前的ItemView放入ScrapViews中,否则把当前显示的ItemView放入ActiveViews中。那么咱们关键的getView方法到底是在哪调用呢,下面看RecycleBin中的方法:

[java]  view plain copy
  1. /** 
  2.      * Get a view and have it show the data associated with the specified 
  3.      * position. This is called when we have already discovered that the view is 
  4.      * not available for reuse in the recycle bin. The only choices left are 
  5.      * converting an old view or making a new one. 
  6.      * 
  7.      * @param position The position to display 
  8.      * @param isScrap Array of at least 1 boolean, the first entry will become true if 
  9.      *                the returned view was taken from the scrap heap, false if otherwise. 
  10.      *  
  11.      * @return A view displaying the data associated with the specified position 
  12.      */  
  13.     View obtainView(int position, boolean[] isScrap)   
  14.     {  
  15.         isScrap[0] = false;  
  16.         View scrapView;  
  17.         scrapView = mRecycler.getScrapView(position);  
  18.         View child;  
  19.         if (scrapView != null)   
  20.         {  
  21.             
  22.             child = mAdapter.getView(position, scrapView, this);  
  23.             if (child != scrapView)   
  24.             {  
  25.                 mRecycler.addScrapView(scrapView);  
  26.                  
  27.             } else   
  28.             {  
  29.                 isScrap[0] = true;  
  30.                 child.dispatchFinishTemporaryDetach();  
  31.             }  
  32.         } else   
  33.         {  
  34.             child = mAdapter.getView(position, nullthis);           
  35.         }  
  36.   
  37.         return child;  
  38.     }  

可以看到,这个方法就是返回当前一个布局用户当前Item的显示,首先根据position去ScrapView中找,找到后调用我们的getView,此时getView里面的convertView!=null了,然后getView如果返回的View发生变化,缓存下来,否则convertView==null了。

好了,主要是为了让大家了解,AbsListView为什么我们可以通过一个Tag的设置保证其正确的显示,以及缓存机制在AbsListView到底是怎么实现的,鉴于源代码实在太长,只能大概的根据代码了解一下原理。

你可能感兴趣的:(Android AdapterView View的复用机制 分析)