从Glide.with(View)开始优化RecyclerView掉帧问题

160 阅读5分钟

前言:

最近在项目中发现有个recyclerview滑动掉帧现象很严重,刚好最近要更新一个版本,就准备把这个问题优化了

图片

一、Glide.with(View) 是低效的

在 Android 中,常用的图片处理工具主要有 Glide、Picasso 和 Fresco,而在直接加载图片到 ImageView 时,Glide 毫无疑问是更优的选择。关于 Glide 的具体实现,这里暂且不展开讨论。需要注意的是,Glide 中的 with 方法提供了多种形式,例如:

@NonNull  public static RequestManager with(@NonNull Context context) {    return getRetriever(context).get(context);  } @NonNull  @Deprecated  public static RequestManager with(@NonNull Activity activity) {    return with(activity.getApplicationContext());  } @NonNull  public static RequestManager with(@NonNull Fragment fragment) {    return getRetriever(fragment.getContext()).get(fragment);  }....

在 RecyclerView 的 item 中,如果需要加载图片,我们通常会在获取数据后,将数据传入 adapter,然后在 adapter 中处理图片的加载。然而,由于在 adapter 中不应该直接持有 ActivityFragment 或 Context 的引用,因此更推荐使用 Glide.with(View) 方法。

Glide.with(View) 的实现如下:

@NonNullpublic static RequestManager with(@NonNull View view) {    return getRetriever(view.getContext()).get(view);}

Glide 中关于这个方法的解释是

图片

有一句话是这样说的:“This method may be inefficient always and is definitely inefficient for large hierarchies. Consider memoizing the result after the View is attached or, again, prefer the Activity and Fragment variants whenever possible.” 这段话的意思是,使用 Glide.with(View) 方法在某些情况下可能会低效,特别是在视图层级较复杂时。如果没有特殊需求(如自定义 View),建议优先选择其他的 with() 方法,比如基于 Activity 或 Fragment 的方法,以确保性能。

Glide 为了优化性能,真的在细节上做足了功夫。让我们一起看看它如何确保高效的图片加载和资源管理:

图片

1) 先是通过 findActivity 方法递归获取 activity

图片

2)然后,通过 findAllSupportFragmentsWithViews 方法递归获取 Activity 中所有的 Fragment,并将每个 Fragment 的视图 (fragment.getView()) 作为键,Fragment 本身作为值,存储在一个 Map 中。

总的来说,Glide 会先尝试获取视图对应的 Activity,如果找到对应的 Fragment,则使用 Fragment,如果未找到 Fragment 但找到了 Activity,则使用 Activity,如果都找不到,则使用 Application,并将生命周期绑定到 Application 上。

实际上,如果每个 RecyclerView 项目中的图片资源较少,性能影响不大。但在我们的场景中,RecyclerView 中每个项都需要加载多个 ImageView,积少成多,就可能导致性能瓶颈。量变引发质变了属于是

二、简化布局的层级

其实首先排查的是布局的层级 , 因为 ui 比较复杂 , 所以布局这块嵌套的层级比较多 , 这里把该省的都省下来了 , 例如 :LinearLayout 中嵌套了 Framlayout , Framlayout 中又嵌套了 ConstraintLayout

三、 尽量使用LinearLayout 和FrameLayout

这当然是因为 LinearLayout 和 FrameLayout 的性能足够优秀 , 在性能对比如下

图片

以 RelativeLayout 举例 ,RelativeLayout 会让子 View 调用 2 次 onMeasure , LinearLayout 在有 weight 时, 才 会调用子View2 次 onMeasure 。而 RelativeLayout 的子 View 如果高度和 RelativeLayout 不同,会导致 RelativeLayout 在 onMeasure() 方法中做横向测量时,纵向的测量结果尚未完成,只好暂时使用自己的高度传入子 View 系统。而父 View 给子 View 传入的值也没有变化就不会做无谓的测量的优化会失效 。

四、 滑 动时不加载,停止时加载

其实到上面那一步之后 , 掉帧问题已经优化了很多 , 用户正常上下滑动是没有感觉到上面卡顿 , 但是 , 如果快速的较长时间的滑动依旧会出现卡顿 , 所以还需要继续优化。 这个时候就准备从每个 item加载多个图片这块入手

主要思路是 : 当用户滑动时停止加载图片资源

Glide.with(this).pauseRequests()

在用户停止滑动时加载图片

Glide.with(this).resumeRequests()

体验起来确实爽多了,不管怎么快速滑动都是OK 的 , 但是又有新的问题 , 用户的体验就很差 , 因为滑动时看到的都是未加载的空白图片 , 还得接着优化 前面说到 , 到第三步时 , 我们正常滑动其实已经不会卡顿了 , 无非就是要避免用户快速滑动的问题 , 那我们只要判断用户滑动时速度 , 当速度过快时 , 停止加载 , 速度慢下来恢复加载 , 就可以完美解决问题 , 参考代码如下 :

mBinding.rvList.addOnScrollListener(object : RecyclerView.OnScrollListener() {   override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {       super.onScrollStateChanged(recyclerView, newState)       if (newState != SCROLL_STATE_IDLE && mScrolled) {           Glide.with(this).pauseRequests()       } else {           Glide.with(this).resumeRequests()       }   }   override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {       super.onScrolled(recyclerView, dx, dy)       mScrolled = dy > 80 || dy < -55   }})

五、 其他优化操作

参考了下别人的方案 , 如果后续还需要优化应该是还有优化的空间

1、 加大RecyclerView 的缓存,用空间换时间,来提高滚动的流畅性。

mBinding.rvList.setItemViewCacheSize(20);mBinding.rvList.setDrawingCacheEnabled(true);mBinding.rvList.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);

2、 增加RecyclerView 预留的额外空间

object : LinearLayoutManager(this) {   override fun getExtraLayoutSpace(state: RecyclerView.State): Int {       return size   }}

3、 设置固定高度

如果item 高度是固定的话,可以使用 RecyclerView.setHasFixedSize(true); 来避免 requestLayout 浪费资源。

4 、 极端情况下可以采用减少 xml 文件 inflate 时间的方式

xml 文件包括: layout 、 drawable 的 xml , xml 文件 inflate 出 ItemView 是通过耗时的 IO 操作。可以使用代码去生成布局,即 newView() 的方式。这种方式是比较麻烦,但是在布局太过复杂,或对性能要求比较高的时候可以使用。