1. 程式人生 > >由Dialog裡面巢狀ListView之後的高度自適應引起的ListView效能優化

由Dialog裡面巢狀ListView之後的高度自適應引起的ListView效能優化

廢話少說先來張bug圖填樓
圖片

前言

隨著RecyclerView的普及,ListView差不多是安卓快要淘汰的控制元件了,但是我們有時候還是會用到,基本上可以說是前些年最常用的Android控制元件之一了.拋開我們的主題,我們先來談談ListView的一些小小的細節,可能是很多開發者在開發過程中並沒有注意到的細節,這些細節設定會影響到我們的App的效能.

  • android:layout_height屬性

我們在使用ListView的時候很可能隨手就會寫一個layout_height=”wrap_content”或者layout_height=”match_parent”,非常非常普通,咋一看,我寫的沒錯啊…可是實際上layout_height=”wrap_content”

是錯誤的寫法!!!會嚴重影響程式的效能 我們先來做一個實驗:
xml佈局檔案如下

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

    <ListView
        android:id="@+id/list_view"
        android:layout_width
="match_parent" android:layout_height="wrap_content" >
</ListView> </LinearLayout>

java部分程式碼
圖片

執行log
圖片

我們會發現getView總共被呼叫了15次!其中4次是null的,11次為重複呼叫,ListView的item數目只有3項!!!太可怕了

我們試著將ListView的高度屬性改為layout_height=”match_parent”,然後看看
這裡寫圖片描述

我們可以看到getView()只被呼叫了3次!這應該是我們期望的結果!

原因分析:


瞭解原因前,我們應該先了解View的繪製流程,之前我的部落格沒有關於View繪製流程的介紹,那麼在這邊說一下,是一個很重要的知識點.
View的繪製流程是通過 onMeasure()->onLayout()->onDraw()
onMeasure() :主要工作是測量檢視的大小.從頂層的父View到子View遞迴呼叫measure方法,measure方法又回撥onMeasure().
onLayout: 主要工作是確定View的位置,進行頁面佈局.從頂層的父View向子View的遞迴呼叫view.layout方法的過程,即父View根據上一步measure子view所得到的佈局大小和佈局引數,將子view放在合適的位置上
onDraw() 主要工作是繪製檢視.ViewRoot建立一個Canvas物件,然後呼叫onDraw()方法.總共6個步驟.1.繪製檢視背景,2.儲存當前畫布的圖層(Layer),3.繪製View內容,4.繪製View的子View檢視,沒有的話就不繪製,5.還原圖層,6.繪製滾動條.

瞭解了View的繪製流程,那麼我們回到這個問題上.設定ListView的屬性layout_height=”wrap_content”,就意味著Listview的高度由子View決定,當在onMeasure()的時候,需要測量子View的高度,那我們來看看Listview的onMeasure()方法.

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // Sets up mListPadding
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int childWidth = 0;
        int childHeight = 0;
        int childState = 0;

        mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
        if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED ||
                heightMode == MeasureSpec.UNSPECIFIED)) {
            final View child = obtainView(0, mIsScrap);

            measureScrapChild(child, 0, widthMeasureSpec);

            childWidth = child.getMeasuredWidth();
            childHeight = child.getMeasuredHeight();
            childState = combineMeasuredStates(childState, child.getMeasuredState());

            if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
                    ((LayoutParams) child.getLayoutParams()).viewType)) {
                mRecycler.addScrapView(child, 0);
            }
        }

        if (widthMode == MeasureSpec.UNSPECIFIED) {
            widthSize = mListPadding.left + mListPadding.right + childWidth +
                    getVerticalScrollbarWidth();
        } else {
            widthSize |= (childState&MEASURED_STATE_MASK);
        }

        if (heightMode == MeasureSpec.UNSPECIFIED) {
            heightSize = mListPadding.top + mListPadding.bottom + childHeight +
                    getVerticalFadingEdgeLength() * 2;
        }

        if (heightMode == MeasureSpec.AT_MOST) {
            // TODO: after first layout we should maybe start at the first visible position, not 0
            heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
        }

        setMeasuredDimension(widthSize , heightSize);
        mWidthMeasureSpec = widthMeasureSpec;        
    }

其中

if (heightMode == MeasureSpec.AT_MOST) {
            // TODO: after first layout we should maybe start at the first visible position, not 0
            heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
        }

