Android UI “疑难杂症”大汇总

      文中所述问题均来自日常开发过程中遇到的Android UI 问题,部分问题各位大佬肯定遇到过,而问题的原因可能部分知道,也可能并未深究就没管了,下次可能还会犯同样的错误。刚好最近负责项目的UI性能优化这一块,借机回顾总结一下,文中主要借助源码来讲解导致这些问题的根源,正所谓“源码之下,了无秘密”

主要讲解内容:

(1)View::inflate正确使用姿势

(2)ListView的 itemView顶层控件设置margin属性失效

(3)RelativeLayout中最底的View其layout_marginBottom无效 (API 19以下)

(4)ListView Header 或Footer使用问题

(5)动态设置Background(.9图)后Padding无效的问题

(6)ListView  height设置wrap_content 导致getView()重复调用问题

   ......

 一 、View::inflate正确使用姿势

       View::inflate使用,想必各位Android 大大们肯定知道,不太清楚的可以快速看看,通常View::inflate()有以下两种方式:

(1)View::inflate(@Layout int resource,@Nullable ViewGroup root)

        当root 不为null 时,inflate(resource,root) 等价于inflate(resource,root ,true)

(2)View::inflate(@Layout int resource,@Nullable ViewGroup root,boolean attachRoot)

        除此之外,在DataBinding中也提供了一个DataBindingUtil.inflate()接口,内部实现与View::inflate()差不多。 

      上述就是View::inflate使用的几种方式,这里我直接列举几个错误案例,后面一一解释导致错误案例的真正原因:

(1)View::inflate(resource,null)  或 View::inflate(resource,null ,true or false)

         当root 为null时,resource 对应布局必须通过addView 才能添加到parent布局

        导致问题 :addView后发现resource对应布局的android:layout_xx属性失效(如宽高属性),且 随着parent ViewGroup 不同表现情况也不同。

(2)View::inflate(resource,root)  或 View::inflate(resource,root ,true)

      导致问题:addView resource 对应布局的根View ,会报错"java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first"

       发生这类问题如果没有想到是什么原因,直接看看View::inflate源码,源码如下:

View::inflate源码

从inflate源码中可以看到有3种情况关于infate出来的View,我们倒着看:

(1)情况3  ,当root == null 或 attachToRoot 为false 时

        View::inflate出来的View 是temp (通过createViewFromTag生成的View),是不带LayoutParam信息的;

(2)情况2,root!=null &&attachToRoot==true

       直接调用root.addView,这就是为什么在这种情况下主动动用addView 报错的原因。

(3)情况1 ,root!=null&&attachToRoot==false

       inflate 传参不当导致  ViewGroup::addView(View child)  ,中child 的 android:layout_xxx(宽高属性失效)且  随着parent ViewGroup 不同表现情况也不同。

      一般遇到此类问题,也没啥好怀疑人生的 ,直接看ViewGroup::addView()(代码少而精)一步步分析即可:

ViewGroup::addView

       根据addView(View chlid,int index)知,导致上述问题的原因取决于child.getLayoutParams(),进一步看 getLayoutParams()方法,从注释上知 mLayoutParam在child View is not attach to a parent ViewGroup 时 为null 。

ViewGroup.LayoutParams::getLayoutParams()

       回到上面addView中 ,如果child.getLayoutParams() 为null ,那么就会生成默认的LayoutParams,这里也不细说,直接列举几个布局的默认LayoutParms  。

ViewGroup&&RelativeLayout::generateDefaultLayoutParams()
LinearLayout::generateDefaultLayoutParams()
FrameLayout::generateDefaultLayoutParams()
AbsListView::generateDefaultLayoutParams()

      相信大家一看就明白 ,再次也不过多细讲。

      另外,在某些情况下误以为固定高度设置正确,使用android:layout_alignParentBottom = 'true' ,发现占满全屏的问题,案例如下。

满屏案例

