Android性能优化-ListView自适应性能问题( 二 )


D、继续测量3、4、5…项,重复C 。

Android性能优化-ListView自适应性能问题

文章插图
所以,我们log中的情况是position=0,convertView=null,而position 1,2 … convertView都是同一个对象实例,即被复用第0项 。
2.2 Layout
当Measure过程结束了,下面就要开始Layout过程了,由于onLayout方法代码较多,我们直接pass,来看makeAndAddView方法,也就是真真创建View的代码 。
Android性能优化-ListView自适应性能问题

文章插图
同样的,子View实例都是由obtainView方法返回的 。这时候就有个小细节了,由于前面Measure的时候,第0项的View已经创建了并且加入到了复用缓存当中,这一次就可以直接拿出来继续用了 。接着创建第1,2 … 后面项的时候就没复用缓存了,只能一次次地Inflate 。
所以,我们log中的情况是position=0,convertView复用第0项,而position 1,2 … convertView=null 。
按理说,Layout之后,应该就不会在调用getView方法了,但是我们明显能看到log仍然多了5次调用,那么这又是怎么回事呢?
前面说到onMeasure方法会导致getView调用,而一个View的onMeasure方法调用时机并不是由自身决定,而是由其父视图来决定 。
ListView放在FrameLayout和RelativeLayout中其onMeasure方法的调用次数是完全不同的 。
2.3 小结
由于onMeasure方法会多次被调用,例子中是两次,其实完整的调用顺序是onMeasure - onLayout - onMeasure - onLayout - onDraw 。所以我们又会看到5次调用,和最前面5次是一模一样的 。
那么,肯定有童鞋又要问,既然onLayout也被执行两次,那为何不是调用5x2+5x2=20次呢?
在第2次onLayout的时候,由于数据并没有变化,即mDataChanged=false,这时候可以直接用当前项已经存在的View了,不要再通过getView方法重新绑定数据,所以getView是不需要被调用的 。
从上面的分析中,我们可以得到wrap_content情况下getView被调用的时机和次数,假设onMeasure(heightMeasureSpec为AT_MOST)次数为n,onLayout次数为m,ListView控件内同时显示的子项数为i,那么getView次数=(n + 1)_i,正常情况match_parent时,getView次数= i,多余的getView调用次数应该是 (n + 1)_i - i = n * i;
由公式可以看出getView多余调用次数与onMeasure次数n以及显示子项数i成正比关系 。
3 三大基础布局性能比较
1层嵌套:
A = FrameLayout
View onMeasure 2次 onLayout 2次 onDraw 1次
A = LinearLayout
View onMeasure 2次 onLayout 2次 onDraw 1次
A = RelativeLayout
View onMeasure 4次 onLayout 2次 onDraw 1次
2层嵌套:
A = FrameLayout
View onMeasure 2次 onLayout 2次 onDraw 1次
A = LinearLayout
View onMeasure 2次 onLayout 2次 onDraw 1次
A = RelativeLayout
View onMeasure 8次 onLayout 2次 onDraw 1次
3层嵌套:
A = FrameLayout
View onMeasure 2次 onLayout 2次 onDraw 1次
A = LinearLayout
View onMeasure 2次 onLayout 2次 onDraw 1次
A = RelativeLayout
View onMeasure 16次 onLayout 2次 onDraw 1次
4层嵌套:
A = FrameLayout
View onMeasure 2次 onLayout 2次 onDraw 1次
A = LinearLayout
View onMeasure 2次 onLayout 2次 onDraw 1次
A = RelativeLayout
View onMeasure 32次 onLayout 2次 onDraw 1次
从上面逻辑可以看出,RelativeLayout会导致子View的onMeasure重复调用,假设嵌套层数为n,子View的onMeasure次数为2^(n+1),如果onMeasure中做了复杂逻辑,将会容易导致卡顿 。
另外,如果上面的子View是ListView,且如果高度设置为wrap_content,恰好一屏幕的item个数是m,那么其adapter的getView方法调用次数=(2^n+1)* m 。假设n=4,m=10,getView=170次!170次!170次!(为何会这样,下回合分解,有时间的可以先去玩下,^-^)
所以,三大布局对子View的影响排名应该是:
LinearLayout = FrameLayout >> RelativeLayout
4 常见错误
4.1 常见错误1
比如4层嵌套的RelativeLayout会使得子View的onMeasure次数达到32,其中heightMeasureSpec为AT_MOST的次数为16,所以如果ListView同时显示的项数为10,那么getView的次数达到(16+1)_10=170次,虽然只有10项,但是却相当于一次性加载了170项,性能损耗之大可想而知 。
可以总结出一个公式:如果RelativeLayout嵌套层数为n,ListView显示项数为m,getView调用次数为(2^n+1)_m
4.2 常见错误2
从官方的设计来看,ListView其实是禁止防止在ScrollView等垂直滚动视图中的,但无奈各种各样的业务和设计导致我们不得不这么做,然后就衍生出了可谓ListView历史上最大的坑:NoScrollListView 。


推荐阅读