當前位置:
首頁 > 最新 > 記一次全民K歌的crash定位過程

記一次全民K歌的crash定位過程

全民K歌4.6版本發布後,出現了一個與RecyclerView相關的IllegalArgumentException,作此記錄。

一、問題

從下面堆棧中可以看出,RecyclerView此時正在執行布局,嘗試獲取ViewHolder緩存時發生了crash。所以在分析這個問題前,我們先來簡單了解一下RecyclerView的布局流程及緩存策略


1、布局流程

通過RecyclerView的dispatchLayout方法,可以知道其布局過程大概分為三個步驟:

dispatchLayoutStep1: preLayout預布局階段,主要處理Adapter的更新、決定使用怎樣的動畫及保存當前子View的邊界等信息,這裡布局的結果是數據變化前的狀態

dispatchLayoutStep2: 修改mInPreLayout狀態為false,然後交由LayoutManager的onLayoutChildren方法處理,它會根據當前子View的ViewHolder狀態將其回收至各個緩存隊列中,然後尋找錨點並往上下兩個方法進行填充,當需要子View時,則請求RecyclerView提供,布局結果為數據變化後的狀態。而上述crash正是發生在這一階段!代碼如下所示:

dispatchLayoutStep3: postLayout,保存當前子View的信息並結合prelayout階段的結果,觸發動畫執行,最後清理一些狀態。

2、緩存策略

RecyclerView共有以下幾種緩存:

未與RecyclerView分離的ViewHolder緩存,用於layout過程中臨時存放,可以簡單理解為當前屏幕正在顯示且數據沒有發生變化的內容,可直接復用。添加前會執行ChildHelper的detachViewForParent方法,設置View的parent對象為null,但不會從RecyclerView中remove;另外,還會對mScrapContainer對象進行設置,使得ViewHolder.isScrap為true

也未與RecyclerView分離,但數據已發生變化,用於動畫執行前的preLayout階段。同樣會執行detachViewForParent及設置mScrapContainer

當itemView滑出屏幕並從RecyclerView中被remove時,會先添加到這裡,其最大容量默認為2

業務自定義的的緩存邏輯,K歌沒有實現

最後一級緩存,添加前需要先從RecyclerView中remove掉,對不同的viewType默認緩存5個ViewHolder,復用時需要重新綁定數據

除了執行動畫的需要,在preLayout階段會優先從緩存中獲取ViewHolder外,其它情況都是先按 >>> 的順序進行復用,如果沒有可用的,就調用Adapter的onCreateViewHolder方法進行創建

有了上面對RecyclerView基礎的了解,再來看到下crash發生的地方:

邏輯上可以判斷,holder是在getScrapOrHiddenOrCachedHolderForPosition方法中獲取到的,其內部實現是對mAttachedScrap、mCachedViews 及ChildHelper中因動畫需要未與RecyclerView分離的ItemView 進行查找並返回(ChildHelper主要是接管了RecyclerView對子View的處理,解決動畫過程中,子View與Adapter數據不同步的問題,有興趣可自行了解,此處不展開),值得注意的是,這裡的緩存查找是以position為索引的,而RecycledViewPool則是通過viewType進行查找的,這很關鍵。

holder.isScrap的判斷則說明了這是中的緩存,之所以會走到引發了crash的removeDetachedView,是因為對holder的校驗沒有通過,已不符合可直接復用的特點,於是準備把它從RecyclerView中remove並改放到中,然後就crash了。

可為什麼會校驗不通過呢?再來看下校驗的源碼:

K歌業務中沒有設置stableId,mAdapter.hasStableIds()一定為false;另外,我們的crash是發生在dispatchLayoutStep2的步驟中,調用onLayoutChildren前會將mState.mInPreLayout設置為false。那就只有兩種可能了:要麼holder處於FLAG_REMOVED的狀態,要麼holder與Adapter取到的類型不一致。此處先作為線索一,後續需要用到。

回歸到crash堆棧中,看下有沒有其它的有用信息。最後,發現了ViewHolder與FeedListView的兩個細節

ViewHolder

