Wydajność przewijania RecyclerView systemu Android


86

Przykład RecyclerView stworzyłem na podstawie przewodnika Tworzenie list i kart . Mój adapter ma implementację wzorca tylko do zawyżania układu.

Problem polega na słabej wydajności przewijania. To w RecycleView z tylko 8 elementami.

W niektórych testach sprawdziłem, że w Androidzie L ten problem nie występuje. Ale w wersji KitKat spadek wydajności jest ewidentny.


1
Spróbuj użyć wzorca projektowego ViewHolder do przewijania wydajności: developer.android.com/training/improving-layouts/ ...
Haresh Chhelana

@HareshChhelana dzięki za odpowiedź! Ale już używam wzorca ViewHolder, zgodnie z linkiem: developer.android.com/training/material/lists-cards.html
falvojr

2
Czy możesz udostępnić kod dotyczący konfiguracji karty i plik XML dla swoich układów. To nie wygląda normalnie. Czy profilowałeś i sprawdzałeś, gdzie spędza się czas?
yigit

2
Mam prawie te same problemy. Z wyjątkiem tego, że jest szybki w wersji przed Lollipop i niesamowicie (naprawdę) wolny w systemie Android L.
Servus7

1
czy możesz również udostępniać wersję importowanej biblioteki.
Droidekas

Odpowiedzi:


227

Niedawno spotkałem się z tym samym problemem, więc oto, co zrobiłem z najnowszą biblioteką wsparcia RecyclerView:

  1. Zastąp złożony układ (widoki zagnieżdżone, RelativeLayout) nowym zoptymalizowanym ConstraintLayout. Aktywuj go w Android Studio: Idź do SDK Manager -> zakładka SDK Tools -> Support Repository -> sprawdź ConstraintLayout for Android & Solver for ConstraintLayout. Dodaj do zależności:

    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    
  2. Jeśli to możliwe, nadaj wszystkim elementom RecyclerView tę samą wysokość . I dodaj:

    recyclerView.setHasFixedSize(true);
    
  3. Użyj domyślnych metod pamięci podręcznej rysunków RecyclerView i dostosuj je do swojego przypadku. Nie potrzebujesz do tego biblioteki innej firmy:

    recyclerView.setItemViewCacheSize(20);
    recyclerView.setDrawingCacheEnabled(true);
    recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);
    
  4. Jeśli używasz wielu obrazów , upewnij się, że ich rozmiar i kompresja są optymalne . Skalowanie obrazów może również wpływać na wydajność. Istnieją dwie strony problemu - użyty obraz źródłowy i zdekodowana mapa bitowa. Poniższy przykład podpowiada, jak zdekodować obraz pobrany z sieci:

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

Najważniejsze jest określenie inPreferredConfig- określa, ile bajtów zostanie wykorzystanych na każdy piksel obrazu. Pamiętaj, że jest to preferowana opcja. Jeśli obraz źródłowy ma więcej kolorów, nadal będzie dekodowany przy użyciu innej konfiguracji.

  1. Upewnij się, że onBindViewHolder () jest tak tani, jak to tylko możliwe. Możesz ustawić OnClickListener raz onCreateViewHolder()i wywołać przez interfejs nasłuchiwanie poza adapterem, przekazując kliknięty element. W ten sposób nie tworzysz przez cały czas dodatkowych obiektów. Sprawdź także flagi i stany przed wprowadzeniem jakichkolwiek zmian w widoku tutaj.

    viewHolder.itemView.setOnClickListener(new View.OnClickListener() {
          @Override
          public void onClick(View view) {
              Item item = getItem(getAdapterPosition());
              outsideClickListener.onItemClicked(item);
          }
    });
    
  2. Gdy dane ulegną zmianie, spróbuj zaktualizować tylko te elementy . Na przykład zamiast unieważniać cały zestaw danych notifyDataSetChanged()przy dodawaniu / ładowaniu kolejnych elementów, wystarczy użyć:

    adapter.notifyItemRangeInserted(rangeStart, rangeEnd);
    adapter.notifyItemRemoved(position);
    adapter.notifyItemChanged(position);
    adapter.notifyItemInserted(position);
    
  3. Z witryny sieci Web dla programistów Androida :

W ostateczności polegaj na notifyDataSetChanged ().

Ale jeśli musisz go użyć, utrzymuj swoje produkty z unikalnymi identyfikatorami :

    adapter.setHasStableIds(true);

