Tmp detached view should be removed from RecyclerView before it can be recycled:ViewHolder

最近接手的項目友盟統計中有個RecyclerView的異常特別多,從日誌中只能看出問題出現的業務範圍,並且從代碼的提交記錄看,一年前有人調查過這個問題^^,但是這個產品和測試也沒有復現這個問題,難道是不影響客戶端的正常使用?

異常信息:java.lang.IllegalArgumentException:Tmp detached view should be removed from RecyclerView before it can be recycled:ViewHolderViewHolder,即在回收之前應該先從RecyclerView中移除。由於日誌中沒有明確到底是哪一行的問題,只能反推代碼了。日誌如下:

原文

java.lang.IllegalArgumentException: Tmp detached view should be removed from RecyclerView
 before it can be recycled:ViewHolder{4a13b78 position=1 id=-1, oldPos=-1, pLpos:-1 
 tmpDetached no parent} android.support.v7.widget.RecyclerView{570c467 VFED.
 .... ........ 0,0-1080,869 #7f090274 app:id/messages}, adapter:com.
 .fragment.adapter. Adapter@917c99d, layout:.LinearLayoutManager@afcde12, context:com.Activity@dd60286
    //代碼1
    at android.support.v7.widget.RecyclerView$Recycler.b(RecyclerView.java:6059)
    //代碼2
    at android.support.v7.widget.RecyclerView.removeAnimatingView(RecyclerView.java:1375)
    //代碼3
    at android.support.v7.widget.RecyclerView$ItemAnimatorRestoreListener.a(RecyclerView.java:12242)
    //代碼4
    at android.support.v7.widget.RecyclerView$ItemAnimator.f(RecyclerView.java:12742)
    //代碼5
    at android.support.v7.widget.SimpleItemAnimator.l(SimpleItemAnimator.java:303)
    // 代碼6
    at android.support.v7.widget.DefaultItemAnimator$6.onAnimationEnd(DefaultItemAnimator.java:311)
    at android.view.ViewPropertyAnimator$AnimatorEventListener.onAnimationEnd(ViewPropertyAnimator.java:1114)
    at android.animation.ValueAnimator.endAnimation(ValueAnimator.java:1239)
    at android.animation.ValueAnimator$AnimationHandler.doAnimationFrame(ValueAnimator.java:766)
    at android.animation.ValueAnimator$AnimationHandler$1.run(ValueAnimator.java:801)
    at android.view.Choreographer$CallbackRecord.run(Choreographer.java:862)
    at android.view.Choreographer.doCallbacks(Choreographer.java:674)
    at android.view.Choreographer.doFrame(Choreographer.java:607)
    at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:848)
    at android.os.Handler.handleCallback(Handler.java:739)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:179)
    at android.app.ActivityThread.main(ActivityThread.java:5769)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:786)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:676)

首先看一下:異常信息時從哪裏出現的
代碼1:類RecyclerView

void recycleViewHolderInternal(ViewHolder holder) {
          。。。。。。

            if (holder.isTmpDetached()) {
                throw new IllegalArgumentException("Tmp detached view should be removed "
                        + "from RecyclerView before it can be recycled: " + holder
                        + exceptionLabel());
            }
            。。。。。

代碼2:類RecyclerView

boolean removeAnimatingView(View view) {
        startInterceptRequestLayout();
        final boolean removed = mChildHelper.removeViewIfHidden(view);
        if (removed) {
            final ViewHolder viewHolder = getChildViewHolderInt(view);
            mRecycler.unscrapView(viewHolder);
            //代碼1
            mRecycler.recycleViewHolderInternal(viewHolder);
            if (DEBUG) {
                Log.d(TAG, "after removing animated view: " + view + ", " + this);
            }
        }
        // only clear request eaten flag if we removed the view.
        stopInterceptRequestLayout(!removed);
        return removed;
    }

代碼3:類RecyclerView

  private class ItemAnimatorRestoreListener implements ItemAnimator.ItemAnimatorListener {

        ItemAnimatorRestoreListener() {
        }

        @Override
        public void onAnimationFinished(ViewHolder item) {
            item.setIsRecyclable(true);
            if (item.mShadowedHolder != null && item.mShadowingHolder == null) { // old vh
                item.mShadowedHolder = null;
            }
            // always null this because an OldViewHolder can never become NewViewHolder w/o being
            // recycled.
            item.mShadowingHolder = null;
            if (!item.shouldBeKeptAsChild()) {
            //代碼2
                if (!removeAnimatingView(item.itemView) && item.isTmpDetached()) {
                    removeDetachedView(item.itemView, false);
                }
            }
        }
    }

代碼4:在RecyclerView.ItemAnimator中

  public final void dispatchAnimationFinished(ViewHolder viewHolder) {
            onAnimationFinished(viewHolder);
            if (mListener != null) {
                //代碼3
                mListener.onAnimationFinished(viewHolder);
            }
        }

代碼5:SimpleItemAnimator extends RecyclerView.ItemAnimator

public final void dispatchAddFinished(ViewHolder item) {
        onMoveFinished(item);
        //代碼4
        dispatchAnimationFinished(item);
    }

代碼6:類SimpleItemAnimator

 void animateAddImpl(final ViewHolder holder) {
        final View view = holder.itemView;
        final ViewPropertyAnimator animation = view.animate();
        mAddAnimations.add(holder);
        animation.alpha(1).setDuration(getAddDuration())
                .setListener(new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationStart(Animator animator) {
                        dispatchAddStarting(holder);
                    }

                    @Override
                    public void onAnimationCancel(Animator animator) {
                        view.setAlpha(1);
                    }

                    @Override
                    public void onAnimationEnd(Animator animator) {
                        animation.setListener(null);
                        //代碼5
                        dispatchAddFinished(holder);
                        mAddAnimations.remove(holder);
                        dispatchFinishedWhenDone();
                    }
                }).start();
    }