引起crash的ViewHolder位於列表中第3位且沒有scrap字樣,也就是isScrap為false,這就不對了,調用removeDetachedView前先判斷了isScrap為true的,為什麼進到方法裡面就變成false了呢?原來傳參給的是itemView,方法內又通過itemView的LayoutParam取到ViewHolder,正常來說,View與ViewHolder間是雙向引用、一一對應的關係,這裡定是出現了 ViewHolder1指向View,View又指向了另一個ViewHolder2的情況,說明我們的View被多個ViewHolder共用了。

要解釋這個問題,就得看下Adapter創建ViewHolder的代碼:

業務使用的RecyclerView是經過了封裝的,添加了對 刷新、Header、Footer、空白、載入的支持。其中,mAdapter.onCreateViewHolder都是通過new ViewHolder(new View())的形式創建的,不可能存在View共用的情況;而另外幾個,確實有對同一類型的viewType創建多個ViewHolder的可能,但這不是正常邏輯,因為列表中的這些類型有且只有一個,只需創建一次就行。再看堆棧中的position=2,就可以鎖定是Footer的異常了,因為除了列表為空時,Footer的position為2,其它幾個類型都不會出現為2的情況。檢查了業務邏輯上Footer相關的代碼並與Header進行了對比,沒找到合理的解釋,暫且放下並標記為線索二:RecyclerView創建了兩個ViewHolder並指向了同一個Footer

繼續看上面提到的另一個細節

FeedListView

View.toString摘要:

雖然叫FeedListView,實際是繼承自RecyclerView。從toString方法可以知道,RecyclerView處於INVISIBLE的狀態。而K歌動態只有在請求到後台數據前才會是INVISIBLE的狀態,只要拿到了數據或協議失敗,都會更改為VISIBLE的狀態。

這是很奇怪的一個現象,因為從log來看,數據是載入成功的了,用戶也有在列表中進行滑動、送禮、收聽之類的互動操作,所以,我們的列表一定是可見的。鑒於Crash堆棧也不可能有錯,為了解釋這種現象,大膽推測:用戶手機上出現了兩個FeedListView,一個正常顯示,一個不可見

相對於上面的這些分析,驗證就顯得簡單多了,我們通過用戶啟動時,Fragment.OnCreate相關的log來印證了線索三是對的,且不僅是存在了兩個列表,還出現了兩個FeedSubFragment,但FeedFragment只有一個,得到線索三:動態頁面出現了兩個FeedSubFragment及FeedListView,一個正常顯示,一個不可見

FeedSubFragment是在FeedFragment的init方法中創建的,init是在onCreateView進行調用的,只會執行一次:

排除了業務邏輯創建兩個Fragment的可能,那就只能是系統創建的了。容易聯想到應用退後台被系統殺掉重建的情況,FeedFragment與FeedSubFragment都會被系統恢復,而FeedFragment恢復的過程中也會走到onCreateView的生命周期,於是又創建一個FeedSubFragment。

通過打開開發者選項中的「不保留活動」,復現了這樣的場景,恢復後產生了2個FeedSubFragment,一個正常顯示,另一個從xml載入布局後沒有發起數據的請求,於是頁面一直是loading的默認狀態,而FeedListView為INVISIBLE。

至於原因,可以先看下我們頁面的結構:

FeedFragment包含2個部分,一個是Titlebar,包含關注、好友、熱門、附近4個Tab選項,另一個是FeedSubFragment用於承載各個Tab的內容,隨Tab切換更新數據顯示。用戶點開K歌時,默認是定位好友頁的,但如果發現用戶上次離開時不在好友,那這次打開應自動切換到用戶離開時的那個頁面,這是通過TitleBar內View的performClick來觸發切換的,FeedFragment監聽到點擊後通知FeedSubFragment發起網路請求。

因為FeedFragment只會有一個FeedSubFragment的引用,所以一個能正常顯示,另一個一直是loadind的狀態,與前面用戶crash時的狀態是一致的。而對用戶來說,這是無感知的,因為正常顯示的那個Fragment不是透明的,蓋在了另一個的上面。


整理下我們已有的線索:

引起crash的holder處於FLAG_REMOVED的狀態或與Adapter取到的類型不一致

RecyclerView創建了兩個ViewHolder並指向了同一個Footer

動態頁面出現了兩個FeedSubFragment及FeedListView,一個正常顯示,一個不可見

