【问题标题】:TextField change triggers full layout cycleTextField 更改触发完整的布局周期
【发布时间】:2011-06-28 16:36:15
【问题描述】:

在查看我的应用程序中的性能问题时,我发现每次按下按钮都会触发对完整 onMeasure()/layout() 循环的调用。我没有理由尝试重新布置整个应用程序。没有添加或删除任何内容,也没有更改我可以看到的大小。

当布局非常拥挤时,问题往往会发生,并且底部一排按钮可能超出屏幕边缘一两个像素。

有人有这方面的经验吗?有什么方法可以确定为什么触发了布局循环?

如果屏幕上的任何文本字段都没有被修改,似乎不会触发布局(请参阅Finding the cause of a layout request in a ViewGroup)。修改 TextField 是否总是会触发重新布局?我可以以某种方式锁定它以防止这种情况吗?想到改变屏幕上任何地方的任何 TextField 都会导致整个测量/布局循环在整个应用程序中级联,这令人沮丧。它正在扼杀我的表现。

容器是一个自定义 ViewGroup 类,但我认为这不是问题所在。我看不出我可以做些什么不同的事情来防止它被调用。

我正在考虑向我的小部件添加“锁定”方法,以防止在初始布局之后发生任何进一步的布局更改。这将提高性能,但我宁愿解决根本问题。

这是调用 onMeasure() 方法时的堆栈:

Gridbox.onMeasure(int, int) 行:217
Gridbox(View).measure(int, int) 行:8171
FrameLayout(ViewGroup).measureChildWithMargins(View, int, int, int, int) 行:3132 FrameLayout.onMeasure(int, int) 行:245
FrameLayout(View).measure(int, int) 行:8171
PhoneWindow$DecorView(ViewGroup).measureChildWithMargins(View, int, int, int, int) 行:3132
PhoneWindow$DecorView(FrameLayout).onMeasure(int, int) 行:245
PhoneWindow$DecorView(View).measure(int, int) 行:8171
ViewRoot.performTraversals() 行:801
ViewRoot.handleMessage(Message) 行:1727
ViewRoot(Handler).dispatchMessage(Message) 行:99 Looper.loop() 行:123 ActivityThread.main(String[]) 行:4627
Method.invokeNative(Object, Object[], Class, Class[], Class, int, boolean) 行:不可用 [本机方法]
Method.invoke(Object, Object...) 行:521
ZygoteInit$MethodAndArgsCaller.run() 行:858
ZygoteInit.main(String[]) 行:616 NativeStart.main(String[]) 行:不可用 [本机方法]

