Android记一次匪夷所思的debug经历——animate的同时gone

我和另一个模块做了交互,用反射实现的依赖注入

1.这过程中由于模块不同,编译隔离,我只能通过debug来发现问题

2.我们对方案产生了分歧,我进入我们的完成页,需要调接口,然后才会去模块交互,另一模块又会调接口,同时展示loading。但是在我request的时候没有loading,或者我可以自己loading,但是需要和他们的loading统一,这维护不便,最终我在讨论的时候,提出了打2次信号的方式,第一次进页面,直接远程调用,展示loading,这个loading吞掉了我和别人模块的两次请求架加在一起的时间。我为此还很高兴,很好地符合了产品的口味,感觉自己又是技术又是产品了

没成想,最后大家修改了需求,远程request的数据基础,不需要基于我们request的结果,直接就可以同时请求。

3.这不是最难受的,最难受的是出现了个匪夷所思的bug。他们那个模块一开始是呼吸动画的loading,然后数据加载出来后,呼吸loading没有消失。但是我跑到他们的小组,发现他们本地调一点事情都没有。所以我和那位老哥苦思冥想啊?线程问题?当然不是,已经切换到主线程了;反射问题?当然不是;机型问题?我这里所有机型都不行啊。反正想了许多办法。

难道是动画的时候,gone无效?可是他们那里为什么可以?

要不改一下,cancel一下试一试?然后他们模块打个class bundle上传我们再跑一次?成本太大了吧?万一不成,真的血崩了!所以说模块交互一旦出现bug真是太恐怖了,调试是非常非常非常之恐怖的~!

最后我回去思考了30分钟左右,想到,他的view的添加是这样的

new Callback {

void call(Object o) {

remove all

add o

}

}

我这里是

new Callback {

void call(Object o) {

if child == 0

add o

}

}

这是我们唯一的区别,那问题肯定就出在这里。

后来沟通了下 很快解决了


自己后来为了探究原理做了测试

oncreate中

View v = findViewById(R.id.v);
v.setAnimation(BreathAnimalFactory.createBreathAnimal(null));
v.setVisibility(View.GONE);

另一种写法

final View v = findViewById(R.id.v);
v.setAnimation(BreathAnimalFactory.createBreathAnimal(null));
getWindow().getDecorView().post(new Runnable() {
    @Override
    public void run() {
        v.setVisibility(View.GONE);
    }
});

log了下,v是gone了,但是宽高还在,就证明没有走到measure或者measure操作被某些标记位阻止了真正的measure,绕过了真正要measure的代码块

想了下,没有走到measure的可能性更大。可能是一直在invalidate,把request layout给无限延后了

final View v = findViewById(R.id.v);
v.setAnimation(BreathAnimalFactory.createBreathAnimal(null));
getWindow().getDecorView().post(new Runnable() {
    @Override
    public void run() {
        v.setVisibility(View.GONE);
        v.post(new Runnable() {
            @Override
            public void run() {
                v.setAnimation(null);
            }
        });
    }
});

这样写,就又没了

防止setAnimation可能引起重绘,反射清除动画字段

final View v = findViewById(R.id.v);
v.setAnimation(BreathAnimalFactory.createBreathAnimal(null));
getWindow().getDecorView().post(new Runnable() {
    @Override
    public void run() {
        v.setVisibility(View.GONE);
        v.post(new Runnable() {
            @Override
            public void run() {
                Field field = null;
                try {
                    field = View.class.getDeclaredField("mCurrentAnimation");
                    field.setAccessible(true);
                    field.set(v, null);
                } catch (NoSuchFieldException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        });
    }
});

果然也没了,所以绘制的时候,会检测mCurrentAnimation这个字段


最后了解到了Choreographer

这里讨论VSync mode,VSync大概16ms打一次信号,Choreographer中维护3条队列,事件,动画,正常绘制,post_draw,每16s这个代码块都会被执行

doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);

mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);

那么我们的动画信号,在哪个地方不断地打给Choreographer呢?

if (more && hardwareAcceleratedCanvas) {
    if (a.hasAlpha() && (mPrivateFlags & PFLAG_ALPHA_SET) == PFLAG_ALPHA_SET) {
        // alpha animations should cause the child to recreate its display list
        invalidate(true);
    }
}

这个是view的某个draw方法,被父类调用,用来绘制child自身。这里的代码的意思是,如果是alpha动画,那就调用invalidate。

而这个代码之前的工作就是applyTransformation-进行计算值,然后绘制。

所以Animation类之所以可以一直运作,是因为他在不断地invalidate。


先考虑下这个问题,view一开始就gone或者后期转变为gone的时候,为什么就没有宽没有高了?

看一看view group的方法

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

显然,如果是GONE,child就不会被measure

1.一开始就是gone,那么自然不会被测绘

2.前期visible,后期设置为gone,那么会request layout,强行重新进行measure、layout。draw是自然进入的。然后在measure、layout中会分别设置measure height/width,height/width。当然了,measure、layout gone掉的view不会进入,所以可以猜到在这些步骤之前,有一个reset的操作

关键在于找到这个reset的操作

然后我发现被我自己坑了,因为我翻遍了源码,也只找到了

if(child visibility!= GONE) {

child.measure

}

那岂不是在第一次未gone,后来gone的情况下,measure height/width,height/width都是脏数据了?

我打印了日志发现,果然真的都是脏数据。


剩下最后一条线索,mCurrentAnimation。

不管是set animation(null)

还是反射改掉这个标记位

都会成功gone掉

所以需要找到这个判断这个标记位的地方,或者这个标记位改变了父的属性,的判断的地方


最后发现在view group的dispatch draw里

if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
        transientChild.getAnimation() != null) {
    more |= drawChild(canvas, transientChild, drawingTime);
}

可见或者有动画,view的上一层的父容器view group会允许draw这个孩子。


这样一来都理清了。dispatch draw是view group用来绘制孩子的方法。

虽然我们gone了,但是有动画,所以自身这个处在动画中的view的绘制依然被调用了,所以依然是可见的!依然被绘制出来了!这就是无法gone掉的秘密。

同时我之前还有疑虑,到底是在哪个地方clear原来所有的像素点呢?我只能猜想,在child确定了draw之后,正式draw之前,child区域的像素点会被clear,或者统一计算和父容器重合的地方clear。这就是gone的实现原理。上面这个判断就是动画的时候gone不掉的原因。

之所以动画的时候gone不掉,还有measure、layout的松懈。measure根本不处理gone情况,导致后来才设置的gone,会留下脏的measured width/height数据。layout也差不多,直接不处理gone,留下脏的width/height数据。

至于我上面瞎猜的Choreographer、sync barrier更是无稽之谈。因为Animation这个类在Choreographer对应的callback是traversal而不是animate;sync barrier很快就释放了,几乎无影响;postcallback里invalidate、requestLayout post的是同一个callback traversal。

猜你喜欢

转载自blog.csdn.net/qq_36523667/article/details/81024453