【问题标题】:Bad layout performance due to view measuring?视图测量导致布局性能不佳?
【发布时间】:2013-07-01 11:11:29
【问题描述】:

我遇到了性能问题,我认为这是由衡量我的观点引起的。 我的布局是以编程方式构建的,所以我无法显示我的布局的 xml。但这里有一些可能导致问题的观点:

  • 用于包装所有内容的 ScrollView(宽度:MATCH_PARENT,高度:MATCH_PARENT)
  • 包含所有不同内容部分的 LinearLayout(宽度:MATCH_PARENT,高度:MATCH_PARENT,前一个视图的子视图)
  • 两个异步填充的LinearListViews (LinearLayout)(宽度:MATCH_PARENT,高度:WRAP_CONTENT,前一个视图的子级)

问题似乎只在我填充 LinearListView 时出现;添加视图似乎会触发 View.measure 被多次调用:

为什么多次调用 View.measured?具有 MATH_PARENT 大小的 LinearLayout 意味着添加孩子时不必再次测量它,对吧?

我不确定如何防止这种情况发生,或者如何调试问题。我尝试将 LinearListView 的可见性设置为 GONE,直到添加了所有项目,但这并没有什么不同。

我希望我提供了足够的信息,以便有人为我指明正确的方向。

编辑 #1 (07/01):

以下是用于 LinearListViews 的适配器基类的一些部分:

public abstract class DataProviderAdapter<T> extends BaseAdapter
{
    ...

    @Override
    public View getView(int position, View convertView, ViewGroup parent)
    {
        View row = convertView;
        T item = getItem(position);

        Log.d(TAG, "getView(): Position: " + position + ", ConvertView: "
                + (convertView == null ? "null" : convertView) + ", TotalCount: " + getCount());

        if (row == null) {
            row = buildRow(position);
            markRowAsLoading(row);
        }

        if (item == null) {

            if (convertView != null) {
                markRowAsLoading(row);
            }

            int skip;
            if (itemsRequestParams != null) {
                int pageSize = itemsRequestParams.getTake();

                // determine how many items to skip
                skip = position - (position % pageSize);
            }
            else {
                skip = 0;
            }

            // if the page isn't already being fetched
            if (!pagesReceiving.contains(skip)) {
                pagesReceiving.add(skip);
                AsyncTaskUtil.executeMultiThreaded(new ReceiveItemsTask(), skip);
            }
        }
        else {
            Log.d(TAG, getClass().getSimpleName() + " - PrepareRow: " + getItemId(position)
                    + ", Pos: " + position);
            prepareRow(row, item, position);
        }
        return row;
    }

    protected void processDataResult(int skip, DataResult<T> result)
    {
        if (result instanceof GroupedDataResult<?>) {
            GroupedDataResult<T> groupedResult = (GroupedDataResult<T>)result;
            totalItemCount = groupedResult.getTotalGroupCount() + groupedResult.getTotalItemCount();
        }
        else {
            totalItemCount = result.getTotalItemCount();
        }

        for (int i = 0; i < result.getItems().size(); i++) {
            itemList.put(skip + i, result.getItems().get(i));
        }

        Log.d(TAG, "processDataResult(" + skip + ")");
        notifyDataSetChanged();
    }

    static protected class CellHolder
    {
        public ProgressBar ProgressSpinner;
        public List<android.widget.ImageView> Images;
        public List<TextView> Labels;
        private final HashMap<String, Object> _extras = new HashMap<String, Object>();

        public void putExtra(String key, Object value)
        {
            _extras.put(key, value);
        }

        @SuppressWarnings("unchecked")
        public <T> T getExtra(String key)
        {
            return _extras.containsKey(key) ? (T)_extras.get(key) : null;
        }
    }

    ...

    private class ReceiveItemsTask extends AsyncTask<Integer, Void, DataResult<T>>
    {
        private int skip;

        @Override
        protected DataResult<T> doInBackground(Integer... params)
        {
            skip = params[0];
            DataResult<T> items = getItems(skip);
            Log.d(TAG, "doInBackground(" + skip + ")");
            return items;
        }

