本文讲的是RecyclerView 数据预取,

更快处理任务,使滚动和滑动更流畅

在我小时候,妈妈为了治疗我的拖延症,总是告诉我:“如果你现在打扫你的房间,就不用以后再打扫了。”但我从没这样做。我知道最好能拖延就尽量拖延。一个原因是:如果我现在打扫了,房间还会变脏,那时候我就必须再打扫一遍了。另外,如果我把这件事放下足够久,妈妈可能会忘了它的。

拖延对我来说总是有效。但我永远不用处理保持帧率的问题,不像我的朋友 RecyclerView 一样。

问题

在一次滚动或惯性滑动中,RecyclerView 需要在新条目抵达屏幕时予以展示。这些新条目需要与数据相绑定(如果缓存中没有相应条目的话,还需要创建一个)。接下来,它们还需要被展开并画出来。如果所有这些都是被懒加载的,在需要展示之前才做,UI 线程就会在工作完成时陷入停顿。接下来渲染可以继续并且滚动(或者说滑动,但我打算用滚动来指代它们,以简化讨论)可以平滑地继续,直到下一个条目进入视野范围。

一次典型的 RecyclerView 内容滚动中的各个渲染阶段(在 Lollipop 版本时的情况)。在UI线程,我们处理输入事件和动画,完成布局,并且记录绘图操作。接下来渲染线程把指令送往GPU。 在一次滚动的大多数帧中,RecyclerView 可以没问题地完成它需要做的事,因为不需要处理新的内容。在这些帧中,UI 线程处理输入事件和动画,完成布局,记录绘图操作。接下来它把绘图信息与渲染线程同步(在 Lollipop 版本时的情况,之前的版本在 UI 线程完成所有工作),渲染线程把指令送往 GPU。

新条目使得输入阶段耗时更长,因为新的 view 需要被创建、绑定并布局。这推迟了渲染阶段的开始,从而导致它可能在帧的边界之后结束。在此情况下,就会发生掉帧。当一个新的条目来到屏幕中时,输入阶段就需要完成更多工作,以绑定(可能还要创建)正确的 view。这推迟了 UI 线程其余的工作,以及渲染线程接下来的工作。如果这些不能在帧边界内完成的话,就会发生卡顿。

输入阶段的调用栈表明:新的条目进入视野范围会导致一大块时间被用于创建和绑定新的 view。 如果我们可以在其它地方完成这些工作,而不推迟所有其它事情,不就很好吗? 

在 view 可以被渲染之前,创建和绑定必须完成。这会在相应的帧中消耗 UI 线程的宝贵时间。然而,UI 线程在前一帧中有大量时间无所事事。 Chris Craik(Android UI Toolkit 组的工程师)在用 Systraces 查看 RecyclerView 滚动时发现了这一点。他特别注意到,我们在需要使用一个条目时,会花费大量时间准备它。而在一帧之前,UI 线程花了大量时间休眠,因为它很早就完成了任务。

解决方案

将创建和绑定工作移到前一帧,使 UI 线程能够与渲染线程同时工作,从而避免接下来在渲染线程绘制结果之前同步完成这些工作。 显然,这是优化耗时的好时机。Chris 重新安排了默认 RecyclerView 布局时事件发生的顺序,它现在在一个条目即将进入视野时预取数据,这样我们可以在空闲期完成工作,避免拖到大家都在等待结果时才完成。 完成这些工作基本上没有任何代价,因为 UI 线程在两帧之间的空隙不做任何工作。我们可以使用这些空闲时间来完成将来的工作,并使得未来的帧出现得更快,因为困难的部分已经被完成了。

细节,细节

这个系统的工作方式是,在 RecyclerView 开始一个滚动时安排一个 Runnable。这个 Runnable 负责根据 layout manager 和滚动的方向预取即将进入视野的条目。预取不限于一个单独的条目。它可以同时取出多个条目,例如在使用 GridLayoutManager 且新的一行马上要出现的时候。在 25.1 版本中,预取操作被分为单独的创建/绑定操作,从而比对整组条目做操作更容易被纳入 UI 线程的空隙中。

有趣的是,系统必须预测操作需要多少时间,以及它们是否可以被放入空隙中。毕竟,如果预取把当前帧推迟到截止时间之后,我们仍然会因掉帧而感觉到卡顿,只是和不预取时原因不同而已。系统处理这些细节的方式是追踪每种 view 类型的平均创建/绑定时间,从而使未来创建/绑定时间的合理预测成为可能。

