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

RecyclerView Performance Tuning. ENG (Codefest 2017, Novosibirsk)

RecyclerView Performance Tuning. ENG (Codefest 2017, Novosibirsk)

Best practices of RecyclerView usage.

Slides with animations are available here - https://goo.gl/GStb0p.


Oleksandr Tolstykh

April 01, 2017

More Decks by Oleksandr Tolstykh

Other Decks in Programming


  1. RecylerView Performance Tuning Oleksandr Tolstykh @a_tolstykh Head of Mobile Development,

    Android engineer
  2. None
  3. 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
  4. 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
  5. Sample App - Travel With Us City guides Base city

    info Data is loaded from server @a_tolstykh RecyclerView!
  6. 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
  7. Minimize onBindViewHolder() Make onBindViewHolder() as cheap as possible Avoid item

    instantiations (memory allocations) Do as much as you can in onCreateViewHolder() @a_tolstykh
  8. 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
  9. 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.
  10. Update only the affected items adapter.notifyItemRemoved(position); adapter.notifyItemChanged(position); adapter.notifyItemInserted(position); * only

    if you know position of updated item @a_tolstykh
  11. Profit! Animations for free! Optimizations for free! @a_tolstykh

  12. 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
  13. 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
  14. 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
  15. 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
  16. 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.
  17. 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; } }
  18. 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?
  19. 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);
  20. @a_tolstykh Without images prefetch With images prefetch Images prefetch

  21. @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); } } } }
  22. @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(); } } }
  23. @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); } } }); } }
  24. 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.
  25. 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
  26. DiffUtil @a_tolstykh *since support library version 24.2.0 https://developer.android.com/reference/android/support/v7/util/DiffUtil.html

  27. 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);
  28. 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
  29. 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
  30. 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
  31. @a_tolstykh Without DiffUtil With DiffUtil Lenovo Vibe X Android OS,

    v4.2 1080 x 1920 pixels Quad-core 1.5 GHz
  32. 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.
  33. 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
  34. 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
  35. Use any animation you want! • android.widget.TextSwitcher • com.hanks.htextview.HTextView •

    custom... @a_tolstykh DiffUtil animated values change
  36. 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
  37. DiffUtil May take significant time for large dataset... @a_tolstykh Use

    it on a background thread!
  38. DiffUtil. Summary Avoid unnecessary UI updates! @a_tolstykh Animated values change!

  39. 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
  40. <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
  41. Simplify your layout @a_tolstykh Before After https://github.com/a-tolstykh/textview-rich-drawable 4 Views 2

  42. RecyclerView Performance Tuning Prefetch. Cache. Reuse! @a_tolstykh

  43. RecyclerView Performance Tuning Do not over engineer. Keep it simple!

  44. RecyclerView Performance Tuning Avoid premature optimizations. Think about optimizations when

    you need them. @a_tolstykh
  45. Thanks @a_tolstykh @a_tolstykh @a_tolstykh +OleksandrTolstykh