Android中ListView原理与优化 浅析

来源:互联网 发布:rstudio数据恢复官网 编辑:程序博客网 时间:2024/05/17 07:56

       Android中ListView的优化不管在开发中还是在面试中经常会遇到。这不,LZ在阿里2015实习生招聘面试中就被问到,被虐的灰头土脸,回来赶紧总结一下。

      提到ListView的优化,首先想到的是利用convertView和ViewHolder来优化ListView数据加载,仅仅就这些吗?其实不是的,首先,想要优化ListView就得先了解ListView加载数据原理。


          列表的显示需要三个元素:

         1. ListView: 用来展示列表的View。

         2. 适配器:   用来把数据映射到ListView上

         3. 数据:      具体的将被映射的字符串,图片,或者基本组件。

 

     根据列表的适配器类型,列表分为三种,ArrayAdapter,SimpleAdapter和SimpleCursorAdapter,这三种适配器的使用大家可学习下官网上面的使用或者自行百度谷歌,一堆DEMO!!!其中以ArrayAdapter最为简单,只能展示一行字。SimpleAdapter有最好的扩充性,可以自定义出各种效果。SimpleCursorAdapter可以认为是SimpleAdapter对数据库的简单结合,可以方便的把数据库的内容以列表的形式展示出来。

     在使用ListView和Adapter需要注意:Adapter.getView().

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

     这个方法就是用来获得指定位置要显示的View。官网解释如下:

     Get a View that displays the data at the specified position in the data set. You can either create a View manually or inflate it from an XML layout file.


     系统要绘制ListView了,他首先用getCount()函数得到要绘制的这个列表的长度,然后开始绘制第一行,怎么绘制呢?调用getView()函数。在这个函数里面首先获得一个View(这个看实际情况,如果是一个简单的显示则是View,如果是一个自定义的里面包含很多控件的时候它其实是一个ViewGroup),然后再实例化并设置各个组件及其数据内容并显示它。好了,绘制完这一行了。那 再绘制下一行,直到绘完为止。

     ListView的工作原理如下:

     ListView 针对每个item,要求 adapter “返回一个视图” (getView),也就是说ListView在开始绘制的时候,系统首先调用getCount()函数,根据他的返回值得到ListView的长度,然后根据这个长度,调用getView()一行一行的绘制ListView的每一项。如果你的getCount()返回值是0的话,列表一行都不会显示,如果返回1,就只显示一行。返回几则显示几行。如果我们有几千几万甚至更多的item要显示怎么办?为每个Item创建一个新的View?不可能!!!实际上Android早已经缓存了这些视图.

     ListView中每个item都是通过getView返回并显示的,假如item有很多个,那么重复创建这么多对象来显示显然是不合理。因此,Android提供了Recycler,将没有正在显示的item放进RecycleBin,然后在显示新视图时从RecycleBin中复用这个 View。

     Recycler的工作原理大致如下:

     假设屏幕最多能看到11个item,那么当第1个item滚出屏幕,这个item的View进入RecycleBin中,第12个要出现前,通过 getView从回收站(RecycleBin)中重用这个View,然后设置数据,而不必重新  创建一个View。

     大家可以看下下面这个截图来理解下,这个图是解释ListView工作原理的最经典的图了大家可以收藏下,不懂的时候拿来看看,加深理解,其实Android中有个叫做Recycler的构件,顺带列举下与Recycler相关的已经由Google做过N多优化过的东东比如:AbsListView.RecyclerListener、ViewDebug.RecyclerTraceType等等,要了解的朋友自己查下,不难理解,下图是ListView加载数据的工作原理


 下面简单说下上图的原理:

  1. 如果你有几千几万甚至更多的选项(item)时,其中只有可见的项目存在内存(内存内存哦,说的优化就是说在内存中的优化!)中,其他的在Recycler中;
  2. ListView先请求一个type1视图(getView)然后请求其他可见的项目。convertView在getView中是空(null)的;
  3. 当item1滚出屏幕,并且一个新的项目从屏幕低端上来时,ListView再请求一个type1视图。convertView此时不是空值了,它的值是item1。你只需设定新的数据然后返回convertView,不必重新创建一个视图。

    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;         }     } 

执行程序,查看日志:

 

getView 被调用 9 次 ,convertView 对于所有的可见项目是空值(如下):

 

然后稍微向下滚动ListView,直到item10出现:

 

convertView仍然是空值,因为recycler中没有视图(item1的边缘仍然可见,在顶端)再滚动列表,继续滚动:

 

convertView不是空值了!item1离开屏幕到Recycler中去了,然后item11被创建(复用的是item1的view),再滚动下:

 


网上有个直观的效果图很好的解释了这个问题:

==============假设屏幕最多能看到11个item:

效果图:

 

可以看到,一打开Activity,看到10个item.

我们看看Log信息:

 

可以看出,每次convertView都是null, 都是新建一个View来显示的。

当我们向下滑动,如下图,

 

 

由于item0和item10都显示一半,所以item10也是新建出来,但是当要显示item11的时候,由于item0已经不在屏幕上,所以item11复用了item0的实例。可以从以下Log信息看出:

我们分析Log信息,可以看出item11的对象是item0, item12的对象是item1,如此类推。后续的item22会复用滑出去的item11的view,依次类推。

这样,通过复用convertView,就可以避免每次都新建View,节省内存而且优化ListView的滑动效果。



结合以上原理,研究下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了,如果数据太多的话,出现最最恶心的错,手机卡死或强制关机,所以单单使用清除缓存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),这个类产生的Thread对象的生命周期是不确定的,是应用程序无法控制的,因此如果AsyncTask作为Activity的内部类,就更容易出现内存泄露的问题。这个问题的解决办法如下: 
    6.1:将线程的内部类,改为静态内部类。
    6.2:在线程内部采用弱引用保存Context引用。

示例代码如下:

public abstract class WeakAsyncTask<Params, Progress, Result, WeakTarget> extends             AsyncTask<Params, Progress, Result> {         protected WeakReference<WeakTarget> mTarget;            public WeakAsyncTask(WeakTarget target) {             mTarget = new WeakReference<WeakTarget>(target);         }            /** {@inheritDoc} */         @Override         protected final void onPreExecute() {             final WeakTarget target = mTarget.get();             if (target != null) {                 this.onPreExecute(target);             }         }            /** {@inheritDoc} */         @Override         protected final Result doInBackground(Params... params) {             final WeakTarget target = mTarget.get();             if (target != null) {                 return this.doInBackground(target, params);             } else {                 return null;             }         }            /** {@inheritDoc} */         @Override         protected final void onPostExecute(Result result) {             final WeakTarget target = mTarget.get();             if (target != null) {                 this.onPostExecute(target, result);             }         }            protected void onPreExecute(WeakTarget target) {             // No default action         }            protected abstract Result doInBackground(WeakTarget target, Params... params);            protected void onPostExecute(WeakTarget target, Result result) {             // No default action         } }

引用:

http://android.amberfog.com/?p=296

http://haking.iteye.com/blog/1147404

http://www.cnblogs.com/itstudent/p/3729117.html(经典)



0 0