Android UI “疑難雜癥”大匯總

? ? ? 文中所述問(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源碼,源碼如下:

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()(代碼少而精)一步步分析即可:

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 。

ViewGroup.LayoutParams::getLayoutParams()

? ? ? ?回到上面addView中 ,如果child.getLayoutParams() 為null ,那么就會(huì)生成默認(rèn)的LayoutParams,這里也不細(xì)說(shuō),直接列舉幾個(gè)布局的默認(rèn)LayoutParms ?。

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

? ? ? 相信大家一看就明白 ,再次也不過(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)源碼如下:

AbsListView::generateLayoutParams

? ? ? ?我們都知道,ViewGroup中除了LayoutParams外,還有一個(gè)MarginLayoutParams,既然是margin屬性值失效,只需要確認(rèn)AbsListView.LayoutParams是否繼承MarginLayoutParams。

AbsListView.LayoutParams繼承了LayoutParams
AbsListView.LayoutParams未繼承MarginLayoutParams

? ? ? ?通過(guò)上述源碼發(fā)現(xiàn),AbsListView.LayoutParams 果然未繼承MarginLayoutParams,沒(méi)有提供margin相關(guān)值。因而 itemView 頂層View的margin屬性失效也是正常的。

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

RecyclerView::measureCh

解決辦法 :使用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

? ? ? ?堆棧信息如下:

crash 錯(cuò)誤堆棧

? ? ? 導(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版本)

RelativeLayout::onMeasure源碼

? ? ? ?當(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)的。

API=18 時(shí),addHeaderView?
API=18 時(shí),addFooterView

2)API>18時(shí),在addHeaderView 或addFooterView時(shí),如果mAdapter為null或者mAdapter不是HeaderViewListAdapter的實(shí)例,則創(chuàng)建一個(gè)HeaderViewListAdapter對(duì)象給mAdapter。

API=19 時(shí),addHeaderView

? ? ? ?綜上知,不管是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),具體代碼如下:

ListView::onMeasure

(2)ListView::measureHeightofChildren()

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è))。

addScrapView()方法

? ? ? ? ? 而在對(duì)應(yīng)清除TransientStateView的時(shí)候,并未清理掉position==-1的那個(gè),具體代碼如下:

clearTransientStateViews()

? ? ? ?那是不是將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。


其實(shí),還有很多類似的相關(guān)問(wèn)題,后續(xù)慢慢匯總.......

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,619評(píng)論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,155評(píng)論 3 425
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,635評(píng)論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,539評(píng)論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,255評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,646評(píng)論 1 326
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,655評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,838評(píng)論 0 289
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,399評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,146評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,338評(píng)論 1 372
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,893評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,565評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,983評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,257評(píng)論 1 292
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,059評(píng)論 3 397
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,296評(píng)論 2 376

推薦閱讀更多精彩內(nèi)容