您好,登錄后才能下訂單哦!
這篇“crash定位過程實例分析”文章的知識點大部分人都不太理解,所以小編給大家總結了以下內容,內容詳細,步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“crash定位過程實例分析”文章吧。
從下面堆棧中可以看出,RecyclerView此時正在執行布局,嘗試獲取ViewHolder緩存時發生了crash。所以在分析這個問題前,我們先來簡單了解一下RecyclerView的布局流程及緩存策略
二、準備
通過RecyclerView的dispatchLayout方法,可以知道其布局過程大概分為三個步驟:
dispatchLayoutStep1: preLayout預布局階段,主要處理Adapter的更新、決定使用怎樣的動畫及保存當前子View的邊界等信息,這里布局的結果是數據變化前的狀態
dispatchLayoutStep2: 修改mInPreLayout狀態為false,然后交由LayoutManager的onLayoutChildren方法處理,它會根據當前子View的ViewHolder狀態將其回收至各個緩存隊列中,然后尋找錨點并往上下兩個方法進行填充,當需要子View時,則請求RecyclerView提供,布局結果為數據變化后的狀態。而上述crash正是發生在這一階段!代碼如下所示:
private void dispatchLayoutStep2() { // some code here // Step 2: Run layout mState.mInPreLayout = false; mLayout.onLayoutChildren(mRecycler, mState); // some code here }
dispatchLayoutStep3: postLayout,保存當前子View的信息并結合prelayout階段的結果,觸發動畫執行,最后清理一些狀態。
RecyclerView共有以下幾種緩存:
mAttachedScrap
未與RecyclerView分離的ViewHolder緩存,用于layout過程中臨時存放,可以簡單理解為當前屏幕正在顯示且數據沒有發生變化的內容,可直接復用。添加前會執行ChildHelper的detachViewForParent方法,設置View的parent對象為null,但不會從RecyclerView中remove;另外,還會對mScrapContainer對象進行設置,使得ViewHolder.isScrap為true
mChangedScrap
也未與RecyclerView分離,但數據已發生變化,用于動畫執行前的preLayout階段。同樣會執行detachViewForParent及設置mScrapContainer
mCachedViews
當itemView滑出屏幕并從RecyclerView中被remove時,會先添加到這里,其最大容量默認為2
mVewCacheExtension
業務自定義的的緩存邏輯,K歌沒有實現
RecycledViewPool
最后一級緩存,添加前需要先從RecyclerView中remove掉,對不同的viewType默認緩存5個ViewHolder,復用時需要重新綁定數據
除了執行動畫的需要,在preLayout階段會優先從mChangedScrap
緩存中獲取ViewHolder外,其它情況都是先按 mAttachedScrap
>mCachedViews
>mViewCachedExtension
>RecycledViewPool
的順序進行復用,如果沒有可用的,就調用Adapter的onCreateViewHolder方法進行創建
有了上面對RecyclerView基礎的了解,再來看到下crash發生的地方:
ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) { // some code here... // 拿到ViewHolder緩存 holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); if (holder != null) { // 對ViewHolder進行校驗,但沒有通過 if (!validateViewHolderForOffsetPosition(holder)) { if (!dryRun) { // 準備添加到到RecyledViewPool holder.addFlags(ViewHolder.FLAG_INVALID); // isScrap 說明是從mAttachedScrap獲取到的 if (holder.isScrap()) { // crash發生在這里 removeDetachedView(holder.itemView, false); holder.unScrap(); } else if (holder.wasReturnedFromScrap()) { holder.clearReturnedFromScrapFlag(); } recycleViewHolderInternal(holder); } holder = null; } else { fromScrapOrHiddenOrCache = true; } } // some code here...
邏輯上可以判斷,holder是在getScrapOrHiddenOrCachedHolderForPosition方法中獲取到的,其內部實現是對mAttachedScrap、mCachedViews 及ChildHelper中因動畫需要未與RecyclerView分離的ItemView 進行查找并返回(ChildHelper主要是接管了RecyclerView對子View的處理,解決動畫過程中,子View與Adapter數據不同步的問題,有興趣可自行了解,此處不展開),值得注意的是,這里的緩存查找是以position為索引的,而RecycledViewPool則是通過viewType進行查找的,這很關鍵。
holder.isScrap的判斷則說明了這是mAttachedScrap
中的緩存,之所以會走到引發了crash的removeDetachedView,是因為對holder的校驗沒有通過,已不符合可直接復用的特點,于是準備把它從RecyclerView中remove并改放到RecycledViewPool
中,然后就crash了。
可為什么會校驗不通過呢?再來看下校驗的源碼:
boolean validateViewHolderForOffsetPosition(ViewHolder holder) { // if it is a removed holder, nothing to verify since we cannot ask adapter anymore // if it is not removed, verify the type and id. if (holder.isRemoved()) { if (DEBUG && !mState.isPreLayout()) { throw new IllegalStateException("should not receive a removed view unless it" + " is pre layout" + exceptionLabel()); } return mState.isPreLayout(); } if (holder.mPosition < 0 || holder.mPosition >= mAdapter.getItemCount()) { throw new IndexOutOfBoundsException("Inconsistency detected. Invalid view holder " + "adapter position" + holder + exceptionLabel()); } if (!mState.isPreLayout()) { // don't check type if it is pre-layout. final int type = mAdapter.getItemViewType(holder.mPosition); if (type != holder.getItemViewType()) { return false; } } if (mAdapter.hasStableIds()) { return holder.getItemId() == mAdapter.getItemId(holder.mPosition); } return true; }
K歌業務中沒有設置stableId,mAdapter.hasStableIds()一定為false;另外,我們的crash是發生在dispatchLayoutStep2的步驟中,調用onLayoutChildren前會將mState.mInPreLayout設置為false。那就只有兩種可能了:要么holder處于FLAG_REMOVED的狀態,要么holder與Adapter取到的類型不一致。此處先作為線索一,后續需要用到。
回歸到crash堆棧中,看下有沒有其它的有用信息。最后,發現了ViewHolder與FeedListView的兩個細節
ViewHolder{394df98d position=2 id=-1, oldPos=-1, pLpos:-1}
// 這里是ViewHolder.toString方法摘要 // some code here... if (isScrap()) { sb.append(" scrap ").append(mInChangeScrap ? "[changeScrap]" : "[attachedScrap]"); } // some code here... return sb.toString();
引起crash的ViewHolder位于列表中第3位且沒有scrap字樣,也就是isScrap為false,這就不對了,調用removeDetachedView前先判斷了isScrap為true的,為什么進到方法里面就變成false了呢?原來傳參給的是itemView,方法內又通過itemView的LayoutParam取到ViewHolder,正常來說,View與ViewHolder間是雙向引用、一一對應的關系,這里定是出現了 ViewHolder1指向View,View又指向了另一個ViewHolder2的情況,說明我們的View被多個ViewHolder共用了。
要解釋這個問題,就得看下Adapter創建ViewHolder的代碼:
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if (viewType == REFRESH_HEADER) { // 下拉刷新 return new RefreshHeaderContainerViewHolder(mRefreshHeaderContainer); } else if (viewType == HEADER) { // Header容器 return new HeaderContainerViewHolder(mHeaderContainer); } else if (viewType == FOOTER) { // Footer容器 return new FooterContainerViewHolder(mFooterContainer); } else if(viewType == FOOTER_EMPTY){ // 列表內容少,希望用空白填滿列表 return new FooterEmptyViewHolder(mFooterEmpty); } else if (viewType == LOAD_MORE_FOOTER) { // 上拉加載 return new LoadMoreFooterContainerViewHolder(mLoadMoreFooterContainer); } else { // 具體業務模塊自行創建 return mAdapter.onCreateViewHolder(parent, viewType); } }
業務使用的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{27f84f4a IFE….. ……ID 0,231-1080,1767 #7f0d0416 app:id/se}
View.toString摘要:
public String toString() { StringBuilder out = new StringBuilder(128); out.append(getClass().getName()); out.append('{'); out.append(Integer.toHexString(System.identityHashCode(this))); out.append(' '); switch (mViewFlags&VISIBILITY_MASK) { case VISIBLE: out.append('V'); break; case INVISIBLE: out.append('I'); break; case GONE: out.append('G'); break; default: out.append('.'); break; } }
雖然叫FeedListView,實際是繼承自RecyclerView。從toString方法可以知道,RecyclerView處于INVISIBLE的狀態。而K歌動態只有在請求到后臺數據前才會是INVISIBLE的狀態,只要拿到了數據或協議失敗,都會更改為VISIBLE的狀態。
這是很奇怪的一個現象,因為從log來看,數據是加載成功的了,用戶也有在列表中進行滑動、送禮、收聽之類的互動操作,所以,我們的列表一定是可見的。鑒于Crash堆棧也不可能有錯,為了解釋這種現象,大膽推測:用戶手機上出現了兩個FeedListView,一個正常顯示,一個不可見
相對于上面的這些分析,驗證就顯得簡單多了,我們通過用戶啟動時,Fragment.OnCreate相關的log來印證了線索三是對的,且不僅是存在了兩個列表,還出現了兩個FeedSubFragment,但FeedFragment只有一個,得到 線索三:動態頁面出現了兩個FeedSubFragment及FeedListView,一個正常顯示,一個不可見。
onCreate:com.tencent.karaoke.module.feed.ui.FeedFragment onCreate:com.tencent.karaoke.module.feed.ui.FeedSubFragment
onCreate:com.tencent.karaoke.module.feed.ui.FeedSubFragment
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的情況。于是線索一糾正為:從mAttachedScrap
中取到的ViewHolder類型與Adapter取到的不一致。
mAttachedScrap
中的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可以從mAttachedScrap
正確獲取到原來的ViewHolder并直接復用
③ 通過position取到了Footer的ViewHolder,發現類型不同,把它從布局中remove并添加到緩存池RecycledViewPool
,最后新創建一個假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的作用,這里不細說了
以上就是關于“crash定位過程實例分析”這篇文章的內容,相信大家都有了一定的了解,希望小編分享的內容對大家有幫助,若想了解更多相關的知識內容,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。