华为手机刷微博体验更好?技术角度的分析和思考
The following article is from Android Performance Author Gracker
从技术上讲,滑动列表停止后再加载图片是目前列表滑动优化中一个比较常见的优化项,很多主流 App 也都是这么做的 ,做这种处理主要是因为 如果在列表滑动的时候,碰到图片视频就加载,那么会加载很多无用的图片&&视频,浪费资源不说,还可能会影响真正用户看到的图片的加载速度 (加载一般都有并行上限和队列,队列里面无效的图片太多,后来的图片就得排队等待)。这里比较 特别的就是同一个版本的微博 APK,在华为的机型上与在其他机型上表现不一致,作为一个系统优化工程师,这个还是值得去搞清楚的(大胆猜测是微博针对华为的机型做了优化),那么这个优化的内容是什么?
从用户体验的角度来讲,列表滑动的同时加载图片,用户可以更早地看到图片,减少图片占位白图的显示时间,可以提升滑动的体验
第三个现象就得认真体验才会感觉到:华为手机上的微博在松手后的滑动曲线和其他手机上的微博在松手后的滑动曲线是不一样的,华为的微博列表松手后的滑动曲线速度更慢,更柔和,结束的时候也不会太突兀,与系统默认的列表滑动曲线明显不一样
由于 “列表滑动的同时加载图片” 这个功能由微博官方服务器控制,可以随时开启或者关闭,所以文章中所说的 “同一个版本的微博客户端,安装到其他手机上,在主页列表上下滑动的情况下,则必须要等到滑动停止之后才会加载图片” 这个现象在 “列表滑动的同时加载图片” 这个功能开启后,现象就会变成 “主页列表上下滑动的时候就会加载图片”
在 2020-6 月左右分析这个问题的时候,“列表滑动的同时加载图片” 这个功能还是关闭的,只有华为手机做了优化才有效果,其他手机是 “滑动停止之后才会加载图片”
在 2020-8 月再看这个问题的时候,“列表滑动的同时加载图片” 这个功能在其他手机上已经开启
华为的 PerfSDK 还有效果么?答案是有,具体分析可以看下文,因为有了这个 SDK,不仅对微博有好处(减少图片加载个数),对华为也有好处(提升微博主页列表在华为手机上的滑动体验,即 Fling 曲线优化) ;而粗暴开启 “列表滑动的同时加载图片” 的其他手机,如果性能不足,开启后反而会增加卡顿出现的概率(微博官方应该有性能监控数据可以看到)
反编译的微博版本:10.8.1
华为提供了一个简单的接口打包成 SDK 提供给微博,这个接口可以让微博的列表监听到列表的当前速度(Velocity),在速度高于阈值或者低于阈值的时候,都会及时通知 App
微博拿到这个速度回调之后,就可以根据列表的滑动速度来决定是否要在滑动过程中加载图片,一旦列表的滑动速度低于设定的阈值,就开启图片加载;一旦列表的滑动速度高于设定的阈值,就关闭图片加载
华为检测到这个应用使用了 SDK,就可以将优化过后的滑动曲线应用在这个 App 的列表 Fling 阶段,提升用户体验
现象分析
手指接触屏幕,上下滑动微博主页列表,但是手指 没有离开屏幕 ,这个阶段我们称之为阶段一,技术术语为 SCROLL_STATE_TOUCH_SCROLL
手指上下滑动的时候 离开屏幕 (必须有一个上滑或者下滑的速度),微博列表有了一个惯性,根据惯性的方向继续滑动,这个阶段我们称之为阶段二,技术术语为 SCROLL_STATE_FLING
列表惯性滑动后停止,这 个 阶段我们称之为阶段三 , 技术术语为 SCROLL_STATE_I DLE
阶段一优化
优化前:只要手指不离开屏幕,图片加载功能关闭 优化后:只要手指不离开屏幕,列表就不会滑动太快,这时候图片加载功能开启
阶段二优化
滑动图片加载优化 列表滑动速度太快,这时候图片加载功能关闭 列表滑动速度掉落到一个阈值,图片加载功能开启 优化前:只要列表滑动不停止,图片加载功能关闭 优化后:图片加载功能是否开启取决于当前列表滑动的速度 列表 Fling 曲线优化 优化前:列表滑动的曲线是默认值,滑动时间比较短,停止的时候比较突兀,不柔和 优化后:列表滑动的曲线是华为经过优化的,滑动时间比较长,停止的时候比较柔,不突兀,比较接近 iPhone 的列表滑动曲线
技术分析
阶段一优化的技术分析
当 滑动图片加载优化生效 的时候,如果 State != 2,那么就允许 ImageLoader 加载图片,State 为 2 也就是 SCROLL_STATE_FLING,熟悉列表滑动的同学应该知道,SCROLL_STATE_FLING 就是 滑动列表的时候手指松手后列表继续滑动的那一段 ,叫 fling,毕竟只有 fling 的时候才有 Velocity,松手后会根据这个值的大小计算滑动曲线和滑动时长
当 滑动图片加载优化不生效 的时候,就到了常规的列表滑动优化:即列表停止之后才开始加载图片 :State !=0,0 即 SCROLL_STATE_IDLE
阶段二优化的技术分析
HwPerfonVelocityDownToThreshold : 当速度降低到阈值之后,打开 ImageLoader 的图片加载功能
HwPerfonVelocityUpToThreshold:当速度升高到阈值之后,关闭 ImageLoader 的图片加载功能
其他厂商处理
最后一个问题:滑动点击
列表滑动图片加载的性能考虑
如果用户滑动非常快,比如是想找昨天发的某个微博,那么今天发的所有的带图片的微博在用户滑动的时候是没必要加载的,因为用户的目标不是这些图片,而 App 去加载这些图片,而程序员是不会为用户提前加载你未看到的数据,因为加载过多的数据不仅容易发生数据复用、缓存过多、内存溢出等错误,还会对服务器造成不必要的资源请求。
如果用户滑动非常快,那么图片加载队列势必有许多无效的资源(对这一刻的用户来说),而用户真正想看的图片反而排在了加载队列后面,造成加载速度变慢,也会影响用户的体验
列表滑动监听背景知识
滑动停止:SCROLL_STATE_IDLE 手指在屏幕上滑动:SCROLL_STATE_TOUCH_SCROLL 手指离开屏幕,列表靠惯性继续滑动:SCROLL_STATE_FLING
列表状态变化时的回调 :onScrollStateChanged 列表滑动时候的回调:onScroll
public interface OnScrollListener {
// The view is not scrolling. Note navigating the list using the trackball counts as being in the idle state since these transitions are not animated.
public static int SCROLL_STATE_IDLE = 0;
//The user is scrolling using touch, and their finger is still on the screen
public static int SCROLL_STATE_TOUCH_SCROLL = 1;
//The user had previously been scrolling using touch and had performed a fling. The animation is now coasting to a stop
public static int SCROLL_STATE_FLING = 2;
// Callback method to be invoked while the list view or grid view is being scrolled. If the view is being scrolled, this method will be called before the next frame of the scroll is rendered. In particular, it will be called before any calls to
public void onScrollStateChanged(AbsListView view, int scrollState);
// Callback method to be invoked when the list or grid has been scrolled. This will be called after the scroll has completed
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount);
}
列表滑动状态的变化
TOUCH_SCROLL:手指滑动 List 阶段,但是手指没有离开屏幕,这时候上下滑动都是 TOUCH_SCROLL FLING:手指滑动 List 后抬手到 List 停止的阶段(必须有一个上滑或者下滑的速度,否则不会进入 Fling) IDLE:List 停止阶段
手指滑动列表,停止后松手:IDLE -> TOUCH_SCROLL -> IDLE 手指滑动列表,松手后列表继续滑动,然后停止:IDLE -> TOUCH_SCROLL -> FLING -> IDLE
列表的 Fling 曲线计算
boolean update() {
final long time = AnimationUtils.currentAnimationTimeMillis();
final long currentTime = time - mStartTime;
double distance = 0.0;
switch (mState) {
case SPLINE: { // Fling 状态
final float t = (float) currentTime / mSplineDuration;
final int index = (int) (NB_SAMPLES * t);
float distanceCoef = 1.f;
float velocityCoef = 0.f;
if (index < NB_SAMPLES) {
final float t_inf = (float) index / NB_SAMPLES;
final float t_sup = (float) (index + 1) / NB_SAMPLES;
final float d_inf = SPLINE_POSITION[index];
final float d_sup = SPLINE_POSITION[index + 1];
velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
distanceCoef = d_inf + (t - t_inf) * velocityCoef;
}
distance = distanceCoef * mSplineDistance;
mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f;
break;
}
case BALLISTIC: { // 列表滑到 底 之后的 拉伸阶段
final float t = currentTime / 1000.0f;
mCurrVelocity = mVelocity + mDeceleration * t;
distance = mVelocity * t + mDeceleration * t * t / 2.0f;
break;
}
case CUBIC: { // 列表滑到底拉伸 之后的 回弹阶段
final float t = (float) (currentTime) / mDuration;
final float t2 = t * t;
final float sign = Math.signum(mVelocity);
distance = sign * mOver * (3.0f * t2 - 2.0f * t * t2);
mCurrVelocity = sign * mOver * 6.0f * (- t + t2);
break;
}
}
mCurrentPosition = mStart + (int) Math.round(distance);
return true;
}
厂商应用联合优化
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
推荐阅读:
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!