对嵌套 RecyclerView(每一个条目自身都是 RecyclerView 的容器)完成这些工作更加复杂,因为绑定内部 RecyclerView 并不涉及任何子控件的分配——RecyclerView 在被绑定和布局时按需取得子控件。预取系统仍然可以预先准备内层的 RecyclerView 内部的子控件,但它必须知道有多少。这就是 25.1 版本中 LinearLayoutManager 新 API setInitialItemPrefetchCount()的意义。它告诉系统,在滚动时需要预取多少条目来充满 RecyclerView。

警告

你需要注意这些危险:

-预取数据可能做一些最终不被需要的工作。因为我们在预取 view 时,有可能会采取太激进的策略,这样 RecyclerView 就可能不会滚动到我们预取的条目。这意味着我们的预取工作可能会被浪费(虽然这些工作是被并行完成的,应该不会浪费太多时间。另外,浪费是不太可能发生的,因为我们在需要数据之前不久才去预取,而且滚动不太可能在两帧之间停止或反转)。 -渲染线程:渲染线程是 Lollipop 版本引入的性能特性,它可以让一个不同的线程分担渲染工作,并且支持其他的一些改进,例如把不可变的动画(如涟漪、环形展现等)完全放在渲染线程,使其不受 UI 线程停顿的影响。这意味着运行 Lollipop 之前的版本的设备将不会受益于这个优化,因为我们无法并行完成这些工作。

我要一些 —— 去哪儿拿?

预取优化是在 Support Library v25中引入,在 v25.1.0中改进的。所以第一步是下载 最新版本的支持库。

如果你使用 RecyclerView 提供的默认 layout manager,你将自动获得这种优化。然而,如果你使用嵌套 RecyclerView 或者自己写 layout manager,你需要改变你的代码来利用这个特性。

对于嵌套 RecyclerView 而言,要获取最佳的性能,在内部的 LayoutManager 中调用 LinearLayoutManager 的setInitialItemPrefetchCount()方法(25.1版本起可用)。例如,如果你竖直方向的list至少展示三个条目,调用 setInitialItemPrefetchCount(4)。

如果你实现了自己的 LayoutManager,你需要重写 LayoutManager.collectAdjacentPrefetchPositions()方法。该方法在数据预取开启时被 RecyclerView 调用(LayoutManager 的默认实现什么都不做)。第二,在嵌套的内层 RecyclerView 中,如果你想让你的 LayoutManager 预取数据,你同样应当实现 LayoutManager.collectInitialPrefetchPositions()。

和以前一样,优化你的创建和绑定步骤,做尽可能少的工作,是值得的。运行的最快的代码是根本不需要运行的代码;即使框架可以通过数据预取并行工作,它仍然消耗时间,而且耗时较长的条目创建仍然可以导致卡顿。例如,一棵最小的 view 树总比一棵复杂的更容易创建和绑定。本质上,绑定应该和调用 setter 一样方便,一样快。即使你用目前的代码就可以在一帧的时间限制中完成工作,进一步优化意味着它将更可能在低端的用户机型上运行良好。此外,在高端设备上为这些常用场景节约性能,总是对电池有益的。如果你已经尽可能缩短了创建和绑定的时间,预取将会帮助你缩短两帧之间的剩余时间。

如果你想要见到实际的优化,在默认或自定义的 LayoutManager 中,你可以切换 LayoutManager.setItemPrefetchEnabled()并比较结果。你应该能够从视觉上直观地看到差异;它确实如此显著,特别是在条目需要大量时间创建和绑定的情况下。但如果你想知道在表面下发生过什么,在预取打开和关闭时运行Systrace, 或者打开 GPU profiling。

Systrace 显示数据预取在UI线程空闲时预取数据。

GOTO 结尾

查看 最新的 Support Library并和能预取数据的 RecyclerView 一起玩耍。同时,我将继续不清理我的房间。






原文发布时间为:2017年2月14日

本文来自云栖社区合作伙伴掘金,了解相关信息可以关注掘金网站。

