由Dialog里面嵌套ListView之后的高度自适应引起的ListView性能优化
先说ListView给高的正确做法.
android:layout_height属性:
必须将ListView的布局高度属性设置为非“wrap_content”(可以是“match_parent / fill_parent / 400dp等绝对数值”)
废话少说先来张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性能优化的更多相关文章
- ListView显示Sqlite的数据美化版与性能优化
在上一篇文章中,我们已经实现在listview显示数据库内容的.但是我们listview中,排版不是很好看,所以这篇文章呢,我们来对listveiw进行美化.哈哈,说白了,就是对listview添加一 ...
- Android中ListView嵌套进ScrollView时高度很小的解决方案
package com.example.test.util; import android.view.View; import android.view.ViewGroup; import andro ...
- android 多个listView的向下滚动设置 listView动态设置高度代码
墨迹天气图: 这里都是用的android里面的shape实现的,实现起来比较简单,只是在滚动的时候有点小麻烦... 当我们多个ListView超出了它的父控件LinearLayout的时候,它们每个L ...
- flex布局嵌套之高度自适应
查遍各大资源无任何flex嵌套布局的例子,经过自己折腾完成了项目中的高度自适应需求(更多应用于前端组件) 效果图: html代码:(关键地方已经用颜色特别标识 ^_^) <!DOCTYPE ht ...
- 解决viewpager+多个fragment+listview,listview展示内容高度不自适应出现多余空白问题
一.重写viewpager import android.content.Context; import android.support.v4.view.ViewPager; import andro ...
- [Swift通天遁地]二、表格表单-(3)在表格中嵌套另一个表格并使Cell的高度自适应
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★➤微信公众号:山青咏芝(shanqingyongzhi)➤博客园地址:山青咏芝(https://www.cnblogs. ...
- iframe高度自适应
前两天在网上看到了一道面试题,问iframe高度自适应的问题.发现自己之前几乎没有关注过iframe的问题,所以在这里记录一下. 原题目是: 页面A的域名是:http://www.taobao.com ...
- 里面的div怎么撑开外面的div,让高度自适应
关于容器高度自适应的兼容性问题.1.有些时候,我们希望容器有一个固定高度,但当其中的内容多的时候,又希望高度能够自适应,也即容器在纵向能被撑开,且如果有背景,也能够自适应.在一般情况下,使用min-h ...
- iframe高度自适应内容
JS自适应高度,其实就是设置iframe的高度,使其等于内嵌网页的高度,从而看不出来滚动条和嵌套痕迹.对于用户体验和网站美观起着重要作用. 如果内容是固定的,那么我们可以通过CSS来给它直接定义一个高 ...
随机推荐
- 关于莫比乌斯函数的塞 : 莫比乌斯前n项和 , 莫比乌斯函数绝对值的前n项和
https://ac.nowcoder.com/acm/contest/874#submit/{%22problemIdFilter%22%3A25455%2C%22statusTypeFilter% ...
- [iOS]使用Windows Azure來做iOS的推播通知 (转帖)
這一篇我們用Windows Azure 的Mobile Service 來實作iOS的推播通知,底下我們分成三個階段來探討如何實作推播通知的服務: 第一階段: 開啓你的Windows Aure服務 ...
- 【数组】Unique Paths
题目: A robot is located at the top-left corner of a m x n grid (marked 'Start' in the diagram below). ...
- Chapter 6. Names
6.2. Names and Identifiers A name is used to refer to an entity declared in a program. There are two ...
- KNN理解
基本思想 K近邻算法,即是给定一个训练数据集,对新的输入实例,在训练数据集中找到与该实例最邻近的K个实例,这K个实例的多数属于某个类,就把该输入实例分类到这个类中.如下面的图: 通俗一点来说,就是找最 ...
- 深度学习(八) Batch Normalization
一:BN的解释: 定义: 顾名思义,batch normalization嘛,就是“批规范化”咯.Google在ICML文中描述的非常清晰,即在每次SGD时,通过mini-batch来对相应的act ...
- bind(0)
通配地址就是全0的地址,由内核随机选取
- PTA (Advanced Level) 1040 Longest Symmetric String
1040 Longest Symmetric String (25 分) Given a string, you are supposed to output the length of the lo ...
- MyBatis Generator 详解(转)
MyBatis Generator中文文档 MyBatis Generator中文文档地址:http://mbg.cndocs.tk/ 该中文文档由于尽可能和原文内容一致,所以有些地方如果不熟悉,看中 ...
- PHP OOP面向对象部分方法归总(代码实例子)
<?php//对象名存在栈内存中,数据存在堆内存中class leyangjun{ var $name; //定义变量 var $age; var $sex; ...