EN

华为手机刷微博体验更好?技术角度的一些分析和思考

字数统计: 4.6k阅读时长: 16 min
2020/08/20
loading

技术群里的小伙伴发了一条微博, https://weibo.com/1808884742/IApbpEVQr, 博主 @王波粒 发现, Mate 30 Pro 有个很特别的现象(建议先去看一下视频)

这里讨论的是 2020 年观察到的一组差异:部分华为样机上的微博会在主页列表滑动中显示图片,而当时部分其他样机要等到停止后才显示。 这个现象不能推广到当前所有机型;相同客户端版本也可能受到配置、缓存和系统实现的影响。

同一个版本的微博,在不同手机上表现不一样,作为系统优化工程师,我还是想搞清楚的:是不是微博专门做了适配?具体改了什么?下面就从技术的角度来分析这个现象,看看我们能从里面学到什么。

历史版本说明(2026-10-14 核查):本文是 2020 年的案例,原文标注反编译微博 10.8.1。下面保留旧图和当时体验;没有原 APK 哈希、完整华为固件/PerfSDK 版本、运行 trace 和对照统计,不能据此评价今天所有华为或其他品牌设备。图片加载控制、厂商滚动物理参数和主观手感需要分别验证。

这个现象有什么特别呢?

  1. 列表快速移动时延后部分图片工作,是一种取舍,不是所有 App 的默认最佳策略。下载、解码、变换、缓存和显示是不同阶段,暂停其中一个阶段不等于全部停止。
  2. 图片在滑动中出现不等于此时才下载,也可能来自缓存或先前任务的回填。滑动中显示图片可减少占位图时间,但也可能增加当时的 CPU、GPU、内存和网络压力,是否改善体验要看用户最终可见内容与帧时序。
  3. 我当时感觉华为样机松手后的曲线更柔和。这是当时的主观观察;没有相同初速度、内容、刷新率和固件条件下的位移/速度曲线,不能量化为所有机型都更慢、更长或更接近 iOS。

下面是我当时对合作方向的评价,不作为本次性能测量结果:

上面三个是从现象上来说的,下面就从技术上来验证,从最后的结果来看,华为和微博的合作从结果看是很成功的,可以作为一个案例推广到其他头部 App,同时作为 Android 开发者,对华为这种非常细致的体验优化真的是非常敬佩

背景备注

  1. 我在 2020 年 6 月分析时,观察到部分非华为样机表现为停止后才加载;8 月再次观察时,也能在滑动中显示图片。这个时间线是历史观察,不是当前服务端开关状态。
  2. 反编译图出现 feed_scroll_loadimage_enable 和灰度配置读取,是功能配置路径的证据;没有当时服务端响应,不能证明开关何时、对哪些账号/设备全量开启,也不能保证修改立刻生效。
  3. 同一 APK 可能因配置、缓存、系统版本和热修复走不同分支。截图里还有 PatchProxy 检查,不能只看版本号就认为运行代码相同。
  4. 原文样本标签是微博 10.8.1;不同截图中的混淆方法编号并不一致,不能把编号当作稳定 API 或精确构建标识。

结论先行

现有图展示了两部分线索:微博注册速度阈值回调来控制 ImageLoader;华为 Framework 片段包含另一套滚动位移和速度计算。回调如何控制加载,可以从图中读出;“一接入 SDK 就必定启用新曲线、并保证更流畅”,还缺完整调用链与运行测量。

  1. HwPerfVelocityCallback 报告速度与阈值的关系,所示方法没有传回每一帧的数值速度。
  2. 低于阈值的回调调用 resume,高于阈值的回调调用 pause。实际会暂停哪些任务、在途请求是否继续,要看该版 ImageLoader 实现,截图本身没有回答。
  3. Framework 图包含 SmartSlideOverScroller 分支;生效条件、设备范围与 SDK 的连接关系,仍需对应固件源码或 trace 核对。

以下是我当时的 AOSP/Demo 实践记录;本仓库没有附上该 Demo,不能算本次重新复现:

对细节感兴趣的同学可以继续阅读,有能力的同学看完后可以修改 Framework 相关代码,编译一个 SDK,然后自己写个 Demo 接入 SDK,就可以打通我下面所说的所有内容了,我自己在 AOSP 的代码上实现了一遍,Demo 也可以正常运行,有兴趣可以跟我私下交流

