Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Oleksandr Tolstykh (Sharewire B.V), RecyclerView Performance Tuning, CodeFest 2017

CodeFest
January 31, 2018

Oleksandr Tolstykh (Sharewire B.V), RecyclerView Performance Tuning, CodeFest 2017

https://2017.codefest.ru/lecture/1183

RecyclerView has been a common part of every Android application's development process since Google released the support library with optimized ListView replacement. RecyclerView is still in the development and improvement cycle and Android developers continuously receive updates for it. Besides improvements made by the Google team, there are some techniques which can be applied to RecyclerView to make it more efficient. Some of these approaches were introduced by Google engineers, some were discovered by enthusiastic developers. I have gathered all these best practices together, added my own ones and some experiments on top of them.

CodeFest

January 31, 2018
Tweet

More Decks by CodeFest

Other Decks in Technology

Transcript

  1. How to measure UI performance? • FPS https://github.com/friendlyrobotnyc/TinyDancer • Profile

    GPU rendering https://developer.android.com/studio/profile/dev-options-rendering.html • Aggregate frame stats https://developer.android.com/training/testing/performance.html • Many others... @a_tolstykh
  2. How to measure UI performance? • FPS https://github.com/friendlyrobotnyc/TinyDancer • Profile

    GPU rendering https://developer.android.com/studio/profile/dev-options-rendering.html • Aggregate frame stats https://developer.android.com/training/testing/performance.html • Many others... @a_tolstykh
  3. Sample App - Travel With Us City guides Base city

    info Data is loaded from server @a_tolstykh RecyclerView!
  4. Optimize Cells hierarchies Avoid deep hierarchy Save level with <merge>

    when <include> Layout matters! @a_tolstykh https://developer.android.com/topic/performance/rendering/optimizing-view-hierarchies.html android.support.constraint.ConstraintLayout com.google.android.flexbox.FlexboxLayout
  5. Minimize onBindViewHolder() Make onBindViewHolder() as cheap as possible Avoid item

    instantiations (memory allocations) Do as much as you can in onCreateViewHolder() @a_tolstykh
  6. Correct images scale Make sure their size and compression are

    optimal. Scaling images may also affect the performance. Do not reinvent the wheel. @a_tolstykh Picasso, ImageLoader, Fresco, Glide. Picasso v/s Imageloader v/s Fresco vs Glide [closed] http://stackoverflow.com/q/29363321/2308720
  7. Nested RecyclerView Override the LinearLayoutManager#getInitialPrefetchItemCount() @a_tolstykh public int getInitialPrefetchItemCount ()

    Defines how many inner items should be prefetched when this LayoutManager's RecyclerView is nested inside another RecyclerView Binding the inner RecyclerView doesn’t allocate any children. The prefetching system has to know how many.
  8. Remove an item with swipe: SimpleCallback touchCallback = new SimpleCallback(0,

    ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) { // ... @Override public void onSwiped(ViewHolder viewHolder, int swipeDir) { adapter.remove(viewHolder.getAdapterPosition()); adapter.notifyItemRemoved(viewHolder.getAdapterPosition()); } }; @a_tolstykh
  9. Items cache recyclerView.setItemViewCacheSize (int size); Documentation: “Set the number of

    offscreen views to retain before adding them to the potentially shared recycled view pool.” @a_tolstykh static final int DEFAULT_CACHE_SIZE = 2; // supportVersion = 25.3.0
  10. Stable ids - adapter.setHasStableIds() adapter.setHasStableIds(true); // YourAdapter.java @Override public long

    getItemId(int position) { return items.get(position).hashcode(); //id() } @a_tolstykh Only if item in the data set can be represented with a unique identifier Avoid unnecessary bindViewHolder() calls if view exists in RecyclerView’s cache
  11. Prefetch Use latest version of support library to use native

    prefetch optimizations supportVersion >= 25.1.0 // (enabled by default) RecyclerView Prefetch by Chet Haase https://medium.com/google-developers/c2f269075710 *Lollipop and newer @a_tolstykh
  12. Advanced prefetch Override the LinearLayoutManager#getExtraLayoutSpace(RecyclerView.State s) @a_tolstykh protected int getExtraLayoutSpace

    (RecyclerView.State state) Returns the amount of extra space that should be laid out by LayoutManager. By default, LinearLayoutManager lays out 1 extra page of items while smooth scrolling and 0 otherwise. You can override this method to implement your custom layout pre-cache logic.
  13. Advanced prefetch @a_tolstykh public class PreCachingLayoutManager extends LinearLayoutManager { //

    .... public void setExtraLayoutSpace(int extraLayoutSpace) { this.extraLayoutSpace = extraLayoutSpace; } @Override protected int getExtraLayoutSpace(RecyclerView.State state) { return extraLayoutSpace > 0 ? extraLayoutSpace : DEFAULT_EXTRA_SPACE; } }
  14. BUT... Expensive if done while the user may change scrolling

    direction. Laying out invisible elements generally comes with significant performance cost. Useless without increasing cache size (setItemViewCacheSize). @a_tolstykh BUT it improves USER EXPERIENCE! Need to find balance. Maybe 1 extra screen is The Balance?
  15. Even more advanced prefetch... Prefetch images! (on wi-fi only) @a_tolstykh

    Picasso.with(context).load(url).fetch(); Glide.with(context).load(url).downloadOnly(width, height);
  16. @a_tolstykh public class CitiesAdapter extends RecyclerView.Adapter<CityViewHolder> { public void updateData(List<City>

    cities) { if (onWifi()) prefetch(cities); // TODO update adapter data } private void prefetch(List<City> cities) { for (City city : cities) { PreFetcher prefetcher = city.getPrefetcher(); if (prefetcher != null) { prefetcher.prefetch(mContext); } } } }
  17. @a_tolstykh public class ImagePrefetcher implements PreFetcher { private final Image[]

    mImages; public ImagePrefetcher(Image... images) { this.mImages = images; } @Override public void prefetch(Context context) { for (Image image : mImages) { Picasso.with(context).load(image.url()).fetch(); } } }
  18. @a_tolstykh public class TweetPrefetcher implements PreFetcher { private final List<String>

    mIds; @Override public void prefetch(Context context) { TwitterHelper.loadTweets(mIds, new Callback<List<Tweet>>() { @Override public void success(Result<List<Tweet>> result) { for (Tweet tweet : result.data) { TwitterHelper.preFetchTweetImage(tweet); } } }); } }
  19. Displayed data pre-calculations Displayed items are POJOs. Pre-format date, time,

    other strings during data parsing. Pre-calculate immutable values during data parsing. @a_tolstykh Profit: + Minor performance optimisations. + Formatting is done in BG thread. + Formatting is done in single place.
  20. Displayed data pre-calculations (formatting) @WorkerThread private static Rating createRating(float value,

    int count) { String ratingFormatted = String.format(Locale.US, "%.1f", value); String countFormatted = formatWithMetricPrefix(count); return Rating.create(value, ratingFormatted, count, countFormatted); } @UiThread public void setRating(Rating rating) { ratingText.setText(rating.ratingFormatted()); reviewsCount.setText(rating.numberOfReviewsFormatted()); } @a_tolstykh
  21. android.support.v7.util.DiffUtil Documentation: “DiffUtil is a utility class that can calculate

    the difference between two lists and output a list of update operations that converts the first list into the second one. @a_tolstykh It can be used to calculate updates for a RecyclerView Adapter.” DiffUtil.Callback cb = new YourDiffCallback(oldList, newList); DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(cb); diffResult.dispatchUpdatesTo(adapter);
  22. DiffUtil.Callback public class DiffCallback extends DiffUtil.Callback { public DiffCallback(@NonNull List<City>

    newList, @NonNull List<City> oldList) {...} public int getOldListSize() {...} public int getNewListSize() {...} public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {...} public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {...} @Nullable public Object getChangePayload(int oldItemPosition, int newItemPosition) {...} } @a_tolstykh
  23. DiffUtil.Callback public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) When areItemsTheSame(int, int)

    returns true for two items and areContentsTheSame(int, int) returns false for them, DiffUtil calls this method to get a payload about the change. public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) Checks whether two items have the same data.You can change its behavior depending on your UI. This method is called by DiffUtil only if areItemsTheSame returns true. @a_tolstykh
  24. DiffUtil.Callback @Override public int getOldListSize() { return oldCities.size(); } @Override

    public int getNewListSize() { return newCities.size(); } @Override public boolean areItemsTheSame(int oldPosition, int newPosition) { return oldCities.get(oldPosition).id() == newCities.get(newPosition).id(); } @Override public boolean areContentsTheSame(int oldPosition, int newPosition) { return oldCities.get(oldPosition).equals(newCities.get(newPosition)); } @a_tolstykh
  25. DiffUtil.Callback public Object getChangePayload(int oldItemPosition, int newItemPosition) When areItemsTheSame(int, int)

    returns true for two items and areContentsTheSame(int, int) returns false for them, DiffUtil calls this method to get a payload about the change. @a_tolstykh For example, if you are using DiffUtil with RecyclerView, you can return the particular field that changed in the item and your ItemAnimator can use that information to run the correct animation.
  26. DiffUtil animated values change public Object getChangePayload(int oldItemPosition, int newItemPosition)

    { Bundle diff = new Bundle(); Rating newRating = newCities.get(newItemPosition).rating(); Rating oldRating = oldCities.get(oldItemPosition).rating(); if (newRating.numberOfReviews() != oldRating.numberOfReviews()) { diff.putString(KEY_NUMBER_OF_REVIEWS, newRating.numberOfReviewsFormatted()); } if (Float.compare(newRating.rating(), oldRating.rating()) != 0) { diff.putString(KEY_RATING_FORMATTED, newRating.ratingFormatted()); } return diff.size() == 0 ? null : diff; } @a_tolstykh
  27. public void onBindViewHolder(CityViewHolder holder, int index, List<Object> p) { if

    (p.isEmpty()) { onBindViewHolder(holder, index); return; } Bundle payload = (Bundle) p.get(0); for (String key : payload.keySet()) { if (key.equals(CitiesDiffCallback.KEY_NUMBER_OF_REVIEWS)) { holder.animateReviews(payload.getString(key)); } else if (key.equals(CitiesDiffCallback.KEY_RATING_FORMATTED)) { holder.animateRating(payload.getString(key)); } } } @a_tolstykh
  28. DiffUtil - average runtimes • 100 items and 10 modifications:

    avg: 0.39 ms, median: 0.35 ms • 100 items and 100 modifications: 3.82 ms, median: 3.75 ms • 100 items and 100 modifications without moves: 2.09 ms, median: 2.06 ms • 1000 items and 50 modifications: avg: 4.67 ms, median: 4.59 ms • 1000 items and 50 modifications without moves: avg: 3.59 ms, median: 3.50 ms • 1000 items and 200 modifications: 27.07 ms, median: 26.92 ms • 1000 items and 200 modifications without moves: 13.54 ms, median: 13.36 ms tests are run on Nexus 5X with M @a_tolstykh
  29. Android TextView with rich support of compound drawables. This is

    a tiny library which empowers TextView's compound drawables with: • size specifying • vector support • tinting @a_tolstykh https://github.com/a-tolstykh/textview-rich-drawable TextViewRichDrawable
  30. <TextView android:id="@+id/reviews_count" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <ImageView android:layout_width="@dimen/icon_small_size" android:layout_height="@dimen/icon_small_size" android:layout_marginEnd="@dimen/space_medium" android:src="@drawable/ic_person"

    /> <TextView android:id="@+id/rating_value" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <ImageView android:layout_width="@dimen/icon_small_size" android:layout_height="@dimen/icon_small_size" android:src="@drawable/ic_star" /> @a_tolstykh <com.tolstykh.textviewrichdrawable.TextViewRichDrawable android:id="@+id/reviews_count" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="@dimen/space_medium" android:drawableRight="@drawable/ic_person" app:compoundDrawableHeight="@dimen/icon_small_size" app:compoundDrawableWidth="@dimen/icon_small_size" /> <com.tolstykh.textviewrichdrawable.TextViewRichDrawable android:id="@+id/rating_value" android:layout_width="wrap_content" android:layout_height="wrap_content" android:drawableRight="@drawable/ic_star" app:compoundDrawableHeight="@dimen/icon_small_size" app:compoundDrawableWidth="@dimen/icon_small_size" /> Before After https://github.com/a-tolstykh/textview-rich-drawable