Let's Sprinkle some #PerfMatters on your ViewGroups - Droidcon Paris 2015

Let's Sprinkle some #PerfMatters on your ViewGroups - Droidcon Paris 2015

Framework classes like RelativeLayout are extremely powerful but their first goal is to be versatile. This comes with significant performances costs that can prevent your app from being fast and smooth, especially in constrained areas like list scrolling. Let's see how we can remedy this by writing our own layouting and drawing logic where it matters.

4d2070e800356da57315d427aa2343f4?s=128

François Blavoet

November 09, 2015
Tweet

Transcript

  1. Let's Sprinkle some #PerfMa(ers on your ViewGroups

  2. • "@deezer.com • google.com/+FrancoisBlavoet

  3. The Material Transi-on

  4. From the Material spec 'Mo$on provides meaning' 'Mo$on is meaningful

    and appropriate, serving to focus a7en$on and maintain con$nuity'
  5. None
  6. None
  7. The 16 ms target 1000 ms / 16.7 = 60

    frames per second - Cinema : 24 fps - Video games : usually 30 or 60 fps
  8. What is Jank ? Problema)c blocking of a so2ware applica)on's

    user interface due to slow opera)ons1. In prac(ce : • Choppy UI • Discon/nuity of mo/on 1 h$ps:/ /en.wik/onary.org/wiki/jank
  9. Let's inves*gate ! Our first tool : SysTrace Displays the

    execu/on /mes of all the system's processes AndroidSdk/platform-tools/systrace/systrace.py
  10. None
  11. Let's zoom in

  12. Fortunately, we can get some advice :

  13. Next step : measure layou0ng performances AndroidSdk/platform-tools/hierarchyviewer

  14. None
  15. Let's profile this view tree : hit the profile node

    bu/on :
  16. None
  17. Let's dive in the code !

  18. ViewGroup 2 main methods : protected void onMeasure(int widthMeasureSpec, int

    heightMeasureSpec) {} protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}
  19. Rela%veLayout At its core, Rela.veLayout is designed around collec.ng and

    applying constraints : private static final int[] RULES_VERTICAL = { ABOVE, BELOW, ALIGN_BASELINE, ALIGN_TOP, ALIGN_BOTTOM }; private static final int[] RULES_HORIZONTAL = { LEFT_OF, RIGHT_OF, ALIGN_LEFT, ALIGN_RIGHT, START_OF, END_OF, ALIGN_START, ALIGN_END };
  20. Rela%veLayout In order to apply these constraints, in onMeasure() :

    • sort the children according to their rela0ve constraints • apply the horizontal constraints • apply the ver0cal constraints • apply baseline, size wrapping, etc
  21. One poten(al solu(on : wri(ng our own ViewGroup Framework classes

    : • Correctness • Versa,lity • Performances
  22. One poten(al solu(on : wri(ng our own ViewGroup Custom implementa.on

    : • Correctness • Versa,lity Specialized • Performances
  23. A simple example

  24. <RelativeLayout> <ImageView/> <LinearLayout> <TextView/> <TextView/> </LinearLayout> <ImageView/> <ImageView/> </RelativeLayout>

  25. <AlbumView android:layout_width="match_parent" android:layout_height="@dimen/album_height android:paddingLeft="16dp" android:paddingRight="16dp"/> layout infla+on : inflater.inflate(R.layout.album_view, parent,

    false);
  26. public class AlbumView extends ViewGroup { public AlbumView(Context context) {

    this(context, null); } public AlbumView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public AlbumView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr, 0); } public AlbumView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(context, attrs, defStyleAttr, defStyleRes); } }
  27. private ImageView cover; private TextView title; private TextView artist; private

    ImageView heart; private ImageView overflow; private void init(Context context) { LayoutInflater.from(context).inflate(R.layout.album_view_internal, this, true); cover = (ImageView) findViewById(R.id.cover); title = (TextView) findViewById(R.id.title); artist = (TextView) findViewById(R.id.artist); heart = (ImageView) findViewById(R.id.artist); overflow = (ImageView) findViewById(R.id.artist); }
  28. <merge> <ImageView/> <TextView/> <TextView/> <ImageView/> <ImageView/> </merge>

  29. @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs);

    } @Override protected LayoutParams generateDefaultLayoutParams() { return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); }
  30. @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthUsed

    = 0; int heightUsed = 0; measureChildWithMargins(cover, widthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed); widthUsed += getMeasuredWidthWithMargins(mCoverView); ... } protected static int getMeasuredWidthWithMargins(View child) { final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); return child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; }
  31. @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthUsed

    = 0; int heightUsed = 0; measureChildWithMargins(cover, widthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed); widthUsed += getMeasuredWidthWithMargins(mCoverView); // same operation for a second child : measureChildWithMargins(overflow, widthMeasureSpec, widthUsed, heightMeasureSpec, heightUsed); widthUsed += getMeasuredWidthWithMargins(mCoverView); ... }
  32. @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthUsed

    = 0; int heightUsed = 0; measureChildWithMargins(cover, ...); widthUsed += getMeasuredWidthWithMargins(mCoverView); measureChildWithMargins(overflow, ...); widthUsed += getMeasuredWidthWithMargins(mCoverView); measureChildWithMargins(heart, ...); widthUsed += getMeasuredWidthWithMargins(mCoverView); measureChildWithMargins(title, ...); measureChildWithMargins(artist, ...); widthUsed += getMeasuredWidthWithMargins(mCoverView); setMeasuredDimension(resolveSize(widthUsed, measureSpec), MeasureSpec.getSize(heightMeasureSpec)); }
  33. None
  34. @Override protected void onLayout(boolean changed, int l, int t, int

    r, int b) { final int paddingLeft = getPaddingLeft(); final int paddingTop = getPaddingTop(); final int paddingRight = getPaddingRight(); int spaceConsumedLeft = paddingLeft; //layout the first view on the left : layoutView(cover, spaceConsumedLeft, paddingTop, cover.getMeasuredWidth(), cover.getMeasuredHeight()); ... } private void layoutView(View view, int l, int t, int width, int height) { final MarginLayoutParams margins = (MarginLayoutParams) view.getLayoutParams(); final int leftWithMargins = l + margins.leftMargin; final int topWithMargins = t + margins.topMargin; view.layout(leftWithMargins, topWithMargins, leftWithMargins + width, topWithMargins + height); }
  35. @Override protected void onLayout(boolean changed, int l, int t, int

    r, int b) { final int paddingLeft = getPaddingLeft(); final int paddingTop = getPaddingTop(); final int paddingRight = getPaddingRight(); int spaceConsumedLeft = paddingLeft; //layout the first view on the left : layoutView(cover,...); spaceConsumedLeft += getMeasuredWidthWithMargin(cover); //layout the overflow button on the right, centered vertically: layoutView(overflow, r - l - paddingRight - overflow.getMeasuredWidth(), (b - t - overflow.getMeasuredHeight()) / 2, overflow.getMeasuredWidth(), overflow.getMeasuredHeight()); int spaceConsumedRight += getMeasuredWidthWithMargin(overflow); ... }
  36. The result : • Exact same disposi/on • Flat View

    hierarchy • Op/mal measure & layout performances
  37. Let's dive deeper !

  38. Another poten*al bo.leneck : TextView In some cases, TextView can

    have unsa4sfactory performances : • Long text to display • Combina4on of several Spans
  39. public class AlbumView extends ViewGroup { private TextLayoutHelper textHelper; @Override

    public boolean willNotDraw() { return false; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); textHelper.draw(canvas); } }
  40. How to draw a text ?

  41. TextView : 10 000 lines of code BUT does not

    know how to draw a text. It is delegated to an abstract class : android.text.Layout 3 implementa+ons : • BoringLayout • Sta.cLayout • DynamicLayout
  42. Did you say boring ? BoringLayout.Metrics boring = BoringLayout.isBoring(mText, mPaint);

    if (boring != null) { mLayout = BoringLayout.make(mText, mPaint, width, Layout.Alignment.ALIGN_NORMAL, SPACING_MULT, SPACING_ADD, boring, true, TextUtils.TruncateAt.END, width); }
  43. Did you say boring ? BoringLayout.Metrics boring = BoringLayout.isBoring(mText, mPaint);

    if (boring != null) { // this is boring ! if (mSavedLayout != null) { mLayout = mSavedLayout.replaceOrMake(mText, ...); } else { mLayout = BoringLayout.make(mText, ...); } mSavedLayout = (BoringLayout) mLayout;
  44. protected final void createLayout(final int width, final int hOffset) {

    BoringLayout.Metrics boring = BoringLayout.isBoring(mText, mPaint); if (boring != null) { // this is boring ! if (mSavedLayout != null) { mLayout = mSavedLayout.replaceOrMake(mText, ...); } else { mLayout = BoringLayout.make(mText, ...); } mSavedLayout = (BoringLayout) mLayout; } else { mLayout = new StaticLayout(mText, 0, mText.length(), mPaint, width, Layout.Alignment.ALIGN_NORMAL, SPACING_MULT, SPACING_ADD, true, TextUtils.TruncateAt.END, width, MAX_LINES); }
  45. Finally, let's draw : public final void draw(final @NonNull Canvas

    canvas) { if (TextUtils.isEmpty(mText)) return; final int saveCount = canvas.save(); canvas.clipRect(mClipBounds); canvas.translate(mClipBounds.left, mClipBounds.top); mLayout.draw(canvas); canvas.restoreToCount(saveCount); }
  46. Advantages : • Total control over the drawing logic, allowing

    even more customiza7on than with Spans • Allows to mul7thread the text layou7ng logic : instead of crea7ng the text layouts on the main thread, we can create them on a Worker Thread and cache the result.
  47. None
  48. To Conclude

  49. Resources • Nexus 5 mockup • Profiling diagram • Perf

    Colt