微博+华为是怎么优化?

现象分析

以 AbsListView 的普通手势为例,可以观察到三个状态:

  1. SCROLL_STATE_TOUCH_SCROLL = 1:手指拖动列表。仅按下但尚未超过触摸阈值,不一定已进入这个状态。
  2. SCROLL_STATE_FLING = 2:符合条件的松手惯性滚动。除了初速度,还要考虑最小 fling 速度、边界和嵌套滚动是否消费了手势。
  3. SCROLL_STATE_IDLE = 0:当前没有滚动。原文 SCROLL_STATE_I DLE 的空格是笔误。

手指拖动同样有速度,甚至可能很快。“只有 fling 才有 Velocity”“手指不离开就不会滑得太快”都不成立;这里的策略只是对不同状态选择不同处理。

阶段一优化

原图里,相关优化标志为 false 时,非 IDLE 状态调用 pause;标志为 true 时,非 FLING 状态调用 resume。因此 TOUCH_SCROLL 可以继续加载,但这不是保证不会卡顿,也不能推断所有网络请求都会立刻开始。

阶段二优化

快速 fling 时暂停、减速后恢复,是原速度回调的意图。它能否减少请求数量、降低掉帧,需要检查缓存、取消机制和实际执行分支,不能由两次函数调用直接计算收益。

另一部分是曲线调整。AOSP 默认 fling 本来就使用非线性 spline,并非匀速直线;更长或更柔和的曲线也不自动意味着帧渲染更快。曲线手感与计算/显示是否按时完成,是不同问题。

技术分析

以下按原图逐段看。它们是反编译和 Framework 的局部片段,缺少完整的 SDK 初始化条件、ImageLoader 源码和固件版本。

阶段一优化的技术分析

这一段 onScrollStateChanged 的图片加载分支可整理为:

相关优化标志 状态 图中直接动作
true IDLE / TOUCH_SCROLL resume
true FLING 此处分支没有直接调用 pause 或 resume
false TOUCH_SCROLL / FLING pause
false IDLE resume

所以不能把第一种情况写成“state == 2 就暂停”;它在这里保留已有加载状态,之后还要看速度回调。图中还处理 async_card、视频预加载等工作,不能把整段代码的性能效果都归给图片开关。PatchProxy 若接管调用,这段可见的普通分支也可能不执行。

阶段二优化的技术分析

原图把 HwPerfSpeedLoader.HwPerfVelocityCallback 传给列表相关工具方法:

  1. HwPerfonVelocityDownToThreshold():调用 ImageLoader.getInstance().resume()。
  2. HwPerfonVelocityUpToThreshold():调用 ImageLoader.getInstance().pause()。

这些是阈值事件,不是公开数值流。图中没有展示阈值大小、单位、是否初始即回调或是否有滞回,不能补出一个适用于所有屏幕密度和刷新率的固定阈值。

原文提供的华为 Framework update 片段调用了 SmartSlideOverScroller 的 getUpdateDistance/getUpdateVelocity,并有位移截止处理:

计算 Distance 的片段:

计算 Velocity 的片段:

速度表达式含指数项和归一化时间,但截图没有给出完整参数来源、入口判断与固件 tag。它能说明存在不同实现,不能单凭形式证明总时长一定大于 AOSP、所有应用都启用,或“与 iPhone 一样”。距离计算还包含修正与截止逻辑,不能只拿一条指数式代表整套曲线。

原文引用的关于 iOS 过渡动画的讨论保留作背景阅读;它不替代当前样机的滚动测量。

其他厂商处理

下面的 boolean a 是工具注册方法的结果;false 可能表示不支持、初始化失败或其他条件不满足,不能仅凭这个值断定手机品牌。普通分支的回退条件是 Build.VERSION.SDK_INT >= 28(Android 9)且 FeedAbManager 开关为 true,两个条件缺一不可:

FeedAbManager 中可见多个 Feature 读取入口:

对应方法读取 feed_scroll_loadimage_enable:

灰度配置片段区分 SYNC_WITH_SERVER、REMAIN_UNCHANGE_INAPPLIFECYCLE 和 NOTCACHED 等策略;不能把方法名称理解为每次回调都立即请求服务器。