比較重要
再看measureHeightOfChildren()

final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
            final int maxHeight, int disallowPartialChildPosition) {

        ...

        for (i = startPosition; i <= endPosition; ++i) {
            child = obtainView(i, isScrap);

            measureScrapChild(child, i, widthMeasureSpec);
            ...

            // Recycle the view before we possibly return from the method
            if (recyle && recycleBin.shouldRecycleViewType(
                    ((LayoutParams) child.getLayoutParams()).viewType)) {
                recycleBin.addScrapView(child, -1);
            }

            returnedHeight += child.getMeasuredHeight();

            if (returnedHeight >= maxHeight) {
                ...
            }

            if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
                ...
            }
        }
        return returnedHeight;
    }

obtainView(i, isScrap)是子View的例項
measureScrapChild(child, i, widthMeasureSpec); 測量子View
recycleBin.addScrapView(child, -1);將子View加入快取,可以用來複用
if (returnedHeight >= maxHeight) {return ...;}如果已經測量的子View的高度大於maxHeight的話就直接return出迴圈,這樣的做法也很好理解,其實是ListView很聰明的一種做法,你可以想想比如說這個螢幕只能畫10個Item高度,你有20個Item,那麼畫出10個就行了,剩下的十個就沒必要畫了~

我們現在看下obtainView()方法

View obtainView(int position, boolean[] isScrap) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");

        isScrap[0] = false;

        // Check whether we have a transient state view. Attempt to re-bind the
        // data and discard the view if we fail.
        final View transientView = mRecycler.getTransientStateView(position);
        if (transientView != null) {
            final LayoutParams params = (LayoutParams) transientView.getLayoutParams();

            // If the view type hasn't changed, attempt to re-bind the data.
            if (params.viewType == mAdapter.getItemViewType(position)) {
                final View updatedView = mAdapter.getView(position, transientView, this);

                // If we failed to re-bind the data, scrap the obtained view.
                if (updatedView != transientView) {
                    setItemViewLayoutParams(updatedView, position);
                    mRecycler.addScrapView(updatedView, position);
                }
            }

            // Scrap view implies temporary detachment.
            isScrap[0] = true;
            return transientView;
        }

        final View scrapView = mRecycler.getScrapView(position);
        final View child = mAdapter.getView(position, scrapView, this);
        if (scrapView != null) {
            if (child != scrapView) {
                // Failed to re-bind the data, return scrap to the heap.
                mRecycler.addScrapView(scrapView, position);
            } else {
                isScrap[0] = true;

                child.dispatchFinishTemporaryDetach();
            }
        }

        if (mCacheColorHint != 0) {
            child.setDrawingCacheBackgroundColor(mCacheColorHint);
        }

        if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
            child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
        }

        setItemViewLayoutParams(child, position);

        if (AccessibilityManager.getInstance(mContext).isEnabled()) {
            if (mAccessibilityDelegate == null) {
                mAccessibilityDelegate = new ListItemAccessibilityDelegate();
            }
            if (child.getAccessibilityDelegate() == null) {
                child.setAccessibilityDelegate(mAccessibilityDelegate);
            }
        }

        Trace.traceEnd(Trace.TRACE_TAG_VIEW);

        return child;
    }

得到一個檢視,它顯示的資料與指定的位置。這叫做當我們已經發現的觀點不是可供重用的回收站。剩下的唯一的選擇是將一個古老的檢視或製作一個新的.

我們應該關注下以下兩行程式碼:

...
  final View scrapView = mRecycler.getScrapView(position);
  final View child = mAdapter.getView(position, scrapView, this);
...

這兩行程式碼的意思就是說先從快取裡面取出來一個廢棄的view,然後將當前的位置跟view作為引數傳入到getView()方法中.這個廢棄的,然後又作為引數的view就是convertView.

然後我們總結下剛剛的步驟:
A、測量第0項的時候,convertView肯定是null的 View scrapView = mRecycler.getScrapView(position)也是空的,所以我們在log上可以看到.
這裡寫圖片描述
B、第0項測量結束,這個第0項的View就被加入到複用快取當中了;
C、開始測量第1項,這時因為是有第0項的View快取的,所以getView的引數convertView就是這個第0項的View快取,然後重複B步驟新增到快取,只不過這個View快取還是第0項的View;
D、繼續測量第2項,重複C。

