Android滚动组件图片加载优化与滚动速度的精确监听

背景

在Android应用中,ListView / RecyclerView / ScrollView 滚动时,如果有过多图片加载容易导致卡顿,特别是快速滚动时,bindView中大量图片加载操作,会导致系统频繁分配回收内存,不仅消耗大量CPU和网络流量资源,而且极端情况下还会因为内存来不及回收产生OOM。

一种最基本的优化策略是滚动时暂停加载、滚动停止才加载图片。但是这种做法很影响用户体验,用户在慢速滚动时图片完全不会加载。

因此希望实现快速滚动时暂停加载图片,慢速滚动时继续加载图片,从而平衡图片加载和滚动流畅度的体验。

速度计算

速度一般按1s时间内滚动的像素值计算。

1
V = diffPixels * 1000 / t_ms

滚动距离精确监听

计算速度需要知道滚动距离和时间,时间很容易计算,滚动距离相对复杂一点。

ScrollView获取滚动距离

ScrollView可以通过View.OnScrollChangeListener获取滚动距离,高版本系统可以直接调用View.setOnScrollChangeListener设置,低版本系统覆写View的onScrollChanged方法即可。

还可以参考:ScrollView滚动事件和滚动状态的监听实现

RecyclerView获取滚动距离

RecyclerView的OnScrollListener可以直接获取滚动像素值,不需要特殊处理。

ListView滚动距离的精确监听

ListView的滚动使用的不是基类View提供的滚动机制,因此不能使用View提供的onScrollChanged方法监听滚动的像素值;而ListView的OnScrollListener只能监听滚动状态、滚动到第几个Item,也不能直接取到滚动像素值。

方案1:近似实现

监听单位时间内滚动的Item数量。

存在的问题:某些Item特别长或者特别短,会导致很大的误差。例如ListView的Header可能会包含超过1屏的内容。

方案2:精确获取滚动距离

如果连续两次回调onScroll,firstVisibleItem都是同一个,则通过第一个可见View的getTop之差,就可以知道滚动距离。

如果两次firstVisibleItem差1,可在每次回调时记录下第一个、第二个可见View的Top,然后两次对同一个View的Top求差,即为滚动距离。

对于两次firstVisibleItem相差超过1的情况,即一帧时间内,滚动的距离超过了一个Item。通常是由于Item特别短,此时可以考虑丢弃数据。

速度抖动的解决

获取到滚动像素后,计算出时间,就可以计算速度了。实际使用ListView进行了尝试。

在Android开启硬件加速、不卡顿的情况下,通常每次调用onScroll的时间间隔约为16.7ms(FPS=60)。

实际测试发现,在ListView中Item布局较为复杂的情况下,可能发生卡顿,特别是在getView复用Item的时候。卡顿时会出现某些帧时间间隔偏差很大,例如只有不到10ms;滚动距离也会有较大偏差。

最后导致计算出来的速度有很大偏差。这可能导致速度在阈值附近波动,频繁暂停、启动图片加载,有可能导致一些性能问题,效果不理想。

平滑滤波

解决上述问题,可以考虑对速度做平滑滤波。例如一种简单的滤波方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 平滑后的速度
*/
private int mSmoothedVelocity = 0;

/**
* 速度变化
*/
public void onVelocityChanged(int velocity) {
L.d("VelocityTracker", "onVelocityChanged, velocity = %d", velocity);
final int smoothedVelocity = mSmoothedVelocity * 4 / 5 + velocity / 5;
if (smoothedVelocity != mSmoothedVelocity) {
mSmoothedVelocity = smoothedVelocity;
onSmoothedVelocityChanged(mSmoothedVelocity);
}
}

/**
* 平滑处理后的速度变化
*/
public void onSmoothedVelocityChanged(int velocity) {
L.d("VelocityTracker", "onSmoothedVelocityChanged, velocity = %d", velocity);
}

