安卓RecyclerView滚动性能优化

97

我基于创建列表和卡片指南创建了RecyclerView示例。我的适配器仅实现布局填充的模式。

问题在于滚动性能差。即使只有8个项目,RecycleView也存在这个问题。

在一些测试中,我发现在Android L上不会出现此问题。但在KitKat版本中,性能下降是明显的。


1
尝试使用ViewHolder设计模式来提高滚动性能:http://developer.android.com/training/improving-layouts/smooth-scrolling.html - Haresh Chhelana
2
你能分享一下你的适配器设置和布局的XML文件的代码吗?这看起来不太正常。 另外,你有分析过时间花在哪里吗? - yigit
2
我遇到了几乎相同的问题。除了在Lollipop之前很快,而在Android L中非常慢(真的)。 - Servus7
1
你能否分享一下你正在导入的库的版本? - Droidekas
@Droidekas,感谢您的评论!性能问题已经在21版库中被确认,但即使在23版中,问题仍然存在。 - falvojr
显示剩余5条评论
16个回答

249

最近我也遇到了同样的问题,这是我在最新的RecyclerView支持库中所做的:

  1. 用新的优化约束布局(ConstraintLayout)替换复杂布局(嵌套视图、RelativeLayout)。 在Android Studio中启用它: 进入SDK管理器-> SDK工具选项卡-> 支持库-> 选中 ConstraintLayout for Android 和 Solver for ConstraintLayout。添加到依赖项:

    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    
  2. 如果可能的话,使RecyclerView的所有元素具有相同的高度。并添加:

  3. recyclerView.setHasFixedSize(true);
    
  4. 使用默认的RecyclerView绘制缓存方法,并根据您的情况进行微调。您不需要第三方库来实现:

    recyclerView.setItemViewCacheSize(20);
    recyclerView.setDrawingCacheEnabled(true);
    recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);
    
  5. 如果您使用了许多图像,请确保它们的大小和压缩率是最优的。缩放图像也可能会影响性能。问题有两个方面 - 使用的原始图像和解码后的位图。以下示例为您提供了如何解码从web下载的图像的提示:

  6. InputStream is = (InputStream) url.getContent();
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPreferredConfig = Bitmap.Config.RGB_565;
    Bitmap image = BitmapFactory.decodeStream(is, null, options);
    

最重要的部分是指定inPreferredConfig - 它定义了每个图像像素将使用多少字节。请注意,这是一个首选选项。如果源图像具有更多颜色,则仍将使用不同的配置进行解码。

  1. 确保 onBindViewHolder() 尽可能地廉价。您可以在 onCreateViewHolder() 中设置 OnClickListener 一次,并通过 Adapter 外部传递已点击的项目来调用接口上的监听器。这样你不会一直创建额外的对象。在此处更改视图之前,还应检查标志和状态。

