【问题标题】:UI lags and is choppy when using Glide to load images in a RecyclerView使用 Glide 在 RecyclerView 中加载图像时 UI 滞后且不稳定
【发布时间】:2020-03-18 23:41:33
【问题描述】:

我有一个 RecyclerView,它使用 Glide 从 URL 加载图像。现在使用分页从 Firebase 检索 URL,如下所示。问题是,当 MainActivity(包含下面的代码和 recyclerview)第一次初始化时,UI 有很大的延迟(滚动非常缓慢和不连贯,选项菜单需要 3 秒才能打开等)并且图像需要同时加载。在我向下滚动并到达第一页数据的 RecyclerView 末尾后,触发 OnScrollListener 并开始从新查询中加载新数据。我已经尽力根据用户在我发布的另一篇帖子中的建议优化 Glide 所做的事情,并且我也将 adapter.setHasFixedSize 设置为 true,但没有运气。知道这里发生了什么吗?尽管查询是异步的,但我是否以某种方式挂起了 UI 线程?

编辑 : Glide 是否会导致主线程延迟,因为它必须将多个图像加载到回收器视图的 imageViews 中?如果是这样,我该如何应对?

以下是我如何处理从 Firebase 获得的数据的分页并通知适配器:

class MainActivity : AppCompatActivity() {

private val TAG: String = MainActivity::class.java.simpleName // Tag used for debugging
private var queryLimit : Long = 50 // how many documents should the query request from firebase
private lateinit var iconsRCV : RecyclerView // card icons recycler view
private lateinit var lastVisible:DocumentSnapshot

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val rootRef: FirebaseFirestore = FirebaseFirestore.getInstance()
    val urlsRef : CollectionReference = rootRef.collection("CardIconUrls")
    val query : Query = urlsRef.orderBy("resID",Query.Direction.ASCENDING).limit(queryLimit) // create a query for the first queryLimit documents in the urlsRef collection

    // Setting Toolbar default settings
    val toolbar : Toolbar = findViewById(R.id.mainToolbar)
    setSupportActionBar(toolbar) // set the custom toolbar as the support action bar
    supportActionBar?.setDisplayShowTitleEnabled(false) // remove the default action bar title

    // RecyclerView initializations
    iconsRCV = findViewById(R.id.cardIconsRCV)
    iconsRCV.layoutManager = GridLayoutManager(this,5) // set the layout manager for the rcv
    val iconUrls : ArrayList<String> = ArrayList() // initialize the data with an empty array list
    val adapter = CardIconAdapter(this,iconUrls) // initialize the adapter for the recyclerview
    iconsRCV.adapter = adapter // set the adapter
    iconsRCV.setHasFixedSize(true)

    iconsRCV.addOnScrollListener(object:OnScrollListener(){
        override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
            super.onScrollStateChanged(recyclerView, newState)

            if(!iconsRCV.canScrollVertically(1) && (newState == RecyclerView.SCROLL_STATE_IDLE) && ((iconsRCV.layoutManager as GridLayoutManager).findLastVisibleItemPosition() == (iconsRCV.layoutManager as GridLayoutManager).itemCount-1)) {
                Log.d(TAG,"End of rcv-Starting query")
                val nextQuery = urlsRef.orderBy("resID",Query.Direction.ASCENDING).startAfter(lastVisible).limit(queryLimit).get().addOnCompleteListener { task ->
                    if(task.isSuccessful) {
                        Log.d(TAG,"Next query called")
                        for(document:DocumentSnapshot in task.result!!) {
                            iconUrls.add(document.get("url").toString())
                        }
                        lastVisible = task.result!!.documents[task.result!!.size()-1]
                        adapter.notifyDataSetChanged()
                    }
                }
            }
        }
    })

    query.get().addOnCompleteListener {task: Task<QuerySnapshot> ->
        if(task.isSuccessful) {
            Log.d(TAG,"Success")
            for(document:DocumentSnapshot in task.result!!) {
                Log.d(TAG,"Task size = " + task.result!!.size())
                iconUrls.add(document.get("url").toString()) // add the url to the list
            }
            lastVisible = task.result!!.documents[task.result!!.size()-1]
            adapter.notifyDataSetChanged() // notify the adapter about the new data
        }
    }
}