平滑前后的Log如下(启动滚动时的Log)。可以看出中间有几帧发生卡顿,原始速度从7000多减小到了1000,而平滑滤波后的速度,只是从3000多减小到2900,稳定性有了一定的提高。

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
D/VelocityTracker: diff = 150
D/VelocityTracker: onScrollBy, diff = 150, ms = 21
D/VelocityTracker: onVelocityChanged, velocity = 7142
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 2384
D/VelocityTracker: diff = 125
D/VelocityTracker: onScrollBy, diff = 125, ms = 16
D/VelocityTracker: onVelocityChanged, velocity = 7812
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 3469
D/VelocityTracker: onReachThreshold, reach = true
D/VelocityTracker: diff = 37
D/VelocityTracker: onScrollBy, diff = 37, ms = 12
D/VelocityTracker: onVelocityChanged, velocity = 3083
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 3391
D/VelocityTracker: diff = 7
D/VelocityTracker: onScrollBy, diff = 7, ms = 7
D/VelocityTracker: onVelocityChanged, velocity = 1000
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 2912
D/VelocityTracker: onReachThreshold, reach = false
D/VelocityTracker: diff = 96
D/VelocityTracker: onScrollBy, diff = 96, ms = 13
D/VelocityTracker: onVelocityChanged, velocity = 7384
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 3805
D/VelocityTracker: onReachThreshold, reach = true
D/VelocityTracker: diff = 117
D/VelocityTracker: onScrollBy, diff = 117, ms = 16
D/VelocityTracker: onVelocityChanged, velocity = 7312
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 4506

双阈值

用前面的平滑滤波,代码中final int smoothedVelocity = mSmoothedVelocity * 4 / 5 + velocity / 5,每次会把新速度的1/5和平滑速度的4/5相加。

这里的1/5如果取得太小,会导致平滑后的速度延迟很大;如果取得太大,则平滑效果不理想,速度波动仍然会比较大。

为了避免速度在固定阈值上下来回波动,可以使用双阈值的方式处理,例如速度下降到2000则启动图片加载,而上升到2500才暂停图片加载。当速度在2000~2500之间波动时,并不会反复切换图片加载。

延长采样周期

每一帧回调onScroll方法时都采样和计算速度,容易导致较大的速度抖动。采用了前面的平滑滤波、双阈值方法,效果还是不理想,尝试使用延长采样周期的方式处理。

实现思路是,每一帧都计算滚动距离并累加,但每8帧才做一次时间采样和速度计算。8帧会持续约0.13s,这样只要不出现连续很久的卡顿,速度的计算就是比较准确的。