viewHolder.itemView.setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View view) {
          Item item = getItem(getAdapterPosition());
          outsideClickListener.onItemClicked(item);
      }
});
当数据发生变化时,尝试仅更新受影响的项目。例如,在添加/加载更多项目时,不要使用notifyDataSetChanged()使整个数据集失效,而是只需使用以下方法:
adapter.notifyItemRangeInserted(rangeStart, rangeEnd);
adapter.notifyItemRemoved(position);
adapter.notifyItemChanged(position);
adapter.notifyItemInserted(position);
  • 来自Android开发者网站

  • 仅在没有其他选择时才使用notifyDataSetChanged()。

    但如果需要使用它,请使用唯一的id来维护您的项目:

        adapter.setHasStableIds(true);
    

    当使用此方法时,RecyclerView将尝试为报告具有稳定ID的适配器合成可见的结构更改事件。这可以帮助进行动画和视觉对象持久化,但是单个项目视图仍需要重新绑定和重新布局。

    即使您做得一切都正确,RecyclerView仍然可能没有像您想要的那样流畅地运行。


    23
    一个对于 adapter.setHasStableIds(true) 方法的投票,它真正帮助了 RecyclerView 的快速运行。 - Atula
    1
    第七部分完全错误!setHasStableIds(true)不会有任何作用,除非您使用adapter.notifyDataSetChanged()。链接:https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html#notifyDataSetChanged() - localhost
    1
    我能理解为什么 recyclerView.setItemViewCacheSize(20); 可以提高性能。但是 recyclerView.setDrawingCacheEnabled(true);recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH); 呢!我不确定这些会改变什么。这些是 View 特定的调用,允许您以编程方式检索绘图缓存作为位图,并在以后为您的优势使用它。RecyclerView 似乎对此没有任何影响。 - Abdelhakim AKODADI
    2
    @AbdelhakimAkodadi,使用缓存后滚动会更加流畅。我已经测试过了。你怎么能说不是呢,这很明显。当然,如果有人疯狂滚动,什么都没用。我只是展示其他选项,比如setDrawingCacheQuality,但在我的情况下图像质量很重要,所以我不使用它。我并不是在宣扬DRAWING_CACHE_QUALI‌​TY_HIGH,而是建议有兴趣的人深入了解并调整该选项。 - Galya
    3
    setDrawingCacheEnabled() 和 setDrawingCacheQuality() 方法已经被弃用,建议使用硬件加速代替。详情请参考:https://developer.android.com/reference/android/view/View.html#setDrawingCacheEnabled(boolean) - Shayan_Aryan
    显示剩余18条评论

    13

    3
    这对我来说稍微提高了一点性能。但是RecyclerView仍然非常慢,比等效的自定义ListView慢得多。 - SMBiggs
    1
    啊,但我发现我的代码为什么这么慢——这与setHasStableIds()无关。我会发布一个带有更多信息的答案。 - SMBiggs

    13

    我发现了至少一种可以拖慢你的性能的模式。请记住,onBindViewHolder()频繁地被调用。因此,在该代码中进行任何操作都有可能使性能降到谷底。如果您的RecyclerView进行任何自定义操作,很容易在此方法中无意中添加一些缓慢的代码。

    我正在根据位置更改每个RecyclerView的背景图像。但是加载图像需要一些工作,这导致我的RecyclerView变得缓慢和卡顿。

    为图像创建缓存效果非常好;现在,onBindViewHolder()只需要修改对已缓存图像的引用,而不是从头加载图像。现在,RecyclerView快速运行。

    我知道并非每个人都会遇到这个确切的问题,因此我不打算提供代码。但请将在onBindViewHolder()中执行的任何工作视为可能导致差劲RecyclerView性能的瓶颈。


    我遇到了相同的问题。目前我正在使用Fresco来加载和缓存图像。您有没有另一个更好的解决方案来在RecyclerView中加载和缓存图像?谢谢。 - Androidicus
    我对Fresco不太熟悉(读了一下,它们的承诺很不错)。或许他们有一些关于如何最好地使用他们的缓存与RecyclerView的见解。而且我发现你并不是唯一遇到这个问题的人:https://github.com/facebook/fresco/issues/414。 - SMBiggs

    11
    除了 @Galya 的详细回答之外,我想要说明的是,尽管它可能是一个优化问题,但启用调试器也会明显减慢速度。
    如果您尝试了一切来优化您的 RecyclerView ,但仍然无法流畅运行,请尝试将构建变体切换到 release,并检查在非开发环境下(关闭调试器)的运行情况。
    我曾经遇到这样的情况,我的应用在调试构建变量下表现缓慢,但只要我切换到 release 变量,它就可以流畅地工作。这并不意味着您应该使用 release 构建变量进行开发,但值得知道的是,只要您准备好发布您的应用程序,它就能够正常工作。

    这个评论对我来说真的很有帮助!我已经尝试了很多方法来提高我的recyclerview的性能,但是没有什么真正的帮助,但一旦我切换到 release build,我意识到一切都很好。 - Tal Barda
    1
    我即使没有连接调试器也遇到了同样的问题,但是一旦切换到发布版本,问题就不再出现了。 - Farmaan Elahi

    11

    我就RecyclerView的性能进行了一次讲解。这里有英文幻灯片俄语录制视频

    其中包含一系列技术(其中一些已经由@Darya的回答涵盖)。

    以下是简要总结:

    • 如果Adapter的项大小是固定的,则设置:
      recyclerView.setHasFixedSize(true);

    • 如果数据实体可以由长整数表示(例如hashCode()),则设置:
      adapter.hasStableIds(true);
      并实现:
      // YourAdapter.java
      @Override
      public long getItemId(int position) {
      return items.get(position).hashcode(); //id()
      }
      在这种情况下,Item.id()将无法工作,因为即使Item的内容已更改,它仍将保持不变。
      P.S. 如果您正在使用DiffUtil,则不需要执行此操作!

    • 使用正确缩放的位图。不要重复发明轮子,使用库。
      了解如何选择here

    • 始终使用最新版本的RecyclerView。例如,在25.1.0中有巨大的性能提升-预取。
      更多信息here

    • 使用DiffUtill。
      DiffUtil is a must.
      Official documentation

    • 简化您的项目布局!
      丰富TextView的微小库 - TextViewRichDrawable

    请查看slides以获取更详细的解释。


    我正在使用带有ListAdapter的DiffUtil,并且仍然需要覆盖getItemId - Bitwise DEVS

    10
    我不确定使用setHasStableId标志是否能解决您的问题。根据您提供的信息,您的性能问题可能与内存问题有关。您的应用程序在用户界面和内存方面的性能是相当相关的。
    上周我发现我的应用程序存在内存泄漏问题。我之所以发现这个问题是因为使用我的应用程序20分钟后,我注意到UI的表现非常缓慢。关闭/打开一个活动或滚动具有大量元素的RecyclerView都非常缓慢。在使用http://flowup.io/监视一些生产中的用户后,我发现了这个问题:

    enter image description here

    帧时间非常高,每秒帧数非常低。你可以看到有些帧需要约2秒才能渲染:S。
    尝试找出导致这种不良帧时间/帧速率的原因,我发现我有一个内存问题,如下所示:

    enter image description here

    即使平均内存消耗接近15MB,该应用程序仍会在同时丢帧。
    这就是我发现UI问题的原因。我的应用程序有一个内存泄漏,导致很多垃圾收集器事件,这导致Android VM必须在每一帧中停止我的应用程序来收集内存,从而导致了不良的UI性能。
    查看代码后,我发现自定义视图中有一个泄漏,因为我没有从Android Choreographer实例中注销监听器。修复后,一切恢复正常 :)
    如果您的应用程序由于内存问题而丢帧,则应检查两个常见错误:
    1. 检查您的应用程序是否在每秒调用多次的方法中分配对象。即使此分配可以在应用程序变慢的不同位置执行,例如在onDraw自定义视图方法或在recycler view view holder的onBindViewHolder中创建对象的新实例。
    2. 检查您的应用程序是否向Android SDK注册了一个实例,但未释放它。向总线事件注册侦听器也可能会导致泄漏。
    免责声明:我使用的监测应用工具正在开发中。因为我是其中一名开发人员,所以我可以访问此工具 :) 如果您想要访问此工具,我们会很快发布测试版!您可以在我们的网站上加入:http://flowup.io/
    如果您想要使用其他工具,可以使用:traveview、dmtracedump、systrace或Android Studio集成的Andorid性能监测器。但请记住,这些工具仅会监测您连接的设备,而不是您的其它用户设备或Android操作系统安装。

    3

    在我的RecyclerView中,我使用位图图像作为item_layout的背景。
    @Galya说的一切都是正确的(我感谢他的出色答案)。但是它们对我没有起作用。

    这就是解决我的问题的方法:

    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inSampleSize = 2;
    Bitmap bitmap = BitmapFactory.decodeStream(stream, null, options);
    

    如果想了解更多信息,请阅读这个答案


    3
    我用这行代码解决了它。
    recyclerView.setNestedScrollingEnabled(false);
    

    2
    具有讽刺意味的是,大多数人可能正在寻找这个简单选项,而通过查看点赞,他们正在进行分析或寻找如此复杂的解决方案。 - nibbana

    3

    另外,检查您放置RecyclerView的父布局也非常重要。当我在NestedScrollView中测试RecyclerView时,遇到了类似的滚动问题。在另一个可滚动视图中滚动的视图可能会在滚动期间性能下降。


    2

    在我的情况下,我发现导致滞后的显著原因是在#onBindViewHolder()方法内频繁加载可绘制对象。我通过在ViewHolder内只加载一次图像并从该方法中访问它来解决了这个问题。这就是我所做的全部。


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接