这是 recyclerview 适配器:

public class CardIconAdapter extends RecyclerView.Adapter<CardIconAdapter.ViewHolder> {

    private List<String> urlsList;
    private Context context;

    class ViewHolder extends RecyclerView.ViewHolder {
        ImageView iconImg;
        ViewHolder(@NonNull View view) {
            super(view);
            iconImg = view.findViewById(R.id.cardIcon);
        }
    }

    public CardIconAdapter(Context cntxt, List<String> data) {
        context = cntxt;
        urlsList = data;
    }

    @NonNull
    @Override
    public CardIconAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view =  LayoutInflater.from(parent.getContext()).inflate(R.layout.card_icons_rcv_item,parent,false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull CardIconAdapter.ViewHolder holder, int position) {
        RequestOptions requestOptions = RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.ALL);
        GlideApp.with(context).load(urlsList.get(position)).thumbnail(0.25f).centerCrop().dontTransform().apply(requestOptions).into(holder.iconImg);
    }

    @Override
    public int getItemCount() {
        return urlsList.size();
    }
}

【问题讨论】:

  • @MartinZeitler 为什么它是重复的?在您链接的帖子中,我在 RecyclerView 中通过 Glide 更快地加载图像时遇到了问题,但问题已解决(尽管用户仍然没有添加答案),而在这篇文章中,我的 Recycler 视图存在性能问题(滞后, UI 滞后、删除以前的数据等)。
  • 对我来说,这似乎是同一个问题;尝试缓存这些缩略图而不是即时缩小它们(这可能会浪费电池,因为它需要 CPU)。已经以预期的大小为它们提供服务将完全消除操纵它们的需要(并且 Glide 不需要这样做)。我的意思是,缩小一个图像是没有问题的 - 但缩小一大堆图像是。
  • @MartinZeitler 不会自动缓存它们吗?即使我没有即时对它们进行下采样,我仍然会遇到长时间加载问题,但现在回收器视图也滞后,并且之前的数据加载很奇怪(如果它们完全加载的话)
  • 您的回收站视图实现有点不同。请发布您的firebase db节点结构数据将如何存储。

标签: android android-recyclerview google-cloud-firestore android-glide


【解决方案1】:

经过多次反复试验,我终于弄清楚了这个问题。我的第一个错误是没有为 recyclerview 项目发布我的 xml 布局文件,因为这是性能问题的根源。第二个错误是我使用的是 LinearLayout 并将其自己的 layout_width 和 layout_height 属性设置为 75dp,而不是嵌套在 LinearLayout 内的 ImageView,并且将 wrap_content 用于 ImageView 的各自属性。因此,为了解决性能问题,我执行了以下操作:

  1. 我将LinearLayout 更改为ConstraintLayout(我读到它总体上更加优化)
  2. 我将 ConstraintLayout 的 layout_widthlayout_height 属性设置为 wrap_content,最后
  3. 我将实际 ImageView 的 layout_width 和 layout_height 属性设置为 75dp,这是我希望图像的实际尺寸

这里是recyclerview每一项更改后的最终布局文件:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="horizontal"
    android:padding="5dp"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <ImageView
        android:id="@+id/cardIcon"
        android:layout_width="75dp"
        android:layout_height="75dp"
        android:contentDescription="cardIcon"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:srcCompat="@tools:sample/avatars" />
</androidx.constraintlayout.widget.ConstraintLayout>

