Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TextField change triggers full layout cycle

While looking at performance problems in my app, I discovered that each button press was triggering a call to the full onMeasure()/layout() cycle. There's no reason that I can see to try to lay out the entire app all over again; nothing has been added or removed, and nothing has changed size that I can see.

The problem tends to happen when the layout is very crowded, and it's possible that the bottom row of buttons has gone beyond the edge of the screen by a pixel or two.

Does anybody have any experience with this? Is there any way to determine why a layout cycle was triggered?

It seems that the layout won't be triggered if none of the TextFields on the screen are modified (see Finding the cause of a layout request in a ViewGroup). Does modifying a TextField always trigger a re-layout? Can I lock it somehow to prevent this? It's frustrating to think that changing any TextField anywhere on the screen can cause a full measure/layout cycle to cascade through the entire app; it's butchering my performance.

The container is a custom ViewGroup class, but I don't think that's the problem. I don't see what I could be doing differently to keep it from being called.

I'm considering adding a "lock" method to my widget to prevent any further layout changes after the initial layout. That will improve performance, but I'd rather solve the underlying problem.

Here's the stack at the time my onMeasure() method is called:

Gridbox.onMeasure(int, int) line: 217
Gridbox(View).measure(int, int) line: 8171
FrameLayout(ViewGroup).measureChildWithMargins(View, int, int, int, int) line: 3132 FrameLayout.onMeasure(int, int) line: 245
FrameLayout(View).measure(int, int) line: 8171
PhoneWindow$DecorView(ViewGroup).measureChildWithMargins(View, int, int, int, int) line: 3132
PhoneWindow$DecorView(FrameLayout).onMeasure(int, int) line: 245
PhoneWindow$DecorView(View).measure(int, int) line: 8171
ViewRoot.performTraversals() line: 801
ViewRoot.handleMessage(Message) line: 1727
ViewRoot(Handler).dispatchMessage(Message) line: 99 Looper.loop() line: 123 ActivityThread.main(String[]) line: 4627
Method.invokeNative(Object, Object[], Class, Class[], Class, int, boolean) line: not available [native method]
Method.invoke(Object, Object...) line: 521
ZygoteInit$MethodAndArgsCaller.run() line: 858
ZygoteInit.main(String[]) line: 616 NativeStart.main(String[]) line: not available [native method]

like image 695
Edward Falk Avatar asked Jun 28 '11 16:06

Edward Falk


3 Answers

Does anybody have any experience with this? Is there any way to determine why a layout cycle was triggered?

There is a way to determine exactly why the layout cycle is triggered.
Go to the layout of the screen you want to debug and replace the topmost container with a custom container that overrides just one method: requestLayout().

So, for instance, if you layout looks like this:

<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- omitted for brevity -->

</android.support.v4.widget.DrawerLayout>

You'll first need to create a new custom class that's a subclass of your container class:

public class RootView extends DrawerLayout {

    public RootView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public void requestLayout() {
        super.requestLayout();
    }

}

And then you'll need to modify your xml:

<?xml version="1.0" encoding="utf-8"?>
<com.example.RootView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- omitted for brevity -->

</com.example.RootView>

Now, the way android works is that when any viewgroup recieves layout request, it'll also call requestLayout() of it's direct parent. Which means that whenever any requestLayout() call is made inside your screen, your RootView's requestLayout() will also be called.

So start a debugging session, place a breakpoint inside the RootView.requestLayout() method and perform an action that you think is causing the layout cycle. Look at the stack trace. It'll always look like this:

RootView.requestLayout() line: 15   
RelativeLayout(View).requestLayout() line: 17364    
RelativeLayout.requestLayout() line: 360    
FrameLayout(View).requestLayout() line: 17364   
... a dozen of other calls to requestLayout() ...
TimeCell(View).requestLayout() line: 17364  
TextViewPlus(View).requestLayout() line: 17364  
TextViewPlus(TextView).setTypeface(Typeface) line: 2713 
... more methods ...

The first method that is not requestLayout() is what is causing the layout cycle to happen.
In the example above, it is TextViewPlus.setTypeface(Typeface).

By resuming the program and waiting until the breakpoint triggers again you can quickly determine all the methods that trigger relayout in your particular case.

However, please note that interface lags are almost always caused by multiple measure calls, not by a multiple layout cycles!

In a complicated layout (scroll views, linear layouts with weights, etc.) single layout pass can require onMeasure to be called multiple times on a single view, sometimes up to 500 times per layout cycle and more. To check if this is your problem, override onMeasure and onLayout methods of one of the bottom views (one of the views that's further away from rootView; note that onLayout to onMeasure ratio will be different for different views on you screen). It might be hard to determine exactly which view has the worst onLayout to onMeasure ratio, but any views that's inside LinearLayout inside LinearLayout inside LinearLayout inside LinearLayout... is a good place to start.

If onMeasure is called more than 16 times per onLayout then you have a problem. Try making your view hierarchy more flat, remove LinearLayouts with weigthSum attribute and ScrollViews.

like image 174
Alexey Avatar answered Sep 28 '22 07:09

Alexey


Changing the content of a TextView (its text) will trigger a relayout. This will however only cause a measure/layout of part of the tree. If this happens only from time to time (when the user clicks a button for instance), don't worry about it.

like image 25
Romain Guy Avatar answered Sep 28 '22 08:09

Romain Guy


According to Edward's comment he managed to prevent re-layout of the whole layout tree by isolating the TextViews in their own LinearLayout. This did NOT work for me. I have lined out below what worked for me for anyone having the same problem.

I encountered a similar re-layout issue when I added a TextClock (which is derived from TextView) to my Layout: this would cause an update of the whole layout every minute on the minute.

Isolating the TextClock by putting it in its own LinearLayout or RelativeLayout with both fixed layout_width/height and fixed positioning using layout_alignParent set on this Layout didn't change the complete re-layout every minute. What did help was to simply make the size of the TextClock fixed i.e. independent from its actual contents:

    <TextClock
    android:layout_height="wrap_content"
    android:layout_width="wrap_content"
    android:ems="4"
    android:lines="1"
    android:layout_alignParentRight="true"
    android:layout_alignParentTop="true"
    />

Where ems="x" and lines="y" fix width and height to x "widest" characters and y lines, when layout_width/height are set to "wrap_content". Of course you could use fixed dimensions in dp (or sp, I would assume) too.

like image 26
Dion Avatar answered Sep 28 '22 07:09

Dion