当前位置: 首页 > news >正文

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

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

相关文章:

  • LINUX命令 cp: omitting directory 出现的问题解决办法
  • 枚举类的简单应用
  • 手把手教你启用Win10的Linux子系统(超详细)
  • [转载]C# Double toString保留小数点方法
  • 自动化部署打破混乱之墙 助力开发、运维、测试协同作战
  • spring restTemplate 上传数据流/字节数组
  • Windows下leapmotion中touchless的使用
  • Session丢失的问题!(转)
  • 架构探险笔记4-使框架具备AOP特性(上)
  • QT 字符串相等间距字符间增加字符
  • 第六篇:面向对象
  • LinuxShell 首字母大写
  • 柯里化/偏函数/Curring用法
  • 兄弟连区块链教程区块链背后的信息安全2DES、3DES加密算法原理二
  • [leetcode]_Symmetric Tree
  • __proto__ 和 prototype的关系
  • flutter的key在widget list的作用以及必要性
  • IDEA常用插件整理
  • JavaSE小实践1:Java爬取斗图网站的所有表情包
  • JAVA并发编程--1.基础概念
  • PaddlePaddle-GitHub的正确打开姿势
  • UEditor初始化失败(实例已存在,但视图未渲染出来,单页化)
  • 不用申请服务号就可以开发微信支付/支付宝/QQ钱包支付!附:直接可用的代码+demo...
  • 程序员该如何有效的找工作?
  • 从地狱到天堂,Node 回调向 async/await 转变
  • 给新手的新浪微博 SDK 集成教程【一】
  • 配置 PM2 实现代码自动发布
  • 使用Maven插件构建SpringBoot项目,生成Docker镜像push到DockerHub上
  • 学习笔记:对象,原型和继承(1)
  • LevelDB 入门 —— 全面了解 LevelDB 的功能特性
  • puppet连载22:define用法
  • ​业务双活的数据切换思路设计(下)
  • #Z0458. 树的中心2
  • $(selector).each()和$.each()的区别
  • (1)(1.13) SiK无线电高级配置(六)
  • (3)Dubbo启动时qos-server can not bind localhost22222错误解决
  • (39)STM32——FLASH闪存
  • (二)fiber的基本认识
  • (牛客腾讯思维编程题)编码编码分组打印下标题目分析
  • (太强大了) - Linux 性能监控、测试、优化工具
  • .net php 通信,flash与asp/php/asp.net通信的方法
  • .net6使用Sejil可视化日志
  • .NET框架
  • .net利用SQLBulkCopy进行数据库之间的大批量数据传递
  • .net专家(高海东的专栏)
  • /ThinkPHP/Library/Think/Storage/Driver/File.class.php  LINE: 48
  • @Query中countQuery的介绍
  • @RequestParam,@RequestBody和@PathVariable 区别
  • [2018][note]用于超快偏振开关和动态光束分裂的all-optical有源THz超表——
  • [23] GaussianAvatars: Photorealistic Head Avatars with Rigged 3D Gaussians
  • [delphi]保证程序只运行一个实例
  • [emuch.net]MatrixComputations(7-12)
  • [IE9] GPU硬件加速到底是实用创新还是噱头
  • [J2ME]如何替换Google Map静态地图自带的Marker
  • [Java][Android][Process] 暴力的服务能够解决一切,暴力的方式运行命令行语句