Android项目:模仿ConvertView原理(ListView的getView方法)对View对象进行回收和复用

简介:

  在项目优化过程中,通过MAT监控发现存在一处内存泄露,反复进入某个页面,内存占用越来越大。后分析找到了泄露原因,原来是在自定义列表中,将行布局的layout文件inflate成view对象的时候,每加载一次列表就要new出一组新的view对象。因为没有对这些布局一致的view进行复用,又没法及时释放,导致了列表的行布局对象越积越多,造成内存泄露。

  解决这个oom问题,首先想到了listview加载中对convertview的回收和复用的方法。于是模仿convertview的原理写了个对view对象进行回收和复用的类,此处类名为ViewRecycler,使用后有效的解决了view对象的复用,远离这个棘手的oom问题。由于项目中需要复用的view对象布局都是一样的,此方法只考虑了复用同一布局的情况。同时,项目中的列表已有获取显示行与隐藏行的相应接口,此方法仅主要从回收与复用的逻辑层面加以实现,并未涉及任何底层代码部分。


一、以下是具体的实现过程:


1.首先ViewRecycler类需要两个容器分别用来保存活动的View对象和回收可复用的View对象。一个map用来保存回收的View(无序添加),一个数组用来记录当前显示的行号以及对应的View(有序添加)。

如下:

1
2
private  SparseArray<View> recycleViews; // 废弃的view
private  View[] activeViews =  new  View[ 0 ]; //正在使用的view


注:key为int类型的HashMap用SparseArray代替,会有更好的性能.


2.每次列表刷新或变化,就更新一次activeViews的大小。即activeViews的数组长度与当前列表的总行数一致。在刷新列表或者加载更多时,调用getCount()方法更新activeViews数组长度。

如下:

1
2
3
4
5
6
// 加载完毕
private  void  loadComplete()
{
     //.....
     mViewRecycler.getCount(mDataList.size());
}


ViewRecycler类里的getCount方法:

1
2
3
4
5
6
7
8
9
10
11
12
     /** 获取活动view的总数 */
     public  void  getCount( int  count)
     {
         final  int  length =  this .activeViews.length;
         if  (count > length)
         {
             final  View[] activeViews =  this .activeViews;
             this .activeViews = Arrays.copyOf(activeViews, count);
                                                                                                                                                                                                                                                                                                                                                                          
//            Log.e("getCount", "activeViews[" + (count - 1) + "]=" + this.activeViews[count-1]);
         }
     }


3.列表每新增显示一行,就先获取是否有可复用的View对象。先判断recycleViews是否已存有该行号对应的View,没有则获取最新回收的View。再结合setTag与getTag便可实现对回收View对象的复用了。如果recycleViews没有可复用的View,则inflate生成新的View。

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public  void  convertFromView( final  ListJson listJson,  final  int  n)
{
     ViewHolder holder =  null ;
                                                                                                                                                                                                                                                                                                                  
     // 判断是否有可重复利用的view
     View resycleView = mViewRecycler.getRecycleView(n);
                                                                                                                                                                                                                                                                                                                  
     if  (resycleView ==  null )
     {
         resycleView = LayoutInflater.from(getActivity()).inflate(R.layout.list_item,  null );
                                                                                                                                                                                                                                                                                                                      
         holder =  new  ViewHolder();
         //.....
         holder.iv_main = (ImageView) resycleView.findViewById(R.id.item_iv_main);
                                                                                                                                                                                                                                                                                                                      
         resycleView.setTag(holder);
     }
     else
     {
         holder = (ViewHolder) resycleView.getTag();
     }
                                                                                                                                                                                                                                                                                                                  
     //保存新增活动的View对象
     mViewRecycler.addActiveView(n, resycleView);
                                                                                                                                                                                                                                                                                                                  
     // 加载行布局数据
     holder.tv_title.setText(lison.getName());
     holder.tv_price.setText( "待定" );
     //.....
                                                                                                                                                                                                                                                                                                                  
     // 下载图片
     ImageLoadingListener listener =  new  ImageLoadingListener()
     {
         @Override
         public  void  onLoadingStarted(String arg0, View arg1)
         {
         }
                                                                                                                                                                                                                                                                                                                      
         @Override
         public  void  onLoadingComplete(String arg0, View arg1, Bitmap bitmap)
         {
             //获取view对象
             View bitmapView = mViewRecycler.getActiveView(n);
             if  (bitmapView !=  null )
             {
                 ViewHolder holder = (ViewHolder) bitmapView.getTag();
                 if  (holder !=  null )
                 {
                     // 加载图片
                     holder.iv_main.setImageBitmap(BitmapFactory.decodeResource(getResources(), R.drawable.bg_item));
                 }
             }
         }
                                                                                                                                                                                                                                                                                                                      
         @Override
         public  void  onLoadingFailed(String arg0, View arg1, FailReason arg2)
         {
         }
                                                                                                                                                                                                                                                                                                                      
         @Override
         public  void  onLoadingCancelled(String arg0, View arg1)
         {
         }
     };
     imageLoader.loadImage(listJson.getStatusPic(), listener);
}


