一個ViewGroup#dispatchDraw()中的NP分析

zhang_sl發表於2018-11-05

0x0 背景

經常在Crash平臺上看到一個Crash,通過崩潰日誌中的CurActivity欄位可以知道崩潰頁面是在搜尋結果頁,然而因為崩潰堆疊中不涉及任何業務程式碼,所以也很難定位原因。

04-06 10:37:43.610: ERROR/AndroidRuntime(23203): java.lang.NullPointerException
04-06 10:37:43.610: ERROR/AndroidRuntime(23203):     at android.view.ViewGroup.dispatchDraw(ViewGroup.java:2122)
04-06 10:37:43.610: ERROR/AndroidRuntime(23203):     at android.view.ViewGroup.drawChild(ViewGroup.java:2506)
04-06 10:37:43.610: ERROR/AndroidRuntime(23203):     at android.view.ViewGroup.dispatchDraw(ViewGroup.java:2123)
04-06 10:37:43.610: ERROR/AndroidRuntime(23203):     at android.view.ViewGroup.drawChild(ViewGroup.java:2506)
04-06 10:37:43.610: ERROR/AndroidRuntime(23203):     at android.view.ViewGroup.dispatchDraw(ViewGroup.java:2123)
04-06 10:37:43.610: ERROR/AndroidRuntime(23203):     at android.view.ViewGroup.drawChild(ViewGroup.java:2506)
04-06 10:37:43.610: ERROR/AndroidRuntime(23203):     at android.view.ViewGroup.dispatchDraw(ViewGroup.java:2123)
04-06 10:37:43.610: ERROR/AndroidRuntime(23203):     at android.view.ViewGroup.drawChild(ViewGroup.java:2506)
04-06 10:37:43.610: ERROR/AndroidRuntime(23203):     at android.view.ViewGroup.dispatchDraw(ViewGroup.java:2123)
04-06 10:37:43.610: ERROR/AndroidRuntime(23203):     at android.view.View.draw(View.java:9032)
04-06 10:37:43.610: ERROR/AndroidRuntime(23203):     at android.widget.FrameLayout.draw(FrameLayout.java:419)
04-06 10:37:43.610: ERROR/AndroidRuntime(23203):     at com.android.internal.policy.impl.PhoneWindow$DecorView.draw(PhoneWindow.java:1910)
04-06 10:37:43.610: ERROR/AndroidRuntime(23203):     at android.view.ViewRoot.draw(ViewRoot.java:1608)
04-06 10:37:43.610: ERROR/AndroidRuntime(23203):     at android.view.ViewRoot.performTraversals(ViewRoot.java:1329)
04-06 10:37:43.610: ERROR/AndroidRuntime(23203):     at android.view.ViewRoot.handleMessage(ViewRoot.java:1944)
04-06 10:37:43.610: ERROR/AndroidRuntime(23203):     at android.os.Handler.dispatchMessage(Handler.java:99)
04-06 10:37:43.610: ERROR/AndroidRuntime(23203):     at android.os.Looper.loop(Looper.java:126)
04-06 10:37:43.610: ERROR/AndroidRuntime(23203):     at android.app.ActivityThread.main(ActivityThread.java:3997)
04-06 10:37:43.610: ERROR/AndroidRuntime(23203):     at java.lang.reflect.Method.invokeNative(Native Method)
04-06 10:37:43.610: ERROR/AndroidRuntime(23203):     at java.lang.reflect.Method.invoke(Method.java:491)
04-06 10:37:43.610: ERROR/AndroidRuntime(23203):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:841)
04-06 10:37:43.610: ERROR/AndroidRuntime(23203):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:599)
04-06 10:37:43.610: ERROR/AndroidRuntime(23203):     at dalvik.system.NativeStart.main(Native Method)

0x1 線索

在stackoverflow上有人提到在Animation的onAnimationEnd回撥中,刪除view會引起這個問題,但是具體原因沒有講。
一個偶然機會,在搜尋結果頁連續快速點選PK時,重現了該問題。檢視該出程式碼,果然存在Animation的onAnimationEnd()回撥中刪除view的情況。

0x2 原因

一句話,Animation的onAnimationEnd()是在draw()函式中同步呼叫的,在draw的時候刪除view,相當於在for迴圈遍歷所有子view的過程中將其中一個元素置空,導致遍歷到時產生NP。
Android具體的動畫執行流程如下:

1. 呼叫View#startAnimation()開始動畫執行,此時只是將Animation物件設定進去,並呼叫invalidate()觸發繪製更新
/**
     * Start the specified animation now.
     *
     * @param animation the animation to start now
     */
    public void startAnimation(Animation animation) {
        animation.setStartTime(Animation.START_ON_FIRST_FRAME);
        //設定Animation物件
        setAnimation(animation);
        invalidateParentCaches();
        //觸發繪製更新
        invalidate(true);
    }