【问题讨论】:

    标签: android android-layout


    【解决方案1】:

    有人有这方面的经验吗?有什么方法可以确定为什么触发了布局循环?

    有一种方法可以准确确定触发布局周期的原因。
    转到您要调试的屏幕布局,并将最顶层的容器替换为仅覆盖一种方法的自定义容器:requestLayout()。

    因此,例如,如果您的布局如下所示:

    <?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>
    

    您首先需要创建一个新的自定义类,它是您的容器类的子类:

    public class RootView extends DrawerLayout {
    
        public RootView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        public void requestLayout() {
            super.requestLayout();
        }
    
    }
    

    然后你需要修改你的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>
    

    现在,android 的工作方式是,当任何视图组收到布局请求时,它也会调用其直接父级的 requestLayout()。这意味着无论何时在您的屏幕内进行任何 requestLayout() 调用,您的 RootView 的 requestLayout() 也会被调用。

    因此,开始一个调试会话,在 RootView.requestLayout() 方法中放置一个断点,并执行您认为会导致布局循环的操作。查看堆栈跟踪。它总是看起来像这样:

    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 ...
    

    不是 requestLayout() 的第一个方法是导致布局循环发生的原因。
    在上面的示例中,它是 TextViewPlus.setTypeface(Typeface)。

    通过恢复程序并等待断点再次触发,您可以快速确定在您的特定情况下触发重新布局的所有方法。

    但是,请注意,界面延迟几乎总是由多次测量调用引起的,而不是由多个布局周期引起的!

    在复杂的布局(滚动视图、带权重的线性布局等)中,单个布局传递可能需要在单个视图上多次调用 onMeasure,有时每个布局周期最多调用 500 次甚至更多。要检查这是否是您的问题,请覆盖底部视图之一的 onMeasure 和 onLayout 方法(远离 rootView 的视图之一;请注意,屏幕上不同视图的 onLayout 与 onMeasure 比率会有所不同)。可能很难准确确定哪个视图的 onLayout 与 onMeasure 比率最差,但任何位于 LinearLayout 内部 LinearLayout 内部 LinearLayout 内部 LinearLayout... 的视图都是一个不错的起点。

    如果每个 onLayout 调用 onMeasure 超过 16 次,那么您就有问题了。尝试使您的视图层次结构更平坦,删除带有 weigthSum 属性和 ScrollViews 的 LinearLayouts。

    【讨论】:

      【解决方案2】:

      更改 TextView 的内容(其文本)将触发重新布局。然而,这只会导致树的一部分的测量/布局。如果这种情况只是偶尔发生(例如当用户单击按钮时),请不要担心。

      【讨论】:

      • 是的,不幸的是,在我的情况下,它发生在每次按钮按下并生成整个应用程序的重新布局。我收到了用户对性能的投诉,所以我必须处理它。
      • 这里有两个问题。 1:按下按钮不应触发整个应用程序的重新布局,框架有办法防止这种情况发生。您的应用程序可能存在某些特定因素导致此行为。 2:重新布局不应该花费太长时间以至于用户会注意到它。我不知道你有多少视图或者你的自定义 ViewGroup 做了什么,但似乎你应该对你的应用进行分析,看看为什么它需要这么长时间。
      • 应用程序是 RpnCalc。它有五个文本字段、39 个按钮和 30 多个标签。我的自定义视图组基本上是我自己的 GridBag 实现。我很乐意与任何想查看它的人分享代码。我是第一个承认它效率不高的人,我很乐意在不需要时阻止它被调用。我确实分析了我的应用程序;配置文件告诉我,大部分时间都被我的布局小部件以及对各种组件的 onMeasure() 调用所占用。
      • 感谢 Romain,您的建议使我找到了解决方案。我使用单个全局布局小部件来保存所有内容。这就是为什么框架不能阻止 onMeasure() 传递测量应用程序中的每个小部件。我通过在它们自己的 LinearLayout 中隔离 TextField 并将所有按钮放在不同的容器中来修复它。这让框架将布局传递限制为仅包含 TextField 的容器。这是一个巨大的性能提升。
      【解决方案3】:

      根据 Edward 的评论,他设法通过 将 TextViews 隔离在自己的 LinearLayout 中来防止重新布局整个布局树。这对我不起作用。我在下面列出了对遇到同样问题的人有用的方法。

      当我将 TextClock(派生自 TextView)添加到我的布局时,我遇到了类似的重新布局问题:这将导致整个布局每分钟更新一次。

      通过将 TextClock 放置在其自己的 LinearLayout 或 RelativeLayout 中来隔离 TextClock,并在此 Layout 上设置了固定 layout_width/height 和使用 layout_alignParent 进行固定定位并没有改变完整每分钟重新布局。 帮助固定 TextClock 的大小,即独立于其实际内容:

          <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"
          />
      

      其中 ems="x"lines="y" 将宽度和高度固定为 x “最宽”字符和 y 行,当 layout_width/height 设置为 "wrap_content"。当然,您也可以在 dp(或 sp,我假设)中使用固定尺寸。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-03-29
        • 1970-01-01
        • 1970-01-01
        • 2011-01-01
        • 2020-05-23
        • 1970-01-01
        • 1970-01-01
        • 2014-03-05
        相关资源
        最近更新 更多