原因:

       设置的固定高度没有生效,实际上layout_height是wrap_content,既然是wrap_content那为何会铺满整屏了,其实在RelativeLayout注释中早有说明如下图所示,RelativeLayout的大小和child View 位置关系设置不对(如高度设置成wrap_content 同时子View 设置android:layout_alignParentBottom = 'true'),可能导致循环依赖 ,导致RelativeLayout实际高度变成了match_parent ,继而出现一些奇怪的布局问题。



 二、ListView的 itemView顶层控件设置margin属性失效

       相信大家看到这个问题时,跟我当时反应一样,肯定是inflate的时候将parentView设置为null导致的,真的是这样吗?

      当我确认已经传了parentView,我就回去翻看View::inflate源码,终于找到原因了 ,在问题 一 中,已经讲过当root 不为null ,attachToRoot为false时,会将root的LayoutParams 传给child View 。ListView  继承于AbsListView,直接看AbsListView::generateLayoutParams(attrs)源码如下:

AbsListView::generateLayoutParams

       我们都知道,ViewGroup中除了LayoutParams外,还有一个MarginLayoutParams,既然是margin属性值失效,只需要确认AbsListView.LayoutParams是否继承MarginLayoutParams。

AbsListView.LayoutParams继承了LayoutParams
AbsListView.LayoutParams未继承MarginLayoutParams

       通过上述源码发现,AbsListView.LayoutParams 果然未继承MarginLayoutParams,没有提供margin相关值。因而 itemView 顶层View的margin属性失效也是正常的。

另外,android.support.v7中的RecyclerView 是继承MarginLayoutParams

RecyclerView::measureCh

解决办法 :使用padding代替margin(部分场景)或者嵌套实现(不推荐)或者直接使用RecyclerView 代替ListView

itemView顶层控件设置margin属性失效的原因,相信大家都已知晓,但在这里我还需要补充两个问题:

(1)能否对ItemView动态设置的margin

        在对一般控件设置margin值时,我们一般采用ViewGroup.MarginLayoutParams来动态设置,正如上面所说AbsListView 是没有继承MarginLayoutParams的,因而无法对ItemView动态设置margin值。

( 2)如果parentView 类型传入不对,在4.x机型上会发生crash

       堆栈信息如下:

crash 错误堆栈

      导致该crash是将 PullToRefreshListView 当作parentView 传给了inflate。为何只在4.x上crash了 ,具体分析详见同事hengwu的总结,感兴趣的可以去看看,分析很细致。

问题一 和问题二中发生的问题,都与View::inflate相关 ,那么View::inflate使用的正确姿势:

(1)使用 inflate(resource,root,false )

(2)关注传入root 类型及root的LayoutParam类型


三、RelativeLayout中最底的View其layout_marginBottom无效 (API 19以下)

失效原因RelativeLayout::onMeasure源码(本文对应api 23版本)

RelativeLayout::onMeasure源码

       当RelativeLayout的高度设置为wrap_content时,其高度height最开始需要遍历其子View计算得到,从上图中可以看到在api<19时,height 取的是最下面View的mBottom值作为height,并未计算最后一个View的margin_bottom。

解决办法:在最底View下面再添加一个height 为0的Space控件即可或者对RelativeLayout设置paddingBottom(适用于部分场景)

同理:RelativeLayout 宽度设置为wrap_content时(这种情况比较少见),也有类似的情况,唯一不同的是还与RTL Layout 布局有关(Android 4.2 ,Api 17开始支持)



四、 ListView Header 或Footer使用问题

(1)设置Header 或Footer状态为GONE后,发现Header和Footer仍然占位,效果相当

          于INVISIBLE状态;

(2)在api<=18 时,addHeader 和addFooter调用必须放在setAdapter之前;

(1)导致占位的原因

在上面分析LinearLayout(其他ViewGroup也一样) 测量源码时,发现当子View 设置成GONE时,是不进行测量的,因而也就不会存在占位情况。

