Scrolling-Leistung von Android RecyclerView


86

Ich habe ein RecyclerView-Beispiel erstellt, das auf dem Handbuch zum Erstellen von Listen und Karten basiert . Mein Adapter hat eine Musterimplementierung nur zum Aufblasen des Layouts.

Das Problem ist die schlechte Bildlaufleistung. Dies in einer RecycleView mit nur 8 Elementen.

In einigen Tests habe ich überprüft, dass dieses Problem in Android L nicht auftritt. In der KitKat-Version ist jedoch eine Leistungsminderung erkennbar.


1
Versuchen Sie, das ViewHolder-Entwurfsmuster für die Bildlaufleistung zu verwenden: developer.android.com/training/improving-layouts/…
Haresh Chhelana

@ HareshChhelana danke für deine Antwort! Aber ich benutze bereits ViewHolder-Muster, laut dem Link: developer.android.com/training/material/lists-cards.html
falvojr

2
Können Sie Code über Ihr Adapter-Setup und die XML-Datei für Ihre Layouts freigeben? Das sieht nicht normal aus. Haben Sie auch ein Profil erstellt und gesehen, wo die Zeit verbracht wird?
Yigit

2
Ich habe fast die gleichen Probleme. Außer es ist schnell in Pre Lollipop und unglaublich (wirklich) langsam in Android L.
Servus7

1
Können Sie auch die Version der Bibliothek freigeben, die Sie importieren?
Droidekas

Antworten:


227

Ich hatte kürzlich das gleiche Problem, daher habe ich Folgendes mit der neuesten RecyclerView-Unterstützungsbibliothek getan:

  1. Ersetzen Sie ein komplexes Layout (verschachtelte Ansichten, RelativeLayout) durch das neue optimierte ConstraintLayout. Aktivieren Sie es in Android Studio: Gehen Sie zu SDK Manager -> Registerkarte SDK Tools -> Support Repository -> überprüfen Sie ConstraintLayout für Android und Solver für ConstraintLayout. Zu den Abhängigkeiten hinzufügen:

    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    
  2. Wenn möglich, stellen Sie alle Elemente der RecyclerView auf dieselbe Höhe . Und füge hinzu:

    recyclerView.setHasFixedSize(true);
    
  3. Verwenden Sie die Standardmethoden für den RecyclerView- Zeichnungscache und passen Sie sie an Ihren Fall an. Dazu benötigen Sie keine Drittanbieter-Bibliothek:

    recyclerView.setItemViewCacheSize(20);
    recyclerView.setDrawingCacheEnabled(true);
    recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);
    
  4. Wenn Sie viele Bilder verwenden , stellen Sie sicher, dass deren Größe und Komprimierung optimal sind . Das Skalieren von Bildern kann sich auch auf die Leistung auswirken. Das Problem hat zwei Seiten: das verwendete Quellbild und die decodierte Bitmap. Das folgende Beispiel gibt Ihnen einen Hinweis zum Dekodieren eines aus dem Internet heruntergeladenen Bildes:

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

The most important part is specifying inPreferredConfig - it defines how many bytes will be used for each pixel of the image. Keep in mind that this is a preferred option. If the source image has more colors, it will still be decoded with a different config.

  1. Make sure onBindViewHolder() is as cheap as possible. You can set OnClickListener once in onCreateViewHolder() and call through an interface a listener outside of the Adapter, passing the clicked item. This way you don't create extra objects all the time. Also check flags and states, before making any changes to the view here.

    viewHolder.itemView.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View view) {
              Item item = getItem(getAdapterPosition());
              outsideClickListener.onItemClicked(item);
          }
    });
    
  2. When data gets changed, try to update only the affected items. For example instead of invalidating the whole data set with notifyDataSetChanged(), when adding / loading more items, just use:

    adapter.notifyItemRangeInserted(rangeStart, rangeEnd);
    adapter.notifyItemRemoved(position);
    adapter.notifyItemChanged(position);
    adapter.notifyItemInserted(position);
    
  3. From Android Developer Web Site :

Rely on notifyDataSetChanged() as a last resort.

But if you need to use it, maintain your items with unique ids:

    adapter.setHasStableIds(true);

RecyclerView will attempt to synthesize visible structural change events for adapters that report that they have stable IDs when this method is used. This can help for the purposes of animation and visual object persistence but individual item views will still need to be rebound and relaid out.

Even if you do everything right, chances are that the RecyclerView is still not performing as smoothly as you would like.


18
one vote for adapter.setHasStableIds(true); method that really helped to make recyclerview fast.
Atula

1
Part 7 is totally wrong! setHasStableIds(true) does will do nothing except you use adapter.notifyDataSetChanged(). Link: developer.android.com/reference/android/support/v7/widget/…
localhost

