浮点数引发的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. 问题解决方法
- Tab1不可见的时候不监听处理onConfigurationChanged。
- 当getWidth为0的时候不应该做下一步处理。
- dispatchDraw中对mScaleRate做非法值校验。
5. 总结
其实应该也算是一个低级问题,不过这个低级问题下面也牵扯到一些高级知识。虽然这边文章写得云里雾里,不过总结一句话:问题都是可以解决,解决问题的同时要深究根源并从中总结知识。
最后,如果觉得我阐述的不够详细的,欢迎补充。