所以前面說到onMeasure方法會導致getView呼叫,而一個View的onMeasure方法呼叫時機並不是由自身決定,而是由其父檢視來決定。ListView放在FrameLayout和RelativeLayout中其onMeasure方法的呼叫次數是完全不同的。在RelativeLayout中oMeasure()方法呼叫會翻倍.

由於onMeasure方法會多次被呼叫,上述問題中是兩次,其實完整的呼叫順序是onMeasure - onLayout - onMeasure - onLayout - onDraw。

所以根據上面的結論我們可以得出,如果LitsView的android:layout_height屬性設定為wrap_content將會引起getView的多次測量

現象

如上bug圖…

產生的原因

  • ListView的高度設定成了android:layout_height屬性設定為wrap_content

  • ListView的父類是RelativeLayout,RelativiLayout佈局會使子佈局View的Measure週期翻倍,有興趣可以看下三大基礎佈局效能比較

解決辦法

根據每個Item的高度,然後再根據Adapter的count來動態算高.
程式碼如下:

public class SetHeight {

    public void setListViewHeightBasedOnChildren(ListView listView, android.widget.BaseAdapter adapter) {

        if (adapter==null){
            return;
        }
        int totalHeight = 0;

        for (int i = 0; i < adapter.getCount(); i++) { // listAdapter.getCount()返回資料項的數目

            View listItem = adapter.getView(i, null, listView);

            listItem.measure(0, 0); // 計運算元項View 的寬高

            totalHeight += listItem.getMeasuredHeight(); // 統計所有子項的總高度

        }

        ViewGroup.LayoutParams params = listView.getLayoutParams();

        params.height = totalHeight
                + (listView.getDividerHeight() * (adapter.getCount() - 1));

        // listView.getDividerHeight()獲取子項間分隔符佔用的高度

        // params.height最後得到整個ListView完整顯示需要的高度

        listView.setLayoutParams(params);

    }

}

xml佈局,注意要將ListView的父類設定為LinearLayout

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_above="@+id/txt_cancel"
        android:orientation="vertical">
        <View
            android:layout_width="fill_parent"
            android:layout_height="@dimen/y2"
            android:background="#cccccc" />

        <ListView
            android:id="@+id/lv_remain_item"
            android:layout_width="fill_parent"
            android:layout_height="0dp"
            android:cacheColorHint="#00000000"
            ></ListView>

        <View
            android:layout_width="fill_parent"
            android:layout_height="@dimen/y2"
            android:background="#cccccc" />

    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:orientation="horizontal"
        >

        <TextView
            android:id="@+id/txt_cancel"
            android:layout_width="fill_parent"
            android:layout_height="@dimen/y120"
            android:layout_alignParentBottom="true"
            android:gravity="center"
            android:text="cancel"
            android:textSize="@dimen/x32" />
    </LinearLayout>
</LinearLayout>

然後在Listview使用處,呼叫該方法.

 userListDialog.getmListView().setAdapter(scaleUserAdapter);
 SetHeight.setListViewHeightBasedOnChildren(userListDialog.getmListView(),scaleUserAdapter);

執行結果

這裡寫圖片描述

getView()呼叫情況
這裡寫圖片描述
GitHub程式碼地址:ListViewDialog,喜歡的話歡迎Start

相關推薦

Dialog裡面ListView之後高度適應引起ListView效能優化

廢話少說先來張bug圖填樓 前言 隨著RecyclerView的普及,ListView差不多是安卓快要淘汰的控制元件了,但是我們有時候還是會用到,基本上可以說是前些年最常用的Android控制元件之一了.拋開我們的主題,我們先來談談ListView的

iframe雙層後,高度適應

/**  * 自適應iframe高度  * @param iParentFrameName :父框架ID  * @param iframeName:框架ID  */   function SetIframeSize(iParentFrameName,iframeName)

DIV時外層無法適應高度三種解決方案

http://developer.51cto.com/art/201009/225428.htm 這裡向大家描述一下解決DIV巢狀時外層(父層)無法自適應高度的方法,原本自己寫的CSS程式碼是沒有錯誤的,但是為什麼在新版的瀏覽器中會發現使用DIV巢狀時外層(父層)無法自

React Native 原生RNwebView,並適應高度

import React, { Component } from 'react'; import { StyleSheet, View, Dimensions, Text } from 'react-native'; const { width, height } = Dim