對於線索1,我們先假設是第一種情況,通過追蹤FLAG_REMOVED設置的路徑,發現只有當業務調用了Adapter的notifyXXXRemoved方法時,才會為ViewHolder添加FLAG_REMOVED標記。而線索二中的Footer實際上是一個容器,業務調用addFooterView添加進來的布局都會填入容器中,不管用戶如何操作,對RecyclerView來說,Footer始終是有且只有一個,不存在刪除Footer的情況。於是線索一糾正為:從中取到的ViewHolder類型與Adapter取到的不一致。

中的ViewHolder是通過對比LayoutPosition查找到的,而Adapter.getItemType的結果則是分析數據集而來,兩者的不一致說明了RecyclerView的狀態與數據集產生了不同步的情況,往往出現在Adapter中的列表數據發生了變化而又沒有調用notityXXX方法通知到RecyclerView的情況下。

crash所在的列表並沒有請求後台數據卻產生了數據的變化,能產生這一現象的只有用戶發布作品後,由客戶端自己構造的假數據了。

因作品發布與K歌業務邏輯關聯較大,參考意義不大,這裡只做簡要的文字說明:

用戶發布作品後,會生成一條發布數據在動態中顯示,這條數據是存在於單例中的,兩個FeedSubFragment都能取到,發布完成並刷新列表才會把它從單例中清除。另外,用戶在K歌內的一些互動操作會觸發廣播,比如在作品詳情頁評論了作品,那動態中這個作品的feed評論計數會實時更新,不需要等待列表的刷新操作,廣播也都是有註冊的。

作品剛發布時,不可見的那個頁面對此無感知,會出現RecyclerView是Refresh、Header、Footer、Empty、Load五個item的狀態,而Adapter的數據集中在Header與Footer間多了一條假feed,雖然沒有調用notifyXXX,但當有互動操作或跳其它Activity返回等其它原因觸發layout時,也不會引起crash,如下:

①② 通過position可以從正確獲取到原來的ViewHolder並直接復用

③ 通過position取到了Footer的ViewHolder,發現類型不同,把它從布局中remove並添加到緩存池,最後新創建一個假Feed的ViewHolder

④ 取到了Empty的ViewHolder,同樣回收至RecycledViewPool,但因為上一步有把Footer的ViewHolder添加到了RecycledViewPool,處理完Empty後,會嘗試從RecycledViewPool查找,而這裡是通過viewType來查找的,所以可以找到上一步添加進來的ViewHolder,從而復用

⑤⑥ 同④

當假feed已經被layout出來,數據被刪除卻沒有notify的情況下執行layout又會怎樣呢?

①② 可直接復用

③ 取到了假feed的ViewHolder,回收至RecycledViewPool,然後重新創建了一個Footer的ViewHolder,這就導致了兩個ViewHolder指向同一個View的出現,一個新創建的添加到RecyclerView中顯示,並清除FLAG_TMP_DETACHED標記,另一個仍然存在於Scrap緩存中未被使用

④ 取到了Scrap緩存中Footer的ViewHolder,嘗試回收至RecycledViewPool,卻發現Footer已經不是FLAG_TMP_DETACHED的狀態,因為上一步已經把它添加到RecyclerView中,清除了這一標記,於是拋出文章開頭的IllegalArgumentException異常

可能有人會感興趣增刪數據並調用了notifyXXXRemoved的正常情況下,RecyclerView是如何在preLayout及postLayout階段都能通過position獲取到正確的ViewHolder的,可以自行了解下ViewHolder的mPreLayoutPosition跟mPosition的作用,這裡不細說了


至此,原因也就比較清晰了:用戶使用K歌停留在動態非好友頁,退後台被系統殺掉重啟時,沒有考慮到Fragment恢復的情況,導致在正常的Fragment下多生成了一個不可見的Fragment,之後發布了作品並對其執行了會引起數據變化的互動操作,使其layout到布局中,刷新列表後不可見的RecyclerView列表狀態與Adapter數據不同步,跳轉到其它Activity再返回時,觸發了RecyclerView的重新布局,檢測到了狀態不對並拋出了異常。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 騰訊音樂技術團隊 的精彩文章:

TAG:騰訊音樂技術團隊 |