        @Override
        protected void onPostExecute(DataResult<T> result)
        {
            Log.d(TAG,
                    "onPostExecute("
                            + (result == null ? "null" : result.getItems().size() + "/"
                                    + result.getTotalItemCount()) + ")");
            if (result == null) {
                return;
            }
            processDataResult(skip, result);
            Log.d(TAG, "onPostExecute end");
        }
    }
}

编辑 #2 (07/01):

我继承了 RelativeLayout 并覆盖了 onMeasure,因此我可以记录正在测量的视图以及测量顺序。 结果如下:

07-01 15:37:37.434: V/DefaultLayout(8172): DefaultLayout.onMeasure
07-01 15:37:37.434: V/DefaultLayout(8172): TabView.onMeasure
07-01 15:37:37.434: V/DefaultLayout(8172): ItemDetailsContainerView.onMeasure
07-01 15:37:37.438: V/DefaultLayout(8172): HeaderView.onMeasure
07-01 15:37:37.438: V/DefaultLayout(8172): ItemDetailsInfoFieldsLayout.onMeasure
07-01 15:37:37.438: V/DefaultLayout(8172): ItemReviewTableView.onMeasure
07-01 15:37:37.438: V/DefaultLayout(8172): InfoFieldsLayout.onMeasure
07-01 15:37:37.438: V/DefaultLayout(8172): InfoFieldsLayout.onMeasure
07-01 15:37:37.442: V/DefaultLayout(8172): InfoFieldsLayout.onMeasure
07-01 15:37:37.442: V/DefaultLayout(8172): InfoFieldsLayout.onMeasure
07-01 15:37:37.442: V/DefaultLayout(8172): InfoFieldsLayout.onMeasure
07-01 15:37:37.442: V/DefaultLayout(8172): InfoFieldsLayout.onMeasure
07-01 15:37:37.442: V/DefaultLayout(8172): InfoFieldsLayout.onMeasure
07-01 15:37:37.442: V/DefaultLayout(8172): InfoFieldsLayout.onMeasure
07-01 15:37:37.446: V/DefaultLayout(8172): ItemReviewTableView.onMeasure
07-01 15:37:37.446: V/DefaultLayout(8172): InfoFieldsLayout.onMeasure
07-01 15:37:37.446: V/DefaultLayout(8172): InfoFieldsLayout.onMeasure
07-01 15:37:37.446: V/DefaultLayout(8172): InfoFieldsLayout.onMeasure
07-01 15:37:37.446: V/DefaultLayout(8172): InfoFieldsLayout.onMeasure
07-01 15:37:37.450: V/DefaultLayout(8172): InfoFieldsLayout.onMeasure
07-01 15:37:37.450: V/DefaultLayout(8172): InfoFieldsLayout.onMeasure
07-01 15:37:37.450: V/DefaultLayout(8172): InfoFieldsLayout.onMeasure
07-01 15:37:37.450: V/DefaultLayout(8172): InfoFieldsLayout.onMeasure
07-01 15:37:37.450: V/DefaultLayout(8172): RelatedItemsTableView.onMeasure
07-01 15:37:37.454: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.458: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.462: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.466: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.470: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.474: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.478: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.478: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.482: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.482: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.486: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.486: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.490: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.490: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.494: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.494: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.498: V/DefaultLayout(8172): RelatedItemsTableView.onMeasure
07-01 15:37:37.498: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.498: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.502: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.502: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.506: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.506: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.510: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.510: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.514: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.514: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.518: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.518: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.522: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.522: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.526: V/DefaultLayout(8172): RatingView.onMeasure
07-01 15:37:37.526: V/DefaultLayout(8172): RatingView.onMeasure
...

这个列表持续了一段时间。但对我来说似乎很奇怪的是,它从测量顶视图(DefaultLayout.onMeasure)开始,然后还测量所有的孩子(所以所有的视图)。它会多次这样做!