RecyclerView 数据预取相关推荐

  1. Intel Sandy Bridge/Ivy Bridge架构/微架构/流水线 (18) - 数据预取

    Data Prefetching 使用软件预取指令,硬件预取机制,或者两者的任意组合,都可以将数据投机式地加载到L1D中. 程序员可以使用4条SSE预取指令来实施软件控制的预取操作.这些指令给处理器提 ...

  2. Intel Core Enhanced Core架构/微架构/流水线 (12) - 数据预取

    Data Prefetch to L1 Caches Intel Core微架构提供了2个硬件预取器,可以将数据预取到一级缓存中,由此加速程序访问数据的速度: 数据缓存单元预取器(DCU prefet ...

  3. Intel Core Solo/Duo处理器架构/微架构/流水线 - 前端/数据预取/SSE3

    Intel Core Solo/Duo处理器采用的微架构类似于Pentium M处理器的微架构,但是在性能和功能方面做了进一步的增强,主要包括: Intel智能缓存 – Intel Core Duo的 ...

  4. Pentium M处理器架构/微架构/流水线 (2) - 数据预取/乱序核/退役单元

    Data Prefetching Intel Pentium M处理器支持3种预取机制: 第一种是硬件指令预取,已在上面章节中讲述 第二种是自动的将数据预取到二级缓存.这个机制基本上与Intel Ne ...

  5. Pentium 4处理器架构/微架构/流水线 (10) - NetBurst执行核详解 - 高速缓存/数据预取

    Caches Intel NetBurst微架构支持最多达3级的片上高速缓存.Pentium 4处理器上只实现了两级片上缓存结构,主要面向于桌面计算环境.最接近处理器执行核的缓存级,即一级缓存,包含独 ...

  6. recycleview横向展开_android 横向recyclerView 数据居中,从中间往两边展开显示

    android 横向recyclerView 数据居中,从中间往两边展开显示 很多人的recyclerView都是自定义过后的recyclerView,所以我这里就不展示recyclerView了 横 ...

  7. CPU数据预取对软件性能的影响

    一.什么是预取 预取是指将内存中的指令和数据提前存放到cache(L1.L2.L3)中,从而加快处理器执行速度. Cache预取可以通过硬件或者软件实现,也就是分为硬件预取和软件预取两类. 硬件预取, ...

  8. Android实战开发-Kotlin教程(入门篇-RecyclerView数据列表的实现)

    在移动应用开发中,很多时候我们需要去展示数据记录,如电商应用中的商品列表.订单列表等等一系列的数据记录的展示时,我们需要用到Android的列表组件展示这些数据,列表组件应该是我们遇到到比较常见也是必 ...

  9. 微信小程序开发11 数据预取:合理缓存提高用户体验

    你好,我是俊鹏,今天我带你学习怎么通过微信提供的数据预取能力,提高小程序的用户体验. 数据预取也叫数据预加载,顾名思义,这项能力能提前加载未来要使用的数据,然后缓存到本地.这样一来,你在使用这些数据时 ...

最新文章

  1. JFinal开发环境搭建,JFinal开发案例
  2. #linux进阶#wget
  3. java调用gbase存储过程,GBase 8T数据库存储过程使用出参示例
  4. 误码率越高越好还是越低越好_ISO永远都是越低越好?不一定!这些情况下要用高 ISO!...
  5. dotnet-cli命令小结
  6. div中文字居中加粗
  7. File Manipulation
  8. 【笔记】《编写高质量代码:改善c#程序的157个建议》-第1章 基本语言要素(SamWang)...
  9. python安装openpyxl库_Python openpyxl 库
  10. 波长间隔与频率间隔换算
  11. 计算机专业春考技能考什么技能,春季高考信息技术考试说明知识技能
  12. matplotlib科研绘图---Times New Roman字体设置
  13. ui界面设计基本原则
  14. 【21天算法学习】折半插入排序
  15. [数学建模]灵敏度分析记录
  16. Orthogonal Convolutional Neural Networks
  17. Failed to push the item
  18. OriginPro2021安装注意事项(详细)
  19. vim: command not found 绝妙解决方案
  20. 【android】EditText输入银行卡号每四位空一格

热门文章

  1. 从零开始学PowerShell(1)初见基础命令
  2. Python print和pprint两者的区别
  3. C/C++socket send函数MSG_NOSIGNAL
  4. VS 多进程调试(C#和C )
  5. 非常详细Redis数据库入门教程
  6. 《论文笔记》Experimental Research on Cooperative vSLAM for UAVs
  7. php生成随机验证码
  8. 密码与安全新技术总结
  9. JavaScript 基础 数据类型与运算符
  10. 【Node.js学习笔记五】npm(Node包管理器)命令行选项