RecyclerView spróbuje zsyntetyzować widoczne zdarzenia zmian strukturalnych dla adapterów, które zgłaszają, że mają stabilne identyfikatory, gdy używana jest ta metoda. Może to pomóc w przypadku animacji i trwałości obiektów wizualnych, ale widoki poszczególnych elementów nadal będą musiały zostać odtworzone i ponownie ułożone.

Nawet jeśli zrobisz wszystko dobrze, są szanse, że RecyclerView nadal nie działa tak płynnie, jak byś chciał.


18
jeden głos na adapter.setHasStableIds (true); metoda, która naprawdę pomogła przyspieszyć przeglądanie recyklingu.
Atula

1
Część 7 jest całkowicie błędna! setHasStableIds (true) nie zrobi nic poza użyciem adapter.notifyDataSetChanged (). Link: developer.android.com/reference/android/support/v7/widget/ ...
localhost

1
Rozumiem, dlaczego recyclerView.setItemViewCacheSize(20);mogą poprawić wydajność. Ale recyclerView.setDrawingCacheEnabled(true);i recyclerView.setDrawingCacheQuality(View.DRAWING_CACHE_QUALITY_HIGH);chociaż! Nie jestem pewien, czy to coś zmieni. Są to Viewokreślone wywołania, które umożliwiają programowe pobranie pamięci podręcznej rysunku jako mapy bitowej i wykorzystanie jej później. RecyclerViewwydaje się, że nic z tym nie robi.
Abdelhakim AKODADI

1
@AbdelhakimAkodadi, przewijanie staje się płynne dzięki pamięci podręcznej. Przetestowałem to. Jak możesz powiedzieć inaczej, to oczywiste. Oczywiście, jeśli ktoś przewija się jak szalony, nic nie pomoże. Po prostu pokazuję inne opcje, takie jak setDrawingCacheQuality, których nie używam, ponieważ jakość obrazu jest w moim przypadku ważna. Nie głoszę DRAWING_CACHE_QUALI‌ TY_HIGH, ale sugeruję każdemu, kto jest zainteresowany, aby zagłębił się głębiej i ulepszył opcję.
Galya

2
setDrawingCacheEnabled () i setDrawingCacheQuality () są przestarzałe. Zamiast tego użyj akceleracji sprzętowej. developer.android.com/reference/android/view/…
Shayan_Aryan

14

2
To powoduje dla mnie niewielką poprawę wydajności. Ale RecyclerView nadal działa powoli - znacznie wolniej niż równoważny niestandardowy ListView.
SMBiggs,

Ach, ale odkryłem, dlaczego mój kod działa tak wolno - nie ma to nic wspólnego z setHasStableIds (). Napiszę odpowiedź z dodatkowymi informacjami.
SMBiggs

13

Odkryłem co najmniej jeden wzorzec, który może zabić twoją wydajność. Pamiętaj, że onBindViewHolder()jest często nazywany . Więc wszystko, co robisz w tym kodzie, może spowodować zatrzymanie wydajności. Jeśli Twój RecyclerView dokonuje jakichkolwiek dostosowań, bardzo łatwo jest przypadkowo umieścić powolny kod w tej metodzie.

Zmieniałem obrazy tła każdego RecyclerView w zależności od pozycji. Ale ładowanie obrazów wymaga trochę pracy, przez co mój RecyclerView jest powolny i szarpany.

Stworzenie pamięci podręcznej dla obrazów zdziałało cuda; onBindViewHolder()teraz tylko modyfikuje odniesienie do obrazu w pamięci podręcznej zamiast ładować go od zera. Teraz RecyclerView zapina się.

Wiem, że nie każdy będzie miał dokładnie ten problem, więc nie kłopoczę się ładowaniem kodu. Ale proszę uważać każdą pracę wykonaną w Państwa onBindViewHolder()za potencjalną szyjkę butelki dla słabej wydajności RecyclerView.


Mam ten sam problem. Obecnie używam Fresco do ładowania i buforowania obrazu. Czy masz inne, lepsze rozwiązanie do ładowania i buforowania obrazów w RecyclerView. Dzięki.
Androidicus

Nie znam Fresco (czytam ... ich obietnice są miłe). Może mają wgląd w to, jak najlepiej wykorzystywać swoje pamięci podręczne za pomocą RecyclerViews. I widzę, że nie jesteś jedyny z tym problemem: github.com/facebook/fresco/issues/414 .
SMBiggs

10

Oprócz szczegółowej odpowiedzi @ Galya, chcę stwierdzić, że chociaż może to być problem optymalizacji, prawdą jest również, że włączenie debugera może znacznie spowolnić.