1
I can see why the recyclerView.setItemViewCacheSize(20); can improuve the performance. But recyclerView.setDrawingCacheEnabled(true); and recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH); though! I am not sure these will change anything. These are View specific calls that allows you to programmatically retrieve the drawing cache as a bitmap and use it for your advantage later. RecyclerView does not seem to do anything about it.
Abdelhakim AKODADI

1
@AbdelhakimAkodadi, the scrolling gets smooth with cache. I've tested it. How could you say otherwise, it's obvious. Of course, if someone scrolls like crazy nothing will help. I just show the other options like setDrawingCacheQuality, which I don't use, because image quality is important in my case. I don't preach DRAWING_CACHE_QUALI‌​TY_HIGH, but suggest whosoever is interested to delve deeper and tweak the option.
Galya

2
setDrawingCacheEnabled() and setDrawingCacheQuality() are deprecated. Instead use hardware acceleration. developer.android.com/reference/android/view/…
Shayan_Aryan


13

I discovered at least one pattern that can kill your performance. Remember that onBindViewHolder() is called frequently. So anything you do in that code has the potential to slam your performance to halt. If your RecyclerView does any customization, it's very easy to accidentally put some slow code in this method.

I was changing the background images of each RecyclerView depending on the position. But loading images takes a bit of work, causing my RecyclerView to be sluggish and jerky.

Creating a cache for the images worked wonders; onBindViewHolder() now just modifies a reference to a cached image instead of loading it from scratch. Now the RecyclerView zips along.

I know that not everyone will have this exact problem, so I'm not bothering to load code. But please consider any work that is done in your onBindViewHolder() as a potential bottle-neck for poor RecyclerView performance.


I'm having the same problem. Right now I'm using Fresco for image loading and caching. Do you have another, better solution to load and cache images within RecyclerView. Thanks.
Androidicus

I am unfamiliar with Fresco (reading...their promises are nice). Maybe they have some insights on how to best use their caches with RecyclerViews. And I see you're not the only one with this problem: github.com/facebook/fresco/issues/414 .
SMBiggs

10

In addition to @Galya's detailed answer, I want to state that even though it may be an optimization issue, it is also true that having the debugger enabled can slow things down a lot.

If you do everything to optimize your RecyclerView and it still doesn't work smoothly, try switching your build variant to release, and check how it works in a non-development environment (with the debugger disabled).

It happened to me that my app was performing slowly in debug build variant, but as soon as I switched to the release variant it worked smoothly. This doesn't mean that you should develop with the release build variant, but it is good to know that whenever you are ready to ship your app, it will work just fine.


This comment was really helpful for me! I have tried everything to improve my recyclerview's performance but nothing really helped, but once I have switched to the release build I realized everything is just fine.
Tal Barda

1
I faced the same issue even without the debugger attached but as soon as moved to release build,issue was no longer to be seen
Farmaan Elahi

8

I had a talk about RecyclerView's performance. Here are slides in English and recorded video in Russian.

It contains a set of techniques (some of them are already covered by @Darya's answer).

Here is a brief summary:

  • If Adapter items have fixed size then set:
    recyclerView.setHasFixedSize(true);

  • If data entities can be represented by long (hashCode() for instance) then set:
    adapter.hasStableIds(true);
    and implement:
    // YourAdapter.java
    @Override
    public long getItemId(int position) {
    return items.get(position).hashcode(); //id()
    }
    In this case Item.id() would not work, because it would stay the same even if Item's content has changed.
    P.S. This is not necessary if you are using DiffUtil!

  • Use correctly scaled bitmap. Don't reinvent the wheel and use libraries.
    More info how to choose here.

  • Always use the latest version of RecyclerView. For instance, there were huge performance improvements in 25.1.0 - prefetch.
    More info here.

  • Use DiffUtill.
    DiffUtil is a must.
    Official documentation.

  • Simplify your item's layout!
    Tiny library to enrich TextViews - TextViewRichDrawable

See slides for more detailed explanation.


7

I'm not really sure if the usage of setHasStableId flag is going to fix your issue. Based on the information you provide your performance issue could be related to a memory issue. Your application performance in terms of user interface and memory is quite related.

Last week I discovered my app was leaking memory. I discovered this because after 20 minutes using my app I noticed the UI was performing really slow. Closing/opening an activity or scrolling a RecyclerView with a bunch of elements was really slow. After monitoring some of my users in production using http://flowup.io/ I found this:

enter image description here

The frame time was really really high and the frames per second really really low. You can see that some frames needed about 2 seconds to render :S.

Trying to figure it out what was causing this bad frame time/fps I discovered I had a memory issue as you can see here:

enter image description here

Even when the average memory consumption was close to the 15MB at the same time the app was dropping frames.

That's how I discovered the UI issue. I had a memory leak in my app causing a lot of garbage collector events and that's was causing the bad UI performance because the Android VM had to stop my app to collect memory every single frame.

Looking at the code I had a leak inside a custom view because I was not unregistering a listener from the Android Choreographer instance. After releasing the fix, everything became normal :)

