博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
一个ViewGroup#dispatchDraw()中的NP分析
阅读量:6535 次
发布时间:2019-06-24

本文共 8487 字,大约阅读时间需要 28 分钟。

0x0 背景

经常在Crash平台上看到一个Crash,通过崩溃日志中的CurActivity字段可以知道崩溃页面是在搜索结果页,然而因为崩溃堆栈中不涉及任何业务代码,所以也很难定位原因。

04-06 10:37:43.610: ERROR/AndroidRuntime(23203): java.lang.NullPointerException04-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之类的操作就会有问题,后续这里进行类似操作需要足够慎重。

转载地址:http://grzdo.baihongyu.com/

你可能感兴趣的文章
java.lang.ClassFormatError
查看>>
NYOJ 简单数据结构
查看>>
结对编程,第二周作业
查看>>
避雷针 Lightning Conductor
查看>>
产品家:你的闷骚,我的产品!
查看>>
在vue里面使用jquery不生效问题
查看>>
[linux] C语言Linux系统编程进程基本概念
查看>>
MongoDB-C#驱动基本操作
查看>>
【笔记】最长递增子序列 Longest increasing subsequence(LIS)
查看>>
2000条你应知的WPF小姿势 基础篇<51-56 依赖属性>
查看>>
JAVA自学笔记13
查看>>
正则01
查看>>
Python cookbook笔记——求N个最大最小元素及lambda表达式
查看>>
制作支持UEFI PC的Server2008 R2系统安装U盘
查看>>
java注解[转]
查看>>
.NET连接MongoDB数据库实例教程
查看>>
北大ACM题库习题分类与简介(转载)
查看>>
排序集锦(rough)
查看>>
QT QML与C++混搭
查看>>
面向对象(Java中普通代码块,构造代码块,静态代码块区别及代码示例)
查看>>