浮点数引发的Canvas绘制血案

浮点数引发的Canvas绘制血案


今天在Android项目开发中遇到一个比较有趣的奔溃问题,感觉也好久没有写文章了,觉得可以跟大家分享一下的。这个问题涉及到浮点数计算、View绘制流程和机制,理清楚后发现问题其实很简单。

1.案发现场回顾

1.1 问题描述

某同学通过外部跳转直接进入WindowA(底部4个tab)的第4个tab的时候打开了WindowB,在WindowB中进行了横竖屏切换,此时返回了WindowA,切换到第1个tab后,发现app卡死之后闪退。

1.2 问题分析

1.2.1 下面简单拆解一下其实现:

  • WindowA中4个tab对应的View通过设置visibility(GONE/VISIBLE)切换。
  • WindowA针对横竖屏切换做了监听,更改了Tab1中某些View的大小和位置并触发重绘制。
  • WindowA中初始时候四个Tab都是GONE,直接进入Tab4的时候这时候只有Tab4是VISIBLE。
  • 从WindowB回来后只有点击Tab1才会触发奔溃。
  • 点击Tab1之后只做了一个处理,那就是切换其Visibility为VISIBLE。

??????为什么仅仅设置了一个View的Visibility就会导致闪退呢??????
??????为什么闪退的时候看不到有奔溃日志??????

1.2.2 部分关键代码简要回顾:

  • WindowA中Tab1针对横竖的监听处理代码如下(仅示例):
protected void onConfigurationChanged(Configuration newConfig) {
    //...
    int width = mRecycleViewPager.getWidth();//mRecycleViewPager为Tab1中的View
    float scaleRateLeft = SCALE_RATE * (1.0f - Math.abs(leftScrollX * 2f / width));
    mCurrentView.setScaleRate(scaleRateCenter);//mCurrentView为Tab1中的View
    //...
}
  • Tab1中mCurrentView.setScaleRate的实现代码如下(仅示例):
   public void setScaleRate(float scaleRate) {
       mScaleRate = scaleRate;
       invalidate();
   }
   // ...
   @Override
   protected void dispatchDraw(Canvas canvas) {
       if (mIsNeedTranslate) {
           canvas.save();
           canvas.translate(mDeltaScrollX, 0);
           if (mScaleRate != 1.0f) {
               canvas.scale(mScaleRate, mScaleRate, getWidth()/2f, getHeight()/2f);
           }
           super.dispatchDraw(canvas);
           canvas.restore();
           if (mDeltaScrollX == 0) {
               mIsNeedTranslate = false;
           }
       } else {
           super.dispatchDraw(canvas);
       }
   }

2.问题分析和定位

或许很多人可能一看代码就能很清楚明了发现问题了,不过下面还是容我分析一般。

2.1 首先,从onConfigurationChanged出发看代码:

  • mRecycleViewPager.getWidth(); //Tab1初始化为GONE,这里直接进入Tab4,此处getWidth为0。

  • leftScrollX * 2f / width; //这里除以width,0的时候抛异常?

那么,问题是否是因为getWidth()==0导致除的时候抛异常能?
答案肯定是否定的,如果除的时候抛异常,那么横竖屏切换的时候就奔溃了,而不是等到Tab1的setVisibility才奔溃。

这里就牵扯出一个关于浮点数计算的问题了:浮点数计算的时候,此处除以0,事实上得到的结果是一个正无穷或者负无穷。

所以,并不是除以0导致的异常。(其实虽然不会异常但得到一个正无穷或者负无穷的值,之后在使用的时候肯定也会有问题)

2.2 接着,从mCurrentView.setScaleRate出发看代码:

  • setScaleRate会触发invalidata
  • dispatchDraw中canvas.scale(mScaleRate, mScaleRate, getWidth()/2f, getHeight()/2f);

其实,可以发现,当mScaleRate为无穷的时候,这个语句在canvas绘制的肯定会出问题。
但是,为什么横竖屏切换的时候明明就已经触发了invalidate但是并没有卡死奔溃?

这里就牵扯出View绘制机制的问题了:当视图不可见(GONE)的时候调用invalidate是不会触发Draw的。

所以,等到Tab1切回了可视(VISIBLE)重绘的时候才会跑到dispatchDraw,这个时候canvas.scale处理一个无穷大的值,你说会不会有问题?

3. 问题总结

1.横竖屏切换的时候给View设置了一个非法数值(无穷大)。
2.切tab触发View的Draw的时候使用了这个非法数值进行了canvas绘制。

4. 问题解决方法

  1. Tab1不可见的时候不监听处理onConfigurationChanged。
  2. 当getWidth为0的时候不应该做下一步处理。
  3. dispatchDraw中对mScaleRate做非法值校验。

5. 总结

其实应该也算是一个低级问题,不过这个低级问题下面也牵扯到一些高级知识。虽然这边文章写得云里雾里,不过总结一句话:问题都是可以解决,解决问题的同时要深究根源并从中总结知识。

最后,如果觉得我阐述的不够详细的,欢迎补充。

你可能感兴趣的:(浮点数引发的Canvas绘制血案)