If your app is dropping frames due to a memory issue you should review two common errors:

Review if your app is allocating objects inside a method invoked multiple times per second. Even if this allocation can be performed in a different place where your application is becoming slow. An example could be creating new instances of an object inside a onDraw custom view method on onBindViewHolder in your recycler view view holder. Review if your app is registering an instance into the Android SDK but not releasing it. Registering a listener into a bus event could also be possible leak.

Disclaimer: The tool I've been using to monitor my app is under development. I have access to this tool because I'm one of the developers :) If you want access to this tool we will release a beta version soon! You can join in our web site: http://flowup.io/.

If you want to use different tools you can use: traveview, dmtracedump, systrace or the Andorid performance monitor integrated into Android Studio. But remember that this tools will monitor your connected device and not the rest of your user devices or Android OS installations.


3

Its also important to check the parent layout in which you put in your Recyclerview. I had a similar scrolling issues when I testing recyclerView in a nestedscrollview. A view that scrolls in another view that scroll can suffer in performance during scrolling


Yes valid point. Me also face the same issue!
Ranjith Kumar

2

In my case, I found out that the notable cause of the lag is frequent drawable loading inside #onBindViewHolder() method. I solved it just by loading the images as Bitmap once inside the ViewHolder and access it from the mentioned method. That is all I did.


2

In my RecyclerView, I use Bitmap Images For background of my item_layout.
Everything @Galya said is true (and I thank him for his great answer). But they didn't work for me.

This is what solved my problem:

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

For more information please read this Answer.


2

In mycase I have complex recyclerview childs. So It affected the activity loading time (~5 sec for activity rendering)

I load the adapter with postDelayed() -> this will give the good result for activity rendering. after activity rendering my recyclerview load with smooth.

Try this answer,

    recyclerView.postDelayed(new Runnable() {
        @Override
        public void run() {
            recyclerView.setAdapter(mAdapter);
        }
    },100); 

1

I see in the comments that you are already implementing the ViewHolder pattern, but I will post an example adapter here that uses the RecyclerView.ViewHolder pattern so you can verify that you are integrating it in a similar way, again your constructor can vary depending on your needs, here is an example:

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

    Context mContext;
    List<String> mNames;

    public RecyclerAdapter(Context context, List<String> names) {
        mContext = context;
        mNames = names;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
        View view = LayoutInflater.from(viewGroup.getContext())
                .inflate(android.R.layout.simple_list_item_1, viewGroup, false);

        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(ViewHolder viewHolder, int position) {
        //Populate.
        if (mNames != null) {
            String name = mNames.get(position);

            viewHolder.name.setText(name);
        }
    }

    @Override
    public int getItemCount() {

        if (mNames != null)
            return mNames.size();
        else
            return 0;
    }

    /**
     * Static Class that holds the RecyclerView views. 
     */
    static class ViewHolder extends RecyclerView.ViewHolder {
        TextView name;

        public ViewHolder(View itemView) {
            super(itemView);
            name = (TextView) itemView.findViewById(android.R.id.text1);
        }
    }
}

If you have any trouble working with RecyclerView.ViewHolder make sure you have the appropriate dependencies which you can verify always at Gradle Please

Hope it resolves your problem.


1

This helped me getting more smooth scrolling:

override the onFailedToRecycleView(ViewHolder holder) in the adapter

and stop any ongoing animations (if any) holder."animateview".clearAnimation();

remember to return true;


1

I solved it by this line of code

recyclerView.setNestedScrollingEnabled(false);

1

Adding to @Galya's answer, in bind viewHolder,I was using Html.fromHtml() method. apparently this has performance impact.


0

i Solve this issue by using the only one line in with Picasso library

.fit()

Picasso.get().load(currentItem.getArtist_image())

                    .fit()//this wil auto get the size of image and reduce it 

                    .placeholder(R.drawable.ic_doctor)
                    .into(holder.img_uploaderProfile, new Callback() {
                        @Override
                        public void onSuccess() {


                        }

                        @Override
                        public void onError(Exception e) {
                            Toast.makeText(context, "Something Happend Wrong Uploader Image", Toast.LENGTH_LONG).show();
                        }
                    });
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.