Jeśli robisz wszystko, aby zoptymalizować swój program RecyclerViewi nadal nie działa on płynnie, spróbuj przełączyć wariant kompilacji na releasei sprawdź, jak działa w środowisku innym niż programistyczne (z wyłączonym debugerem).

Zdarzyło mi się, że moja aplikacja działała wolno w debugwariancie kompilacji, ale jak tylko przełączyłem się na releasewariant, działała płynnie. Nie oznacza to, że powinieneś programować z releasewariantem kompilacji, ale dobrze jest wiedzieć, że zawsze, gdy będziesz gotowy do wysłania aplikacji, będzie działać dobrze.


Ten komentarz był dla mnie bardzo pomocny! Próbowałem wszystkiego, aby poprawić wydajność widoku recyklingu, ale nic tak naprawdę nie pomogło, ale kiedy przestawiłem się na wersję wydania, zdałem sobie sprawę, że wszystko jest w porządku.
Tal Barda,

1
Napotkałem ten sam problem nawet bez dołączonego debugera, ale gdy tylko przeniosłem się do wydania kompilacji, problemu już nie było
Farmaan Elahi

8

Rozmawiałem o RecyclerViewwystępie. Oto slajdy w języku angielskim i nagrane wideo w języku rosyjskim .

Zawiera zestaw technik (niektóre z nich są już objęte odpowiedzią @ Darya ).

Oto krótkie podsumowanie:

  • Jeśli Adapterprzedmioty mają stały rozmiar, ustaw:
    recyclerView.setHasFixedSize(true);

  • Jeśli jednostki danych mogą być reprezentowane przez długie ( hashCode()na przykład), ustaw:
    adapter.hasStableIds(true);
    i zaimplementuj:
    // YourAdapter.java
    @Override
    public long getItemId(int position) {
    return items.get(position).hashcode(); //id()
    }
    W tym przypadku Item.id()nie zadziała, ponieważ pozostanie niezmienione, nawet jeśli Itemzmieni się zawartość.
    PS Nie jest to konieczne, jeśli używasz DiffUtil!

  • Użyj poprawnie przeskalowanej mapy bitowej. Nie odkrywaj na nowo koła i nie korzystaj z bibliotek.
    Więcej informacji jak wybrać tutaj .

  • Zawsze używaj najnowszej wersji RecyclerView. Na przykład nastąpiła ogromna poprawa wydajności 25.1.0- pobieranie wstępne.
    Więcej informacji tutaj .

  • Użyj DiffUtill.
    DiffUtil jest koniecznością .
    Oficjalna dokumentacja .

  • Uprość układ przedmiotu!
    Niewielka biblioteka wzbogacająca TextViews - TextViewRichDrawable

Zobacz slajdy, aby uzyskać bardziej szczegółowe wyjaśnienia.


7

Nie jestem pewien, czy użycie setHasStableIdflagi rozwiąże Twój problem. Na podstawie podanych informacji Twój problem z wydajnością może być związany z problemem z pamięcią. Wydajność aplikacji pod względem interfejsu użytkownika i pamięci jest dość powiązana.

W zeszłym tygodniu odkryłem, że moja aplikacja przecieka pamięć. Odkryłem to, ponieważ po 20 minutach korzystania z mojej aplikacji zauważyłem, że interfejs użytkownika działa bardzo wolno. Zamykanie / otwieranie działania lub przewijanie RecyclerView z kilkoma elementami było naprawdę powolne. Po monitorowaniu niektórych moich użytkowników w produkcji za pomocą http://flowup.io/ znalazłem to:

wprowadź opis obrazu tutaj

Czas klatek był naprawdę wysoki, a liczba klatek na sekundę naprawdę niska. Jak widać, niektóre klatki potrzebowały około 2 sekund na renderowanie: S.

Próbując dowiedzieć się, co powodowało ten zły czas wyświetlania klatek / fps, odkryłem, że mam problem z pamięcią, jak widać tutaj:

wprowadź opis obrazu tutaj

Nawet gdy średnie zużycie pamięci było bliskie 15 MB, w tym samym czasie aplikacja gubiła klatki.

W ten sposób odkryłem problem z interfejsem użytkownika. Miałem wyciek pamięci w mojej aplikacji, powodując wiele zdarzeń odśmiecania pamięci, co powodowało złą wydajność interfejsu użytkownika, ponieważ maszyna wirtualna z systemem Android musiała zatrzymać moją aplikację, aby zebrać pamięć w każdej klatce.