什么可能导致在顶视图上调用 onMeasure?

编辑 #3 (07/02):

会不会是notifyDataSetChanged()触发了顶视图的测量?

编辑 #4 (07/03):

是的,notifyDataSetChanged() 似乎确实触发了要测量的顶级视图(我能够在新的测试项目中重现这一点)。由于我的适配器的工作方式,notifyDataSetChanged() 被多次调用。

当我致电notifyDataSetChanged() 时,所有观看次数都被衡量的原因是什么?有什么办法可以防止这种情况发生吗?

【问题讨论】:

  • 您可能希望提供一些代码来展示如何填充 LinearListView。没有它,你不会比猜测更好。它似乎确实使用了 很多 时间来衡量。不过,RelativeLayout 是什么(您的个人资料列表中的第 19 项)?
  • @Geobits 我添加了一些用于 LinearListViews 的适配器代码。它可能看起来没有很多时间,但那是因为我没有使用默认的 Android 模拟器。使用默认模拟器,窗口会冻结大约 30 秒(取决于添加到 LinearListViews 的项目数)。我会尝试更多地了解RelativeLayout。
  • 您是否使用layout_weight 作为LinearListView?还有一个明显的问题,为什么不用ListView 而不是应该复制ListView 的自定义LinearLayout
  • 我没有在任何地方使用 layout_weight。而且我没有使用 ListView,因为我已经在 ScrollView 中,所以它需要是一个不滚动的内联列表。
  • 我们在谈论多少子视图?我在单个LinearLayout 中遇到了 > 50 行左右的严重减速。此外,您可能希望删除大部分数据,并专注于添加 one 行时发生的情况。这样你的日志可能更有意义。

标签: android android-layout android-view


【解决方案1】:

你没有说相对布局的来源,这是列表的包装器吗?

根据 Romain Guy 的说法,相对布局总是执行两次测量传递,因此这可能会导致一些减速。你甚至有嵌套的相对布局吗?显然,这可能具有指数时间复杂度。

请参阅此答案以供参考(以及他实际说过的 Google io 视频的链接):

https://stackoverflow.com/a/17496262/1281144

【讨论】:

  • 救命稻草!我确实有嵌套的相对布局。用框架布局替换它们后,性能提高了 10 倍以上。非常感谢!
【解决方案2】:

在您的描述中立即让我印象深刻的一件事是您的 LinearLayout,它是您的 ScrollView 的直接(也是唯一)子级,其高度设置为 MATCH_PARENT - 这是错误的 - 它应该具有 WRAP_CONTENT 的高度。 ScrollView 的子级不能与 ScrollView 本身高度相同 - 否则无法滚动。

除此之外,查看 LinearListView 的代码我看到:

private DataSetObserver mDataObserver = new DataSetObserver() {

    @Override
    public void onChanged() {
        setupChildren();
    }

    @Override
    public void onInvalidated() {
        setupChildren();
    }

};

因此,每当底层数据集更改时,setupChildren() 就会被调用,这会删除所有视图并重新绘制它们。

在您的 processDataResult() 方法中,您可以这样做:

 for (int i = 0; i < result.getItems().size(); i++) {
        itemList.put(skip + i, result.getItems().get(i));
    }

所以列表在添加每个项目后会重新绘制(当然是在 UI 线程上)。

我会自己修改库以删除 onChanged() 中对 setupChildren() 的调用,如下所示:

private DataSetObserver mDataObserver = new DataSetObserver() {

    @Override
    public void onChanged() {
        //setupChildren(); do nothing here
    }

    @Override
    public void onInvalidated() {
        setupChildren();
    }

};

我认为当你调用 notifyDataSetChanged() 时会调用 onInvalidated()

【讨论】:

  • 谢谢,这确实是错误的。更改此设置并不能解决问题。
  • 删除 setupChildren(); 完全阻止了列表的填充。 onChanged() 只被调用了两次,所以这不是问题:(
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-02-18
  • 1970-01-01
相关资源
最近更新 更多