【讨论】:

    【解决方案2】:

    在加载大量图像时修复滞后的一些方法

    • 为您的RecyclerView 设置固定大小,但如果涉及分页,它不会帮助您

      mRecyclerView.setHasFixedSize(true);
      
    • 像这样使用滑动覆盖图像的像素

      Glide.with(this)
           .load(YOUR_URL)
           .apply(new RequestOptions().override(dimenInPx, dimenInPx)
           .placeholder(R.drawable.placeHolder).error(R.drawable.error_image))
           .into(imageview);
      
    • hardwareAccelerated="true" 属性添加到清单内的活动标签中,例如

      <activity
           ...
           android:hardwareAccelerated="true"
           ...
      />
      

    我认为这可能会有所帮助。

    【讨论】:

    • 遗憾的是,我看不出有什么不同,我的意思是他们在 2 分钟后不停地加载而没有进展,而且还在继续。这是用户界面的样子:imgur.com/a/Ie45UCi。我还注意到,在 logcat 中我没有得到任何实际错误,所以我发现很难找到这个问题的根源。
    • 覆盖图像的高度和宽度可以提高recyclerview的性能,如果你需要在加载时显示进度参考这个stackoverflow.com/a/55632581/10239870
    【解决方案3】:

    【讨论】:

    • 我实际上是在分页数据,正如您在我的代码中看到的那样。我加载数据页面,其中每个页面包含 50 个图标,并且仅在滚动到回收站视图底部后才请求新的图标。我只是没有使用 Paging 库,因为我认为对于如此简单的数据(字符串),浏览所有文档等是不值得的。您是否尝试过类似的事情并看到显着的性能改进?
    【解决方案4】:

    This answer from Github was very helpful

    基本上,如果您设置了图像的确切尺寸,Glide 就不必不断地计算图像需要多大,从而显着提高性能。这有助于将我的 RecyclerView 从不可用变为完全正常,只需在我正在加载的 ImageView 上设置定义的高度和宽度。

    【讨论】:

      【解决方案5】:

      相信我。不要在接受的答案中使用ConstraintLayout。与LinearLayout 相比,它的性能较低。根据我的测量,使用 ConstraintLayout 填充布局文件平均需要 16 毫秒,而使用 LinearLayout 填充布局文件需要 10 毫秒。要获得平滑的滚动效果,它必须至少在 16 毫秒内膨胀布局。

      在 Glide 示例应用 here 中,他们创建了一个 SquareImageView,它返回相同的宽度和高度。

      class SquareImageView @JvmOverloads constructor(
          context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
      ) :
          AppCompatImageView(context, attrs, defStyleAttr) {
      
          override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
              super.onMeasure(widthMeasureSpec, widthMeasureSpec)
          }
      }
      

      别忘了在attrs.xml中声明styleable

      <LinearLayout
          android:layout_width="match_parent"
          android:layout_height="wrap_content"
          android:orientation="vertical">
      
          <com.example.SquareImageView
              android:id="@+id/image_view"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:padding="@dimen/padding_normal"
              android:scaleType="centerCrop" />
      
          <TextView
              android:id="@+id/text_view"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:padding="@dimen/padding_small"
              android:textAlignment="center"
              android:textColor="@color/secondaryTextColor"
              android:textSize="15sp" />
      </LinearLayout>
      

      尽量避免在RecyclerView 中使用ConstraintLayout,因为它会在第一次膨胀 ViewHolders 时导致一些延迟。

      另一种选择是预膨胀一些视图并在onCreateViewHolder中使用它们

      init {
          val layoutInflater = LayoutInflater.from(context)
          mPreInflated = List(20) {
              MyLayoutBinding.inflate(layoutInflater, null, false)
          }.iterator()
      }
      
      override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
          val binding = if (mPreInflated.hasNext()) {
              mPreInflated.next()
          } else {
              val layoutInflater = LayoutInflater.from(context)
              MyLayoutBinding.inflate(layoutInflater, null, false)
          }
          return MyViewHolder(binding)
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-06-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多