Investigation of IllegalArgumentException in Android RecyclerView after QQ Music K歌 4.6 Release
The QQ Music K歌 4.6 crash occurs when a hidden RecyclerView, created by fragment recreation after the app is killed, retains a stale cached Footer ViewHolder that mismatches the adapter’s type after an unnotified data change, triggering an IllegalArgumentException during the RecyclerView’s layout step 2.
The article records a crash that appeared in the QQ Music K歌 4.6 version, where an IllegalArgumentException was thrown by RecyclerView during layout. The author first reviews the RecyclerView layout process and its caching strategy before analysing the stack trace.
1. Layout Process
RecyclerView’s dispatchLayout consists of three steps:
dispatchLayoutStep1 : pre‑layout – handles adapter updates, animation decisions and saves child view bounds.
dispatchLayoutStep2 : sets mState.mInPreLayout = false , then calls the LayoutManager’s onLayoutChildren . During this step the crash occurs. Example code:
private void dispatchLayoutStep2() {
// some code here
// Step 2: Run layout
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);
// some code here
}dispatchLayoutStep3 : post‑layout – saves view information, runs animations and cleans up state.
2. Caching Strategy
RecyclerView maintains several caches:
mAttachedScrap : ViewHolders still attached to RecyclerView, used for temporary reuse during layout.
mChangedScrap : Similar to mAttachedScrap but for items whose data has changed.
mCachedViews : Holds ViewHolders that have been removed from the screen (default capacity 2).
mViewCacheExtension : Custom cache logic (not implemented in K歌).
RecycledViewPool : The final cache, keyed by view type, with a default pool size of 5 per type.
During pre‑layout, RecyclerView prefers mChangedScrap , then mAttachedScrap , mCachedViews , mViewCacheExtension , and finally RecycledViewPool to obtain a ViewHolder.
3. Crash Analysis
The stack trace shows the crash happening inside ViewHolder.tryGetViewHolderForPositionByDeadline . The relevant code:
ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
// ...
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
if (holder != null) {
if (!validateViewHolderForOffsetPosition(holder)) {
if (!dryRun) {
holder.addFlags(ViewHolder.FLAG_INVALID);
if (holder.isScrap()) {
// Crash occurs here
removeDetachedView(holder.itemView, false);
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder);
}
holder = null;
} else {
fromScrapOrHiddenOrCache = true;
}
}
// ...
}The validation method checks for removed state, position bounds, type consistency, and stable IDs. In this project stable IDs are not used, so the crash is caused either by a FLAG_REMOVED holder or a type mismatch.
Further investigation revealed:
The offending ViewHolder belonged to mAttachedScrap but its isScrap() returned false at the moment of removeDetachedView , indicating that the same itemView was referenced by two different ViewHolders.
The adapter’s onCreateViewHolder creates distinct ViewHolder types for header, footer, refresh, etc. The crash was linked to the Footer ViewHolder, which should be unique.
Two FeedSubFragment instances (and thus two FeedListView RecyclerViews) were created: one visible, one invisible, due to the system recreating fragments after the app was killed in the background.
These observations formed three clues:
Holder is either FLAG_REMOVED or has a type mismatch.
Two ViewHolders point to the same Footer view.
Two fragments/recycler views exist, one invisible.
By reproducing the scenario with “Don’t keep activities” enabled, the invisible fragment persisted without data requests, causing its RecyclerView to stay invisible while the visible one processed updates. When a fake feed item was added without calling notifyXXX , the data set and RecyclerView state diverged. Subsequent layout attempts fetched a Footer ViewHolder from mAttachedScrap whose type differed from the adapter’s expectation, moved it to RecycledViewPool , and later attempted to reuse it, leading to the IllegalArgumentException.
4. Root Cause
The crash originates from fragment recreation after the app is killed, which creates an extra invisible RecyclerView. When the user publishes a new feed, the invisible RecyclerView’s data set changes without notifying the adapter, causing a mismatch between cached ViewHolder types and the adapter’s view types during the next layout pass.
5. Summary
Understanding RecyclerView’s layout steps, cache hierarchy, and the importance of keeping the adapter’s data set synchronized with the view state prevents such crashes. Developers should ensure that fragment recreation does not leave hidden RecyclerViews with stale caches and always invoke the appropriate notify... methods after data modifications.
—
QQ Music team is hiring testers and developers. Interested candidates can send their resumes to [email protected] with a note indicating the source is the public account.
Tencent Music Tech Team
Public account of Tencent Music's development team, focusing on technology sharing and communication.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.