? ? ? 文中所述問(wèn)題均來(lái)自日常開發(fā)過(guò)程中遇到的Android UI 問(wèn)題,部分問(wèn)題各位大佬肯定遇到過(guò),而問(wèn)題的原因可能部分知道,也可能并未深究就沒(méi)管了,下次可能還會(huì)犯同樣的錯(cuò)誤。剛好最近負(fù)責(zé)項(xiàng)目的UI性能優(yōu)化這一塊,借機(jī)回顧總結(jié)一下,文中主要借助源碼來(lái)講解導(dǎo)致這些問(wèn)題的根源,正所謂“源碼之下,了無(wú)秘密”。
主要講解內(nèi)容:
(1)View::inflate正確使用姿勢(shì)
(2)ListView的 itemView頂層控件設(shè)置margin屬性失效
(3)RelativeLayout中最底的View其layout_marginBottom無(wú)效 (API 19以下)
(4)ListView Header 或Footer使用問(wèn)題
(5)動(dòng)態(tài)設(shè)置Background(.9圖)后Padding無(wú)效的問(wèn)題
(6)ListView? height設(shè)置wrap_content 導(dǎo)致getView()重復(fù)調(diào)用問(wèn)題
? ?......
?一 、View::inflate正確使用姿勢(shì)
? ? ? ?View::inflate使用,想必各位Android 大大們肯定知道,不太清楚的可以快速看看,通常View::inflate()有以下兩種方式:
(1)View::inflate(@Layout int resource,@Nullable ViewGroup root)
? ? ? ? 當(dāng)root 不為null 時(shí),inflate(resource,root) 等價(jià)于inflate(resource,root ,true)
(2)View::inflate(@Layout int resource,@Nullable ViewGroup root,boolean attachRoot)
? ? ? ? 除此之外,在DataBinding中也提供了一個(gè)DataBindingUtil.inflate()接口,內(nèi)部實(shí)現(xiàn)與View::inflate()差不多。?
? ? ? 上述就是View::inflate使用的幾種方式,這里我直接列舉幾個(gè)錯(cuò)誤案例,后面一一解釋導(dǎo)致錯(cuò)誤案例的真正原因:
(1)View::inflate(resource,null)? 或 View::inflate(resource,null ,true or false)
? ? ? ? ?當(dāng)root 為null時(shí),resource 對(duì)應(yīng)布局必須通過(guò)addView 才能添加到parent布局
? ? ? ? 導(dǎo)致問(wèn)題 :addView后發(fā)現(xiàn)resource對(duì)應(yīng)布局的android:layout_xx屬性失效(如寬高屬性),且 隨著parent ViewGroup 不同表現(xiàn)情況也不同。
(2)View::inflate(resource,root)? 或 View::inflate(resource,root ,true)
? ? ? 導(dǎo)致問(wèn)題:addView resource 對(duì)應(yīng)布局的根View ,會(huì)報(bào)錯(cuò)"java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first"
? ? ? ?發(fā)生這類問(wèn)題如果沒(méi)有想到是什么原因,直接看看View::inflate源碼,源碼如下:
從inflate源碼中可以看到有3種情況關(guān)于infate出來(lái)的View,我們倒著看:
(1)情況3 ?,當(dāng)root == null 或 attachToRoot 為false 時(shí)
? ? ? ? View::inflate出來(lái)的View 是temp (通過(guò)createViewFromTag生成的View),是不帶LayoutParam信息的;
(2)情況2,root!=null &&attachToRoot==true
? ? ? ?直接調(diào)用root.addView,這就是為什么在這種情況下主動(dòng)動(dòng)用addView 報(bào)錯(cuò)的原因。
(3)情況1 ,root!=null&&attachToRoot==false
? ? ? ?inflate 傳參不當(dāng)導(dǎo)致 ?ViewGroup::addView(View child) ?,中child 的 android:layout_xxx(寬高屬性失效)且 ?隨著parent ViewGroup?不同表現(xiàn)情況也不同。
? ? ? 一般遇到此類問(wèn)題,也沒(méi)啥好懷疑人生的 ,直接看ViewGroup::addView()(代碼少而精)一步步分析即可:
? ? ? ?根據(jù)addView(View chlid,int index)知,導(dǎo)致上述問(wèn)題的原因取決于child.getLayoutParams(),進(jìn)一步看 getLayoutParams()方法,從注釋上知 mLayoutParam在child View is not attach to a parent ViewGroup 時(shí) 為null 。
? ? ? ?回到上面addView中 ,如果child.getLayoutParams() 為null ,那么就會(huì)生成默認(rèn)的LayoutParams,這里也不細(xì)說(shuō),直接列舉幾個(gè)布局的默認(rèn)LayoutParms ?。
? ? ? 相信大家一看就明白 ,再次也不過(guò)多細(xì)講。
? ? ? 另外,在某些情況下誤以為固定高度設(shè)置正確,使用android:layout_alignParentBottom = 'true' ,發(fā)現(xiàn)占滿全屏的問(wèn)題,案例如下。
原因:
? ? ? ?設(shè)置的固定高度沒(méi)有生效,實(shí)際上layout_height是wrap_content,既然是wrap_content那為何會(huì)鋪滿整屏了,其實(shí)在RelativeLayout注釋中早有說(shuō)明如下圖所示,RelativeLayout的大小和child View 位置關(guān)系設(shè)置不對(duì)(如高度設(shè)置成wrap_content 同時(shí)子View 設(shè)置android:layout_alignParentBottom = 'true'),可能導(dǎo)致循環(huán)依賴 ,導(dǎo)致RelativeLayout實(shí)際高度變成了match_parent ,繼而出現(xiàn)一些奇怪的布局問(wèn)題。
?二、ListView的 itemView頂層控件設(shè)置margin屬性失效
? ? ? ?相信大家看到這個(gè)問(wèn)題時(shí),跟我當(dāng)時(shí)反應(yīng)一樣,肯定是inflate的時(shí)候?qū)arentView設(shè)置為null導(dǎo)致的,真的是這樣嗎?
? ? ? 當(dāng)我確認(rèn)已經(jīng)傳了parentView,我就回去翻看View::inflate源碼,終于找到原因了 ,在問(wèn)題 一 中,已經(jīng)講過(guò)當(dāng)root 不為null ,attachToRoot為false時(shí),會(huì)將root的LayoutParams 傳給child View 。ListView ?繼承于AbsListView,直接看AbsListView::generateLayoutParams(attrs)源碼如下:
? ? ? ?我們都知道,ViewGroup中除了LayoutParams外,還有一個(gè)MarginLayoutParams,既然是margin屬性值失效,只需要確認(rèn)AbsListView.LayoutParams是否繼承MarginLayoutParams。
? ? ? ?通過(guò)上述源碼發(fā)現(xiàn),AbsListView.LayoutParams 果然未繼承MarginLayoutParams,沒(méi)有提供margin相關(guān)值。因而 itemView 頂層View的margin屬性失效也是正常的。
另外,android.support.v7中的RecyclerView 是繼承MarginLayoutParams
解決辦法 :使用padding代替margin(部分場(chǎng)景)或者嵌套實(shí)現(xiàn)(不推薦)或者直接使用RecyclerView 代替ListView
itemView頂層控件設(shè)置margin屬性失效的原因,相信大家都已知曉,但在這里我還需要補(bǔ)充兩個(gè)問(wèn)題:
(1)能否對(duì)ItemView動(dòng)態(tài)設(shè)置的margin
? ? ? ? 在對(duì)一般控件設(shè)置margin值時(shí),我們一般采用ViewGroup.MarginLayoutParams來(lái)動(dòng)態(tài)設(shè)置,正如上面所說(shuō)AbsListView 是沒(méi)有繼承MarginLayoutParams的,因而無(wú)法對(duì)ItemView動(dòng)態(tài)設(shè)置margin值。
( 2)如果parentView 類型傳入不對(duì),在4.x機(jī)型上會(huì)發(fā)生crash
? ? ? ?堆棧信息如下:
? ? ? 導(dǎo)致該crash是將 PullToRefreshListView 當(dāng)作parentView 傳給了inflate。為何只在4.x上crash了 ,具體分析詳見(jiàn)同事hengwu的總結(jié),感興趣的可以去看看,分析很細(xì)致。
問(wèn)題一 和問(wèn)題二中發(fā)生的問(wèn)題,都與View::inflate相關(guān) ,那么View::inflate使用的正確姿勢(shì):
(1)使用 inflate(resource,root,false )
(2)關(guān)注傳入root 類型及root的LayoutParam類型
三、RelativeLayout中最底的View其layout_marginBottom無(wú)效 (API 19以下)
失效原因:RelativeLayout::onMeasure源碼(本文對(duì)應(yīng)api 23版本)
? ? ? ?當(dāng)RelativeLayout的高度設(shè)置為wrap_content時(shí),其高度height最開始需要遍歷其子View計(jì)算得到,從上圖中可以看到在api<19時(shí),height 取的是最下面View的mBottom值作為height,并未計(jì)算最后一個(gè)View的margin_bottom。
解決辦法:在最底View下面再添加一個(gè)height 為0的Space控件即可或者對(duì)RelativeLayout設(shè)置paddingBottom(適用于部分場(chǎng)景)
同理:RelativeLayout 寬度設(shè)置為wrap_content時(shí)(這種情況比較少見(jiàn)),也有類似的情況,唯一不同的是還與RTL Layout 布局有關(guān)(Android 4.2 ,Api 17開始支持)
四、 ListView Header 或Footer使用問(wèn)題
(1)設(shè)置Header 或Footer狀態(tài)為GONE后,發(fā)現(xiàn)Header和Footer仍然占位,效果相當(dāng)
? ? ? ? ? 于INVISIBLE狀態(tài);
(2)在api<=18 時(shí),addHeader 和addFooter調(diào)用必須放在setAdapter之前;
(1)導(dǎo)致占位的原因:
在上面分析LinearLayout(其他ViewGroup也一樣) 測(cè)量源碼時(shí),發(fā)現(xiàn)當(dāng)子View 設(shè)置成GONE時(shí),是不進(jìn)行測(cè)量的,因而也就不會(huì)存在占位情況。
那為什么在ListView 中會(huì)存在了 ,只能去看源碼 : 在ListView::onMeasure測(cè)量函數(shù)中,無(wú)論其寬 和高設(shè)置是什么類型,最終都會(huì)調(diào)用measureScrapChild()這個(gè)方法,如下:
在進(jìn)行測(cè)量前,并未判斷View 是否GONE,就直接進(jìn)行了測(cè)量,然后強(qiáng)制布局,因此出現(xiàn)了上述占位問(wèn)題。
解決辦法:1)多嵌套一層 ;2)不將Header或Footer設(shè)置成GONE,采用addView/removeView方式
(2)在api<=18 時(shí),addHeader 和addFooter調(diào)用必須放在setAdapter之前
? ? ? ?1)API<=18時(shí),addHeaderView會(huì)先判斷mAdapter,如果mAdapter不為null且mAdapter不是HeaderViewListAdapter的實(shí)例就會(huì)拋異常;但是addFooterView則不會(huì)主動(dòng)拋異常,但是FooterView是不會(huì)顯示出來(lái)的。
2)API>18時(shí),在addHeaderView 或addFooterView時(shí),如果mAdapter為null或者mAdapter不是HeaderViewListAdapter的實(shí)例,則創(chuàng)建一個(gè)HeaderViewListAdapter對(duì)象給mAdapter。
? ? ? ?綜上知,不管是addHeaderView還是addFooterView,為了避免兼容性問(wèn)題,addHeaderView和addFooterView最好在setAdapter()之前調(diào)用。
五、動(dòng)態(tài)設(shè)置Background(.9圖)后Padding無(wú)效的問(wèn)題
上述問(wèn)題可直接參考:設(shè)置Background導(dǎo)致Padding無(wú)效問(wèn)題追溯
解決辦法:在動(dòng)態(tài)設(shè)置background之后,再重新設(shè)置一遍padding值
六、ListView? height設(shè)置wrap_content 導(dǎo)致getView()重復(fù)調(diào)用問(wèn)題
? ? ? ? ?大家都知道當(dāng)ListView對(duì)應(yīng)的Adapter數(shù)據(jù)發(fā)生變化的時(shí)候(notifyDataSetChanged())、ItemView設(shè)置成GONE、addView 或removeView時(shí),都會(huì)觸發(fā)調(diào)用getView(),而我下面要講的是當(dāng)ListView 的layout_height設(shè)置成wrap_content時(shí),為何會(huì)重復(fù)調(diào)用getView()。
? ? ? 既然getView()被重復(fù)調(diào)用,那只能找對(duì)應(yīng)調(diào)用處,在AbsListView中,只有obtainView()中會(huì)mAdapter.getView()。而obtainView 在AbsListView中,只有g(shù)etHeightForPosition()有使用,用于計(jì)算ScrapView 的高度,這個(gè)可以忽略。那么直接在子類ListView中去看,發(fā)現(xiàn) obtainView 在onMeasure 、measureHeightOfChildren、makeAndAddView、addViewAbove等中有調(diào)用,其他函數(shù)比較簡(jiǎn)單且getView多次調(diào)用與ListView的layout_height設(shè)置有關(guān),因此 直接分析測(cè)量相關(guān)即可。
分析過(guò)程如下:
(1)在ListView::onMeasure方法中,發(fā)現(xiàn)當(dāng)ListView的高度設(shè)置為wrap_content時(shí),ListView的高度heightSize需要測(cè)量child View 來(lái)確認(rèn),具體代碼如下:
(2)ListView::measureHeightofChildren()
? ?在ListView::measureHeightofChildren()方法中,主要關(guān)注一下方法內(nèi)的for循環(huán):
? ? 1)obtainView() ,內(nèi)部會(huì)調(diào)用 mAdapter.getView();
? ? 2)measureScrapChild(),測(cè)量廢棄的child,進(jìn)一步浪費(fèi)資源;
? ?3)recycleBin.addScrapView(child,-1),在某些情況下會(huì)導(dǎo)致部分資源無(wú)法回收,具體如下:
? ? ? ? ?當(dāng)ScrapView ?有transient State 且 數(shù)據(jù)未發(fā)生變化時(shí) mTransientStateViews會(huì)保存這個(gè)信息(不管遍歷多少次,只會(huì)保存最后一個(gè))。
? ? ? ? ? 而在對(duì)應(yīng)清除TransientStateView的時(shí)候,并未清理掉position==-1的那個(gè),具體代碼如下:
? ? ? ?那是不是將ListView 的 height 設(shè)置成match_parent 就不會(huì)多次調(diào)用 getView() 了 。 親試 不會(huì)完全解決。
? ? ? 如果這時(shí)候getView()還重復(fù)調(diào)用,那就看Listview的上一級(jí)的高是不是也是設(shè)置也match_parent的,如果不是,也將ListView 的上一級(jí)設(shè)置成match_parent。