ViewRecycler类里的对应方法:

(1)将行号与对应的View填充到activeViews数组里保存。添加前对行号与activeViews的长度进行校正,避免越界。

1
2
3
4
5
6
7
8
9
10
11
12
     /** 添加记录当前活动的view */
     public  void  addActiveView( int  position, View view)
     {
         final  int  length =  this .activeViews.length;
         if  (position > length -  1 )
         {
             getCount(position +  1 );
         }
         this .activeViews[position] = view;
                                                                                                                                                                                                                                                                                       
//        Log.e("addActiveView", "activeViews.size() = " + Arrays.toString(activeViews));
     }


(2)根据行号获取对应的View对象。

1
2
3
4
5
6
7
8
9
10
/** 获取某个活动view */
public  View getActiveView( int  position)
{
     final  int  length =  this .activeViews.length;
     if  (position > length -  1 )
     {
         getCount(position +  1 );
     }
     return  activeViews[position];
}


(3)获取已回收的View对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/** 获取回收的view */
View getRecycleView( int  position)
{
     return  retrieveFromRecycle(recycleViews, position);
}
/** 检索回收的view */
static  View retrieveFromRecycle(SparseArray<View> recycleViews,  int  position)
{
     int  size = recycleViews.size();
     if  (size >  0 )
     {
         // See if we still have a view for this position.
         for  ( int  i =  0 ; i < size; i++)
         {
             int  fromPosition = recycleViews.keyAt(i);
             View view = recycleViews.get(fromPosition);
             if  (fromPosition == position)
             {
                 recycleViews.remove(fromPosition);
                 return  view;
             }
         }
         int  index = size -  1 ;
         View r = recycleViews.valueAt(index);
         recycleViews.remove(recycleViews.keyAt(index));
         return  r;
     }
     else
     {
         return  null ;
     }
}


4.列表每隐藏一行,将消失的行布局对应的View对象回收,添加到recycleViews容器里,同时移除activeViews里的这个View。然后再进行比较并清除recycleViews容器里的回收对象,保证回收对象总数不多于活动view容器的总长度。

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 隐藏
@Override
public  void  onInvalidateItem( int  id)
{
     super .onInvalidateItem(id);
     // .....
                                                                                                                                                                                          
                                                                                                                                                                                          
     // 回收view对象
     View recycleView = mViewRecycler.getActiveView(id);
     if  (recycleView !=  null )
     {
         mViewRecycler.addRecycleView(id, recycleView);
     }
                                                                                                                                                                                          
}


ViewRecycler类里的对应方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
     /** 添加废弃的view,无序添加 */
     void  addRecycleView( int  position, View scrap)
     {
         recycleViews.put(position, scrap);
                                                                                                                                                                                                      
         final  int  length =  this .activeViews.length;
         if  (position < length)
         {
             this .activeViews[position] =  null ;
         }
         pruneRecycleViews();
//        Log.e("Recycle", "Recycle.size() = " +recycleViews.size());
     }


1
2
3
4
5
6
7
8
9
10
11
12
/** 确保废弃的view总数不多于活动的view容器的长度(此方法可再改进为不多于当前活动的View对象数量) */
private  void  pruneRecycleViews()
{
     final  int  maxViews = activeViews.length;
     int  size = recycleViews.size();
     final  int  extras = size - maxViews;
     size--;
     for  ( int  j =  0 ; j < extras; j++)
     {
         recycleViews.remove(recycleViews.keyAt(size--));
     }
}


5.退出时,清除所有View对象及其引用。

以下方法则是根据项目需求,将所有的活动view移到recycleViews里,再整理recycleViews。

如下:

1
2
3
4
5
6
7
@Override
public  void  onDestroy()
{
     super .onDestroy();
     //销毁所有view对象
     mViewRecycler.recycleAllActiveViews();
}


ViewRecycler类里的对应方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/** 将所有剩余的活动view移到废弃view里 */
void  recycleAllActiveViews()
{
     final  View[] activeViews =  this .activeViews;
     SparseArray<View> recycleViews =  this .recycleViews;
     final  int  count = activeViews.length;
     for  ( int  i = count -  1 ; i >=  0 ; i--)
     {
         final  View victim = activeViews[i];
         if  (victim !=  null )
         {
             activeViews[i] =  null ;
             recycleViews.put(i, victim);
         }
     }
                                                                                                                                                                          
     pruneRecycleViews();
}



二、调试结果

滑动时,请求加载行数据。每次添加一个行布局对象,当达到10行后开始执行View对象回收。每次将栈底的View回收并复用到栈顶的行布局里。如下图:

wKioL1MT8iTS8UokAAbh95qOXfU070.jpg

wKiom1MT8kqy3pXUAAIx3_1C-iQ550.jpg



三、最后附上完整的ViewRecycler类

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62