这条回退路径可以允许滑动中加载,但图中未显示它获得了同样的速度阈值回调或厂商曲线。因此两个路径的行为和性能需要分别测,不能把“图片出现了”当作 SDK 生效证明。

最后一个问题:滑动点击

在 Android 10 AbsListView中,fling 期间 ACTION_DOWN 的拦截和触摸状态转换会让手势先参与停止/接管滚动,而不是普通 Item 点击。这可以解释一些场景需要先停再点;它不是所有 Android 列表、所有状态都必须点击两次的规则。

更长的 fling 可能延长可被中断的时间,但点击是否成功还取决于事件分发、目标稳定性、触摸阈值和控件实现。下面保留原文的手感比喻与当时系统比较,它们属于主观体验:

滑动(Fling 状态)的时间越短,列表越容易停下,用户点击列表越容易触发 Item 的点击,但是容易停止带来的问题就是不够柔和。想象你在粗糙的水泥地上滑出去一块石头,这石头没有滑动多久就会停止,不管是扔石头的你还是旁边看你扔石头的我,都不会觉得这有什么美感,但是没得选。这个的 代表其实就是 Android 原生的 Fling 曲线

滑动(Fling 状态)的时间越长,列表越不容易停下,用户点击列表越不容易触发 Item 的点击,如果曲线优化的好,给人的感觉就是很柔和,符合物理规律,想象你在光滑的冰面上滑出去一块冰,冰面越滑,冰块滑动的时间就越长,越不容易停下。这其中的极端代表就是 iOS 的 Fling 曲线。说 iOS 极端是因为,iOS 的滑动曲线调的太磨叽了,时间长不说,停的异常慢,很多时候你都需要点击一下列表让他先停止,然后再进行下一步的点击动作。而小米的 MIUI12 对这个也进行了调整,效果要比 iOS 好一些,如果再和三方进行类似华为和微博的合作,体验会更上一层楼

我当时借鉴 Launcher,提出过“完成 95% 或 98% 距离后允许点击”的设计想法,不是已经验证 MIUI 12/华为按该阈值落地。距离进度不等于剩余时间或瞬时速度;实际设计还应检查目标是否稳定、手指移动量和误触率,并保留正确的点击及无障碍语义,不能简单透传事件给正在移动的 Item。

延伸阅读

列表滑动图片加载的性能考虑

图片工作涉及请求调度、下载、解码、变换、内存/磁盘缓存、纹理上传和 View 更新。快速滚动时,可优先处理当前可见内容、限制预取窗口,并取消或降低已离屏请求的优先级;预加载本身是常见策略,不能笼统说“程序员不会加载用户未看到的数据”。

队列也不一定严格 FIFO。“前面 49 张没结束,第 50 张就必定等待”取决于并发、优先级、取消和缓存;列表复用是正常机制,错图通常需要检查请求与 Item 的绑定,而不是把复用本身当作错误。暂停后恢复还可能形成突发工作,应验证而非假定必然更省资源。

现代项目可以先使用公开滚动回调和图片库的生命周期/请求管理接口,不必依赖某个厂商私有 SDK。阈值策略应明确速度单位、取绝对值的方式、初始状态、暂停/恢复的滞回和离屏清理;不要通过反射读取 ListView 私有 Scroller。自己持有 OverScroller 时可按其公开接口读取状态,手势速度则可使用 VelocityTracker。

验收可结合 Macrobenchmark 的帧指标和 Perfetto,固定内容、缓存冷热、网络、账号/灰度、设备刷新模式及滑动输入,比较帧超时、可见图片出现时间、请求/字节数和内存,而不是只凭视频观感或平均 fps。本文没有这些对照数据,不报告新的收益数字。

列表滑动监听背景知识

AbsListView.OnScrollListener的接口摘要如下;注释按回调含义整理,省略完整文档:

1
2
3
4
5
6
7
8
9
10
11
12
public interface OnScrollListener {
int SCROLL_STATE_IDLE = 0;
int SCROLL_STATE_TOUCH_SCROLL = 1;
int SCROLL_STATE_FLING = 2;

// Called when the reported scroll state changes.
void onScrollStateChanged(AbsListView view, int scrollState);

// Reports the visible range after a scroll/layout update; not only at final idle.
void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount);
}

