浮點數引發的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. 總結
其實應該也算是一個低級問題,不過這個低級問題下面也牽扯到一些高級知識。雖然這邊文章寫得云里霧里,不過總結一句話:問題都是可以解決,解決問題的同時要深究根源并從中總結知識。
最后,如果覺得我闡述的不夠詳細的,歡迎補充。