摘要
Android滚动组件图片加载优化与滚动速度的精确监听,含源码实现。
背景
在Android应用中,ListView / RecyclerView / ScrollView 滚动时,如果有过多图片加载容易导致卡顿,特别是快速滚动时,bindView中大量图片加载操作,会导致系统频繁分配回收内存,不仅消耗大量CPU和网络流量资源,而且极端情况下还会因为内存来不及回收产生OOM。
一种最基本的优化策略是滚动时暂停加载、滚动停止才加载图片。但是这种做法很影响用户体验,用户在慢速滚动时图片完全不会加载。
因此希望实现快速滚动时暂停加载图片,慢速滚动时继续加载图片,从而平衡图片加载和滚动流畅度的体验。
速度计算
速度一般按1s时间内滚动的像素值计算。
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;滚动距离也会有较大偏差。
最后导致计算出来的速度有很大偏差。这可能导致速度在阈值附近波动,频繁暂停、启动图片加载,有可能导致一些性能问题,效果不理想。
平滑滤波
解决上述问题,可以考虑对速度做平滑滤波。例如一种简单的滤波方式如下:
/**
* 平滑后的速度
*/
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,稳定性有了一定的提高。
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输出,可以看出,速度比较平稳的减小。即使不使用平滑滤波和双阈值,也能比较好的实现需要的效果。
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
最后,欢迎扫码关注微信公众号。程序员同行学习交流,聊天交友,国内外名企求职内推(微软 / 小冰 / Amazon / Shopee / Coupang / ATM / 头条 / 拼多多等),可加我微信 jzj2015 进技术群(备注进技术群,并简单自我介绍)。

本文由jzj1993原创,转载请注明来源:https://www.paincker.com/android-scroll-velocity
(标注了原文链接的文章除外)
暂无评论