代碼7:類RecyclerView.ItemAnimator

 public void runPendingAnimations() {
        boolean removalsPending = !mPendingRemovals.isEmpty();
        boolean movesPending = !mPendingMoves.isEmpty();
        boolean changesPending = !mPendingChanges.isEmpty();
        boolean additionsPending = !mPendingAdditions.isEmpty();
        if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
            // nothing to animate
            return;
        }
        。。。。。。。。
        // Next, add stuff
        if (additionsPending) {
            final ArrayList<ViewHolder> additions = new ArrayList<>();
            additions.addAll(mPendingAdditions);
            mAdditionsList.add(additions);
            mPendingAdditions.clear();
            Runnable adder = new Runnable() {
                @Override
                public void run() {
                    for (ViewHolder holder : additions) {
                        //代碼6
                        animateAddImpl(holder);
                    }
                    additions.clear();
                    mAdditionsList.remove(additions);
                }
            };
            if (removalsPending || movesPending || changesPending) {
                long removeDuration = removalsPending ? getRemoveDuration() : 0;
                long moveDuration = movesPending ? getMoveDuration() : 0;
                long changeDuration = changesPending ? getChangeDuration() : 0;
                long totalDelay = removeDuration + Math.max(moveDuration, changeDuration);
                View view = additions.get(0).itemView;
                ViewCompat.postOnAnimationDelayed(view, adder, totalDelay);
            } else {
                adder.run();
            }
        }
    }

代碼8:類RecyclerView

 private Runnable mItemAnimatorRunner = new Runnable() {
        @Override
        public void run() {
            if (mItemAnimator != null) {
               //代碼7
                mItemAnimator.runPendingAnimations();
            }
            mPostedAnimatorRunner = false;
        }
    };

代碼9:類RecyclerView,。在下一幀執行Item的動畫

    /**
     * Post a runnable to the next frame to run pending item animations. Only the first such
     * request will be posted, governed by the mPostedAnimatorRunner flag.
     */
    void postAnimationRunner() {
        if (!mPostedAnimatorRunner && mIsAttached) {
        //代碼8
            ViewCompat.postOnAnimation(this, mItemAnimatorRunner);
            mPostedAnimatorRunner = true;
        }
    }

從異常日誌信息來看:代碼7,8,9好像沒有體現,而日誌中體現的問題就是在Item執行動畫是產生的,
因此此異常的解決方案就是取消Item的動畫執行,從代碼8中只要設置mItemAnimator =null就可以了,RecyclerView也提供的Api:setItemAnimator()

 /**
     * Sets the {@link ItemAnimator} that will handle animations involving changes
     * to the items in this RecyclerView. By default, RecyclerView instantiates and
     * uses an instance of {@link DefaultItemAnimator}. Whether item animations are
     * enabled for the RecyclerView depends on the ItemAnimator and whether
     * the LayoutManager {@link LayoutManager#supportsPredictiveItemAnimations()
     * supports item animations}.
     *
     * @param animator The ItemAnimator being set. If null, no animations will occur
     * when changes occur to the items in this RecyclerView.
     */
    public void setItemAnimator(ItemAnimator animator) {
        if (mItemAnimator != null) {
            mItemAnimator.endAnimations();
            mItemAnimator.setListener(null);
        }
        mItemAnimator = animator;
        if (mItemAnimator != null) {
            mItemAnimator.setListener(mItemAnimatorListener);
        }
    }

另外附上: ItemAnimator作觸發於以下三種事件
I
notifyItemInserted(int position)方法
notifyItemRemoved(int position) 方法
notifyItemChanged(int position) 方法
注意:notifyDataSetChanged(),會觸發列表的重繪,並不會出現任何動畫效果

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章