onScrollStateChanged 反映状态变化,不是逐帧速度回调。onScroll 在滚动或布局更新后报告可见范围,滚动过程中会多次触发;官方注释中的 “after the scroll has completed” 不能误读成只在最后停稳时触发。AOSP 的 invokeOnItemScrollListener 调用位置也能验证这一点。

AndroidX 对照:RecyclerView.OnScrollListener(1.0.0 起)使用 IDLE、DRAGGING、SETTLING;SETTLING 不只代表手指松开的 fling,也可能是程序发起的平滑滚动。onScrolled 的 dx/dy 是本次位移,不是速度;布局改变可产生 dx=dy=0 的回调。若估算速度,需要时间间隔和有效样本,不能照搬 AbsListView 常量名称或仅用状态值判断快慢。

列表滑动状态的变化

典型手势可能是:

  1. 拖动后低速松手:IDLE → TOUCH_SCROLL → IDLE。
  2. 松手满足 fling 条件:IDLE → TOUCH_SCROLL → FLING → IDLE。

这不是穷尽的状态机:新的按下、边界、嵌套滚动、程序滚动都可能改变路径。手指未离屏也会由 VelocityTracker 估算速度;是否开始 fling 还需满足最小速度等条件。

列表的 Fling 曲线计算

下面补全 AOSP android-10.0.0_r1 OverScroller.SplineOverScroller.update() 的零时间/结束判断。它是 Framework 内部方法,依赖类中字段,不是可直接粘贴给 App 的完整类;也不是前面华为修改版的源码。

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
boolean update() {
final long time = AnimationUtils.currentAnimationTimeMillis();
final long currentTime = time - mStartTime;

if (currentTime == 0) {
// Skip work but report that we're still going if we have a nonzero duration.
return mDuration > 0;
}
if (currentTime > mDuration) {
return false;
}

double distance = 0.0;
switch (mState) {
case SPLINE: {
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;
}

SPLINE 对采样曲线插值,mSplineDistance / mSplineDuration * 1000 在该分支把像素/毫秒换为像素/秒。BALLISTIC 是越界后的减速阶段,CUBIC 是返回有效范围的回弹,不能直接等同于新系统的 EdgeEffect 拉伸动画。

更新由滚动调用链驱动,FlingRunnable 会继续安排后续工作;“每帧更新”描述的是预期调度,不保证每个屏幕刷新都执行一次。按经过时间计算位置,可在调度延迟后跳到对应进度。mCurrVelocity 是内部轴状态,不能假定所有分支都可直接作为统一的物理速度合同;公开 getCurrVelocity 返回 X/Y 速度向量的模,不提供方向。

厂商应用联合优化

微博这个优化就是厂商和应用之间联合优化的一个案例,应用对用户体验的极致追求,让这种合作在未来会变得更加频繁,像微信、快手、抖音这些…

下面这个招聘是拼多多的一个 JD,看职位描述是专门对接厂商的优化,也可以看出应用对厂商的合作越来越重视。之前厂商和应用是魔高一尺道高一丈的关系,互相攻防导致最终体验受损的还是用户;而现在这种厂商和应用合作的关系,不仅提升了双方的体验,也会带动 Android 生态圈向好的方面去发展

本文其他地址

微信公众号 - https://mp.weixin.qq.com/s/wJKOvU7CqP3vM0TG7rO66g
知乎专栏(求个赞) - https://zhuanlan.zhihu.com/p/191460094

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球:付费社群已于 2026 年停止运营,不再招收新成员;此处保留历史介绍。

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

CATALOG
  1. 1. 背景备注
  2. 2. 结论先行
  • 微博+华为是怎么优化?
    1. 1. 现象分析
      1. 1.1. 阶段一优化
      2. 1.2. 阶段二优化
    2. 2. 技术分析
      1. 2.1. 阶段一优化的技术分析
      2. 2.2. 阶段二优化的技术分析
    3. 3. 其他厂商处理
  • 最后一个问题:滑动点击
  • 延伸阅读
    1. 1. 列表滑动图片加载的性能考虑
    2. 2. 列表滑动监听背景知识
    3. 3. 列表滑动状态的变化
    4. 4. 列表的 Fling 曲线计算
  • 厂商应用联合优化
  • 本文其他地址
  • 关于我 && 博客