中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

crash定位過程實例分析

發布時間:2022-01-11 17:22:04 來源:億速云 閱讀:145 作者:iii 欄目:開發技術

這篇“crash定位過程實例分析”文章的知識點大部分人都不太理解,所以小編給大家總結了以下內容,內容詳細,步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“crash定位過程實例分析”文章吧。

一、問題

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

二、準備

1、布局流程

通過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階段的結果,觸發動畫執行,最后清理一些狀態。

2、緩存策略

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進行調用的,只會執行一次:

crash定位過程實例分析

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

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

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

crash定位過程實例分析

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

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

四、關聯

整理下我們已有的線索:

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

  2. RecyclerView創建了兩個ViewHolder并指向了同一個Footer

  3. 動態頁面出現了兩個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,如下:

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定位過程實例分析”這篇文章的內容,相信大家都有了一定的了解,希望小編分享的內容對大家有幫助,若想了解更多相關的知識內容,請關注億速云行業資訊頻道。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

武邑县| 永济市| 上栗县| 银川市| 永康市| 南京市| 湟中县| 上饶市| 榆社县| 天门市| 偏关县| 紫金县| 伊通| 双辽市| 靖西县| 舒兰市| 洞口县| 阿克苏市| 光泽县| 钟山县| 武胜县| 宁夏| 五指山市| 霸州市| 泰顺县| 连云港市| 怀远县| 土默特右旗| 余姚市| 三河市| 谢通门县| 西昌市| 泗阳县| 龙胜| 札达县| 新宁县| 府谷县| 邓州市| 忻州市| 剑阁县| 沿河|