2. 繪製流程從root view的draw()方法呼叫到ViewGroup#dispatchView(),在這個函式中又會遍歷它的子view,分別呼叫他們的draw()函式
    @Override
    protected void dispatchDraw(Canvas canvas) {
        boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
        final int childrenCount = mChildrenCount;
        final View[] children = mChildren;
        int flags = mGroupFlags;

        if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) {
            final boolean buildCache = !isHardwareAccelerated();
            //遍歷子view
            for (int i = 0; i < childrenCount; i++) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
                    final LayoutParams params = child.getLayoutParams();
                    attachLayoutAnimationParameters(child, params, i, childrenCount);
                    bindLayoutAnimation(child);
                }
            }
        ...
        }
        ...
    }
3. 正在執行動畫的view,在其View#draw()中獲取Animation資訊,並影響本次繪製

    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        ...
        final Animation a = getAnimation();
        if (a != null) {
            //更新動畫資訊
            more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
            concatMatrix = a.willChangeTransformationMatrix();
            if (concatMatrix) {
                mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
            }
            transformToApply = parent.getChildTransformation();
        } else {
            if ((mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_TRANSFORM) != 0) {
                // No longer animating: clear out old animation matrix
                mRenderNode.setAnimationMatrix(null);
                mPrivateFlags3 &= ~PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
            }
            if (!drawingWithRenderNode
                    && (parentFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {
                final Transformation t = parent.getChildTransformation();
                final boolean hasTransform = parent.getChildStaticTransformation(this, t);
                if (hasTransform) {
                    final int transformType = t.getTransformationType();
                    transformToApply = transformType != Transformation.TYPE_IDENTITY ? t : null;
                    concatMatrix = (transformType & Transformation.TYPE_MATRIX) != 0;
                }
            }
        }

        ...
    }


    private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime,
            Animation a, boolean scalingRequired) {
        Transformation invalidationTransform;
        final int flags = parent.mGroupFlags;
        final boolean initialized = a.isInitialized();
        if (!initialized) {
            a.initialize(mRight - mLeft, mBottom - mTop, parent.getWidth(), parent.getHeight());
            a.initializeInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop);
            if (mAttachInfo != null) a.setListenerHandler(mAttachInfo.mHandler);
            onAnimationStart();
        }


        final Transformation t = parent.getChildTransformation();

        //這裡會獲取Animation的Transformation資訊
        boolean more = a.getTransformation(drawingTime, t, 1f);
        ...
        return more;
    }
4. 動畫結束時Animation#getTransformation()函式內部會直接同步回撥onAnimationEnd()
public boolean getTransformation(long currentTime, Transformation outTransformation) {
        if (mStartTime == -1) {
            mStartTime = currentTime;
        }

        final long startOffset = getStartOffset();
        final long duration = mDuration;
        float normalizedTime;
        if (duration != 0) {
            normalizedTime = ((float) (currentTime - (mStartTime + startOffset))) /
                    (float) duration;
        } else {
            // time is a step-change with a zero duration
            normalizedTime = currentTime < mStartTime ? 0.0f : 1.0f;
        }

        final boolean expired = normalizedTime >= 1.0f || isCanceled();

        if (expired) {
            if (mRepeatCount == mRepeated || isCanceled()) {
                if (!mEnded) {
                    mEnded = true;
                    guard.close();

                    //釋出AnimationEnd資訊
                    fireAnimationEnd();
                }
            } else {
                if (mRepeatCount > 0) {
                    mRepeated++;
                }

                if (mRepeatMode == REVERSE) {
                    mCycleFlip = !mCycleFlip;
                }

                mStartTime = -1;
                mMore = true;

                fireAnimationRepeat();
            }
        }
        ...
        return mMore;
    }


//這裡是同步呼叫注入的Listener的onAnimationEnd()函式
private void fireAnimationEnd() {
        if (mListener != null) {
            if (mListenerHandler == null) mListener.onAnimationEnd(this);
            else mListenerHandler.postAtFrontOfQueue(mOnEnd);
        }
    }

至此,如果在onAnimationEnd()同步執行removeView()操作,那是會有引發空指標的風險的。

0x3 後記

就這個問題而言把removeView的操作自己放到一個Handler中非同步化,問題就能解決了。在Animation設定listener,並監聽其開始和結束的資訊,很容易讓人有一種這是非同步回撥的錯覺,殊不知這是一個同步回撥,如果在這裡同步的執行類似刪除view之類的操作就會有問題,後續這裡進行類似操作需要足夠慎重。


相關文章