下面是延长采样周期后,一次完整的滚动Log输出,可以看出,速度比较平稳的减小。即使不使用平滑滤波和双阈值,也能比较好的实现需要的效果。

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
D/VelocityTracker: count = 0, diff = -2147483648, mDiff = 0
D/VelocityTracker: count = 1, diff = 39
D/VelocityTracker: count = 2, diff = 45
D/VelocityTracker: count = 3, diff = 312
D/VelocityTracker: count = 4, diff = 0
D/VelocityTracker: count = 5, diff = 79
D/VelocityTracker: count = 6, diff = 97
D/VelocityTracker: count = 7, diff = 102
D/VelocityTracker: onScrollBy, diff = 776, ms = 152
D/VelocityTracker: onVelocityChanged, velocity = 5105
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 1021
D/VelocityTracker: count = 0, diff = 102
D/VelocityTracker: count = 1, diff = 96
D/VelocityTracker: count = 2, diff = 106
D/VelocityTracker: count = 3, diff = 94
D/VelocityTracker: count = 4, diff = 98
D/VelocityTracker: count = 5, diff = 90
D/VelocityTracker: count = 6, diff = 95
D/VelocityTracker: count = 7, diff = 119
D/VelocityTracker: onScrollBy, diff = 767, ms = 135
D/VelocityTracker: onVelocityChanged, velocity = 5681
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 1952
D/VelocityTracker: count = 0, diff = 69
D/VelocityTracker: count = 1, diff = 77
D/VelocityTracker: count = 2, diff = 85
D/VelocityTracker: count = 3, diff = 77
D/VelocityTracker: count = 4, diff = 75
D/VelocityTracker: count = 5, diff = 76
D/VelocityTracker: count = 6, diff = 73
D/VelocityTracker: count = 7, diff = 66
D/VelocityTracker: onScrollBy, diff = 596, ms = 131
D/VelocityTracker: onVelocityChanged, velocity = 4549
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 2470
D/VelocityTracker: count = 0, diff = 67
D/VelocityTracker: count = 1, diff = 64
D/VelocityTracker: count = 2, diff = 58
D/VelocityTracker: count = 3, diff = 58
D/VelocityTracker: count = 4, diff = 55
D/VelocityTracker: count = 5, diff = 49
D/VelocityTracker: count = 6, diff = 50
D/VelocityTracker: count = 7, diff = 45
D/VelocityTracker: onScrollBy, diff = 424, ms = 133
D/VelocityTracker: onVelocityChanged, velocity = 3187
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 2613
D/VelocityTracker: count = 0, diff = 45
D/VelocityTracker: count = 1, diff = 43
D/VelocityTracker: count = 2, diff = 38
D/VelocityTracker: count = 3, diff = 39
D/VelocityTracker: count = 4, diff = 34
D/VelocityTracker: count = 5, diff = 43
D/VelocityTracker: count = 6, diff = 27
D/VelocityTracker: count = 7, diff = 28
D/VelocityTracker: onScrollBy, diff = 282, ms = 134
D/VelocityTracker: onVelocityChanged, velocity = 2104
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 2510
D/VelocityTracker: count = 0, diff = 30
D/VelocityTracker: count = 1, diff = 28
D/VelocityTracker: count = 2, diff = 29
D/VelocityTracker: count = 3, diff = 24
D/VelocityTracker: count = 4, diff = 23
D/VelocityTracker: count = 5, diff = 22
D/VelocityTracker: count = 6, diff = 23
D/VelocityTracker: count = 7, diff = 22
D/VelocityTracker: onScrollBy, diff = 189, ms = 132
D/VelocityTracker: onVelocityChanged, velocity = 1431
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 2294
D/VelocityTracker: count = 0, diff = 18
D/VelocityTracker: count = 1, diff = 20
D/VelocityTracker: count = 2, diff = 17
D/VelocityTracker: count = 3, diff = 18
D/VelocityTracker: count = 4, diff = 16
D/VelocityTracker: count = 5, diff = 16
D/VelocityTracker: count = 6, diff = 20
D/VelocityTracker: count = 7, diff = 11
D/VelocityTracker: onScrollBy, diff = 130, ms = 133
D/VelocityTracker: onVelocityChanged, velocity = 977
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 2030
D/VelocityTracker: count = 0, diff = 12
D/VelocityTracker: count = 1, diff = 13
D/VelocityTracker: count = 2, diff = 14
D/VelocityTracker: count = 3, diff = 12
D/VelocityTracker: count = 4, diff = 11
D/VelocityTracker: count = 5, diff = 11
D/VelocityTracker: count = 6, diff = 11
D/VelocityTracker: count = 7, diff = 10
D/VelocityTracker: onScrollBy, diff = 92, ms = 133
D/VelocityTracker: onVelocityChanged, velocity = 691
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 1762
D/VelocityTracker: count = 0, diff = 10
D/VelocityTracker: count = 1, diff = 10
D/VelocityTracker: count = 2, diff = 8
D/VelocityTracker: count = 3, diff = 9
D/VelocityTracker: count = 4, diff = 8
D/VelocityTracker: count = 5, diff = 8
D/VelocityTracker: count = 6, diff = 8
D/VelocityTracker: count = 7, diff = 7
D/VelocityTracker: onScrollBy, diff = 65, ms = 133
D/VelocityTracker: onVelocityChanged, velocity = 488
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 1506
D/VelocityTracker: count = 0, diff = 7
D/VelocityTracker: count = 1, diff = 6
D/VelocityTracker: count = 2, diff = 7
D/VelocityTracker: count = 3, diff = 6
D/VelocityTracker: count = 4, diff = 6
D/VelocityTracker: count = 5, diff = 6
D/VelocityTracker: count = 6, diff = 5
D/VelocityTracker: count = 7, diff = 5
D/VelocityTracker: onScrollBy, diff = 46, ms = 133
D/VelocityTracker: onVelocityChanged, velocity = 345
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 1273
D/VelocityTracker: count = 0, diff = 5
D/VelocityTracker: count = 1, diff = 5
D/VelocityTracker: count = 2, diff = 5
D/VelocityTracker: count = 3, diff = 4
D/VelocityTracker: count = 4, diff = 4
D/VelocityTracker: count = 5, diff = 4
D/VelocityTracker: count = 6, diff = 4
D/VelocityTracker: count = 7, diff = 4
D/VelocityTracker: onScrollBy, diff = 33, ms = 134
D/VelocityTracker: onVelocityChanged, velocity = 246
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 1067
D/VelocityTracker: count = 0, diff = 3
D/VelocityTracker: count = 1, diff = 3
D/VelocityTracker: count = 2, diff = 4
D/VelocityTracker: count = 3, diff = 3
D/VelocityTracker: count = 4, diff = 3
D/VelocityTracker: count = 5, diff = 2
D/VelocityTracker: count = 6, diff = 3
D/VelocityTracker: count = 7, diff = 2
D/VelocityTracker: onScrollBy, diff = 23, ms = 132
D/VelocityTracker: onVelocityChanged, velocity = 174
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 887
D/VelocityTracker: count = 0, diff = 3
D/VelocityTracker: count = 1, diff = 2
D/VelocityTracker: count = 2, diff = 2
D/VelocityTracker: count = 3, diff = 2
D/VelocityTracker: count = 4, diff = 2
D/VelocityTracker: count = 5, diff = 2
D/VelocityTracker: count = 6, diff = 2
D/VelocityTracker: count = 7, diff = 1
D/VelocityTracker: onScrollBy, diff = 15, ms = 133
D/VelocityTracker: onVelocityChanged, velocity = 112
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 731
D/VelocityTracker: count = 0, diff = 2
D/VelocityTracker: count = 1, diff = 1
D/VelocityTracker: count = 2, diff = 2
D/VelocityTracker: count = 3, diff = 1
D/VelocityTracker: count = 4, diff = 1
D/VelocityTracker: count = 5, diff = 1
D/VelocityTracker: count = 6, diff = 1
D/VelocityTracker: count = 7, diff = 1
D/VelocityTracker: onScrollBy, diff = 9, ms = 133
D/VelocityTracker: onVelocityChanged, velocity = 67
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 597
D/VelocityTracker: count = 0, diff = 1
D/VelocityTracker: count = 1, diff = 1
D/VelocityTracker: count = 2, diff = 0
D/VelocityTracker: count = 3, diff = 1
D/VelocityTracker: count = 4, diff = 1
D/VelocityTracker: count = 5, diff = 0
D/VelocityTracker: count = 6, diff = 0
D/VelocityTracker: count = 7, diff = 1
D/VelocityTracker: onScrollBy, diff = 4, ms = 133
D/VelocityTracker: onVelocityChanged, velocity = 30
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 483
D/VelocityTracker: count = 0, diff = 0
D/VelocityTracker: count = 1, diff = 0
D/VelocityTracker: count = 2, diff = 1
D/VelocityTracker: count = 3, diff = 0
D/VelocityTracker: count = 4, diff = 0
D/VelocityTracker: count = 5, diff = 0
D/VelocityTracker: count = 6, diff = 0
D/VelocityTracker: count = 7, diff = 0
D/VelocityTracker: onSmoothedVelocityChanged, velocity = 0
D/VelocityTracker: onVelocityChanged, velocity = 0

结论

经过尝试,最后确定同时使用延长采样周期、双阈值两种方法,比较好的解决了速度抖动的问题。

代码实现

将时间采样、速度计算、阈值处理相关的逻辑,放在一个单独的类ScrollVelocityTracker里,ScrollView、ListView、RecyclerView的监听器分别调用这个类,每次传入位移像素即可。最后在回调中,可以设置图片库暂停、继续加载,从而优化图片加载性能。

完整的代码实现和Demo示例在此:

https://github.com/jzj1993/AndroidPlayground/tree/master/app/src/scrollvelocity