Patrząc na kod, miałem wyciek w widoku niestandardowym, ponieważ nie wyrejestrowywałem nasłuchiwania z instancji Android Choreographer. Po wypuszczeniu poprawki wszystko wróciło do normy :)

Jeśli Twoja aplikacja gubi ramki z powodu problemu z pamięcią, zapoznaj się z dwoma typowymi błędami:

Sprawdź, czy Twoja aplikacja alokuje obiekty wewnątrz metody wywoływanej wiele razy na sekundę. Nawet jeśli ta alokacja może zostać wykonana w innym miejscu, w którym aplikacja staje się wolniejsza. Przykładem może być tworzenie nowych instancji obiektu wewnątrz niestandardowej metody widoku onDraw w onBindViewHolder w uchwycie widoku recyklera. Sprawdź, czy Twoja aplikacja rejestruje wystąpienie w Android SDK, ale go nie zwalnia. Zarejestrowanie słuchacza w zdarzeniu autobusowym może być również możliwym wyciekiem.

Zastrzeżenie: narzędzie, którego używam do monitorowania mojej aplikacji, jest w trakcie opracowywania. Mam dostęp do tego narzędzia, ponieważ jestem jednym z programistów :) Jeśli chcesz mieć dostęp do tego narzędzia, wkrótce udostępnimy wersję beta! Możesz dołączyć na naszej stronie internetowej: http://flowup.io/ .

Jeśli chcesz korzystać z różnych narzędzi, możesz użyć: traveview, dmtracedump, systrace lub monitora wydajności Andorid zintegrowanego z Android Studio. Pamiętaj jednak, że te narzędzia będą monitorować podłączone urządzenie, a nie pozostałe urządzenia użytkowników lub instalacje systemu operacyjnego Android.


3

Ważne jest również sprawdzenie układu nadrzędnego, w którym umieściłeś widok recyklingu. Miałem podobne problemy z przewijaniem, gdy testowałem recyclinglerView w nestedscrollview. Widok, który jest przewijany w innym widoku, którego przewijanie może pogorszyć wydajność podczas przewijania


Tak, ważny punkt. Ja też mam ten sam problem!
Ranjith Kumar

2

W moim przypadku dowiedziałem się, że istotną przyczyną opóźnienia jest częste ładowanie ciągnione #onBindViewHolder()metodą wewnętrzną . Rozwiązałem to po prostu ładując obrazy jako Bitmap raz w ViewHolder i uzyskując do nich dostęp z wymienionej metody. To wszystko, co zrobiłem.


2

W moim RecyclerView używam obrazów bitmapowych jako tła mojego elementu item_layout.
Wszystko, co powiedział @Galya, jest prawdą (i dziękuję mu za świetną odpowiedź). Ale dla mnie nie działały.

Oto, co rozwiązało mój problem:

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

Aby uzyskać więcej informacji, przeczytaj tę odpowiedź .


2

W Mycase mam złożone dzieci z recyklingu. Więc wpłynęło to na czas ładowania aktywności (~ 5 sekund do renderowania aktywności)

Ładuję adapter za pomocą postDelayed () -> to da dobry wynik do renderowania aktywności. po aktywności renderującej ładowanie mojego recyklingu z płynnym.

Wypróbuj tę odpowiedź,

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

1

Widzę w komentarzach, że już implementujesz ViewHolderwzorzec, ale wrzucę tutaj przykładowy adapter wykorzystujący RecyclerView.ViewHolderwzorzec, abyś mógł sprawdzić, czy integrujesz go w podobny sposób, znowu Twój konstruktor może się różnić w zależności od potrzeb, tutaj to przykład:

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);
        }
    }
}

Jeśli masz jakiekolwiek problemy z pracą, RecyclerView.ViewHolderupewnij się, że masz odpowiednie zależności, które możesz zawsze sprawdzić w Gradle Please

Mam nadzieję, że to rozwiązuje twój problem.


1

Pomogło mi to w płynniejszym przewijaniu:

Zastąp onFailedToRecycleView (uchwyt ViewHolder) w karcie

i zatrzymaj wszystkie trwające animacje (jeśli są). "animateview" .clearAnimation ();

pamiętaj, aby zwrócić prawdę;


1

Rozwiązałem to za pomocą tej linii kodu

recyclerView.setNestedScrollingEnabled(false);

1

Dodając do odpowiedzi @ Galya, w bind viewHolder użyłem metody Html.fromHtml (). najwyraźniej ma to wpływ na wydajność.


0

i Rozwiąż ten problem, używając jedynego wiersza w bibliotece Picassa

.dopasowanie()

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();
                        }
                    });
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.