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

简介: # 0x0 背景 经常在Crash平台上看到一个Crash,通过崩溃日志中的CurActivity字段可以知道崩溃页面是在搜索结果页,然而因为崩溃堆栈中不涉及任何业务代码,所以也很难定位原因。 ``` 04-06 10:37:43.610: ERROR/AndroidRuntime(23203): java.lang.NullPointerException 04-06 10:37:43.

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之类的操作就会有问题,后续这里进行类似操作需要足够慎重。

目录
相关文章
|
7月前
|
传感器 API
DIS
DIS
73 2
|
前端开发 Java 开发者
Dispatch 设计| 学习笔记
快速学习 Dispatch 设计。
134 0
Dispatch 设计| 学习笔记
|
Java Spring
05DispatcherServlet的初始化策略
流程回顾及后续规划 接收到spring之后回调之后的初始化策略
134 0
|
数据安全/隐私保护
WindowManager.LayoutParams Flag 含义
WindowManager.LayoutParams Flag 含义
247 0
GCD全解-dispatch_after/dispatch_time-t延迟操作
GCD全解-dispatch_after/dispatch_time-t延迟操作
345 0
|
前端开发 Java 开发者
Dispatch设计|学习笔记
快速学习Dispatch设计
Dispatch设计|学习笔记
|
机器学习/深度学习 数据建模 vr&ar
为什么都是ViewGroup的LayoutParams,也会报cannot be cast to android.view.ViewGroup$MarginLayoutParams?
为什么都是ViewGroup的LayoutParams,也会报cannot be cast to android.view.ViewGroup$MarginLayoutParams?
380 0
|
Android开发
Android事件分发详解(三)——ViewGroup的dispatchTouchEvent()源码学习
package cc.aa; import android.os.Environment; import android.view.MotionEvent; import android.
935 0
|
Android开发
Android中onInterceptTouchEvent、dispatchTouchEvent及onTouchEvent的调用顺序及内部原理
终于建了一个自己个人小站:https://huangtianyu.gitee.io,以后优先更新小站博客,欢迎进站,O(∩_∩)O~~ 在Android中需要经常对用户手势进行判断,在判断手势时需要精细的分清楚每个触摸事件以及每个View对事件的接收情况,在View,ViewGroup,Activity中都可以接收事件,在对事件进行处理时onInterceptTouchEvent、dispatchTouchEvent及onTouchEvent这三个函数的调用顺序及关系需要好好理清楚。
1161 0