ScrollViewListView、RecyclerView,使其高度適應

1、針對ScrollView巢狀ListView時只顯示第一個item高度的bug,可在activity裡動態修改ListView的高度(即計算每個item和分割線的高度後進行相加得到總高度),在setAdapter之後呼叫下面這個函式即可。 值得注意的是,此時ListVi

當PullToRefreshScrollView裡面ListView,下拉重新整理時ListView主動向上滑

當PullToRefreshScrollView裡面巢狀ListView,ListView上面還是有內容的,當下拉重新整理的 時候,資料填充完成之後ListView就會往上面滑動,導致ListView上面的資料沒法顯示,這個時候,我們能夠設定ListView上面的控制元件

關於頁面裡面視訊播放器,使用彈出層之後會被視訊擋住

<embed src="http://www.iqiyi.com/player/20140611102651/Player.swf?albumId=250686800&tvId=250686800&autoplay=true&cyclePlay=false&exclusi

ScrollViewListView或GridView等,使得其高度適應解決方案

這類的文章有很多,寫此文的目的是為了備忘吧。ScrollView裡面巢狀ListView或GridView等,兩個View都有滾動的效果,在巢狀使用時起了衝突,一般不建議兩者套用。解決的方案有很多但是

在SQL server中單引號的使用--單引號裡面一層單引號

           在使用SQL server時,遇到一個小問題:單引號裡面還要再加單引號,但是直接加會報錯。查了一些網上的解決方法。     select * from sysobjects where

div裡iframe,設定iframe及div的高度適應

1. div+iframe <div class="main"> <iframe id="contentFrame" name="contentFrame" src="url" onload="javascript:reinitIframe(

[Swift通天遁地]二、表格表單-(3)在表格中另一個表格並使Cell的高度適應

本文將演示如何在表格中巢狀另一個表格並使Cell的高度自適應,建立更加強大的佈局效果。 在專案資料夾【DemoApp】上點選滑鼠右鍵,彈出右鍵選單。 【New File】->【Cocoa Touch Class】->【Next】-> 【Class】:CustomizeUITableVi

winfrom 使用Panel 裡面Form介面使用效果

使用第三方外掛 (DevExpress 9.2)介面效果 程式碼: using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using S

iOS 中 TableView 內 CollectionView 動態高度的實現

在有社交分享平臺屬性的app中,我們經常看見類似有tableview中多圖展示。不管是釋出的表單介面中,還是社交動態的時間線的介面中,都需要根據圖片數量動態變化介面。最近剛好寫了一個這樣的介面,花了點時間寫了個Demo總結一下,希望可以幫助有需要的人。實現Demo效果如下圖。

ScrollView 使用小計 裡面的View 如何設定全屏

設定ScrollView的屬性android:fillViewport=”true” 即可 <?xml version="1.0" encoding="utf-8"?> <Scrol

List 裡面List解析

如下: testList 是List inspec 中有一個欄位inspec.insList返回值是List <s:iterator value="testList" status="st"

fragment裡面另一個fragment

最近換了工作,說白了,就是去填坑啦,首頁改版,原來是viewpager+標籤切換滑動實現,現在改成popupwindow+recyclerview實現,為了減少工時,我在原來的基礎上覆用了fragment,即需要實現在一個fragment裡面嵌入另一個fragment。Fra

flex佈局高度適應加滾動條

<div class="colwarp" style="display: flex; flex-direction: column; height: 100%; background-color: #bbf;justify-content:space-between;

ViewPager 多個不同高度的Fragment,ViewPager 高度適應

問題: ViewPager 巢狀多個Fragment,但是每個Fragment高度不一致,導致高度比較小的Fragment底部留有大片空白區域。 解決方法: 參考文章 關於ViewPager高度自適應(隨著pager頁的高度改變Viewpager的高度)

divul時div的寬度和高度適應

div中巢狀ul時div的寬度和高度隨著ul裡元素的變化而變化,下面是一個事例,可以改變li元素的內容和個數去驗證: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.

定義viewgroup裡面viewgroup佈局出問題,如fill_parent失效

我的一個新專案用到了一個自定義可以拖動的控制元件,我在網上找到了一個demo,是通過重寫viewgroup來實現的,但是當我真正投入 使用的時候,發現我在裡面自己寫的佈局fill_parent失效,找了一上午終於發現問題,沒有重寫viewgroup裡面的onMeasure方