那为什么在ListView 中会存在了 ,只能去看源码 : 在ListView::onMeasure测量函数中,无论其宽 和高设置是什么类型,最终都会调用measureScrapChild()这个方法,如下:

在进行测量前,并未判断View 是否GONE,就直接进行了测量,然后强制布局,因此出现了上述占位问题。

解决办法:1)多嵌套一层 ;2)不将Header或Footer设置成GONE,采用addView/removeView方式

(2)在api<=18 时,addHeader 和addFooter调用必须放在setAdapter之前

       1)API<=18时,addHeaderView会先判断mAdapter,如果mAdapter不为null且mAdapter不是HeaderViewListAdapter的实例就会抛异常;但是addFooterView则不会主动抛异常,但是FooterView是不会显示出来的。

API=18 时,addHeaderView 
API=18 时,addFooterView

2)API>18时,在addHeaderView 或addFooterView时,如果mAdapter为null或者mAdapter不是HeaderViewListAdapter的实例,则创建一个HeaderViewListAdapter对象给mAdapter。

API=19 时,addHeaderView

       综上知,不管是addHeaderView还是addFooterView,为了避免兼容性问题,addHeaderView和addFooterView最好在setAdapter()之前调用。


五、动态设置Background(.9图)后Padding无效的问题

上述问题可直接参考:设置Background导致Padding无效问题追溯

解决办法:在动态设置background之后,再重新设置一遍padding值


六、ListView  height设置wrap_content 导致getView()重复调用问题

         大家都知道当ListView对应的Adapter数据发生变化的时候(notifyDataSetChanged())、ItemView设置成GONE、addView 或removeView时,都会触发调用getView(),而我下面要讲的是当ListView 的layout_height设置成wrap_content时,为何会重复调用getView()。

      既然getView()被重复调用,那只能找对应调用处,在AbsListView中,只有obtainView()中会mAdapter.getView()。而obtainView 在AbsListView中,只有getHeightForPosition()有使用,用于计算ScrapView 的高度,这个可以忽略。那么直接在子类ListView中去看,发现 obtainView 在onMeasure 、measureHeightOfChildren、makeAndAddView、addViewAbove等中有调用,其他函数比较简单且getView多次调用与ListView的layout_height设置有关,因此 直接分析测量相关即可。

分析过程如下:

(1)在ListView::onMeasure方法中,发现当ListView的高度设置为wrap_content时,ListView的高度heightSize需要测量child View 来确认,具体代码如下:

ListView::onMeasure

(2)ListView::measureHeightofChildren()

ListView::measureHeightofChildren()

   在ListView::measureHeightofChildren()方法中,主要关注一下方法内的for循环:

    1)obtainView() ,内部会调用 mAdapter.getView();

    2)measureScrapChild(),测量废弃的child,进一步浪费资源;

   3)recycleBin.addScrapView(child,-1),在某些情况下会导致部分资源无法回收,具体如下:

         当ScrapView  有transient State 且 数据未发生变化时 mTransientStateViews会保存这个信息(不管遍历多少次,只会保存最后一个)。

addScrapView()方法

          而在对应清除TransientStateView的时候,并未清理掉position==-1的那个,具体代码如下:

clearTransientStateViews()

       那是不是将ListView 的 height 设置成match_parent 就不会多次调用 getView() 了 。 亲试 不会完全解决。

      如果这时候getView()还重复调用,那就看Listview的上一级的高是不是也是设置也match_parent的,如果不是,也将ListView 的上一级设置成match_parent。


其实,还有很多类似的相关问题,后续慢慢汇总.......

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,340评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,762评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,329评论 0 329
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,678评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,583评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,995评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,493评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,145评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,293评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,250评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,267评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,973评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,556评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,648评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,873评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,257评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,809评论 2 339

推荐阅读更多精彩内容