浮點數(shù)引發(fā)的Canvas繪制血案
今天在Android項目開發(fā)中遇到一個比較有趣的奔潰問題溪猿,感覺也好久沒有寫文章了,覺得可以跟大家分享一下的穴吹。這個問題涉及到浮點數(shù)計算盆昙、View繪制流程和機(jī)制羽历,理清楚后發(fā)現(xiàn)問題其實很簡單。
1.案發(fā)現(xiàn)場回顧
1.1 問題描述
某同學(xué)通過外部跳轉(zhuǎn)直接進(jìn)入WindowA(底部4個tab)的第4個tab的時候打開了WindowB淡喜,在WindowB中進(jìn)行了橫豎屏切換秕磷,此時返回了WindowA,切換到第1個tab后炼团,發(fā)現(xiàn)app卡死之后閃退澎嚣。
1.2 問題分析
1.2.1 下面簡單拆解一下其實現(xiàn):
- WindowA中4個tab對應(yīng)的View通過設(shè)置visibility(GONE/VISIBLE)切換。
- WindowA針對橫豎屏切換做了監(jiān)聽瘟芝,更改了Tab1中某些View的大小和位置并觸發(fā)重繪制易桃。
- WindowA中初始時候四個Tab都是GONE,直接進(jìn)入Tab4的時候這時候只有Tab4是VISIBLE锌俱。
- 從WindowB回來后只有點擊Tab1才會觸發(fā)奔潰晤郑。
- 點擊Tab1之后只做了一個處理,那就是切換其Visibility為VISIBLE嚼鹉。
贩汉??锚赤?匹舞??线脚?為什么僅僅設(shè)置了一個View的Visibility就會導(dǎo)致閃退呢赐稽??浑侥?姊舵??寓落?
括丁??伶选?史飞??仰税?為什么閃退的時候看不到有奔潰日志构资??陨簇?吐绵???
1.2.2 部分關(guān)鍵代碼簡要回顧:
- WindowA中Tab1針對橫豎的監(jiān)聽處理代碼如下(僅示例):
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的實現(xiàn)代碼如下(僅示例):
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.問題分析和定位
或許很多人可能一看代碼就能很清楚明了發(fā)現(xiàn)問題了己单,不過下面還是容我分析一般唉窃。
2.1 首先,從onConfigurationChanged出發(fā)看代碼:
mRecycleViewPager.getWidth(); //Tab1初始化為GONE荷鼠,這里直接進(jìn)入Tab4句携,此處getWidth為0榔幸。
leftScrollX * 2f / width; //這里除以width允乐,0的時候拋異常?
那么削咆,問題是否是因為getWidth()==0導(dǎo)致除的時候拋異常能牍疏?
答案肯定是否定的,如果除的時候拋異常拨齐,那么橫豎屏切換的時候就奔潰了鳞陨,而不是等到Tab1的setVisibility才奔潰。
這里就牽扯出一個關(guān)于浮點數(shù)計算的問題了:浮點數(shù)計算的時候瞻惋,此處除以0厦滤,事實上得到的結(jié)果是一個正無窮或者負(fù)無窮。
所以歼狼,并不是除以0導(dǎo)致的異常掏导。(其實雖然不會異常但得到一個正無窮或者負(fù)無窮的值,之后在使用的時候肯定也會有問題)
2.2 接著羽峰,從mCurrentView.setScaleRate出發(fā)看代碼:
- setScaleRate會觸發(fā)invalidata
- dispatchDraw中canvas.scale(mScaleRate, mScaleRate, getWidth()/2f, getHeight()/2f);
其實趟咆,可以發(fā)現(xiàn),當(dāng)mScaleRate為無窮的時候梅屉,這個語句在canvas繪制的肯定會出問題值纱。
但是,為什么橫豎屏切換的時候明明就已經(jīng)觸發(fā)了invalidate但是并沒有卡死奔潰坯汤?
這里就牽扯出View繪制機(jī)制的問題了:當(dāng)視圖不可見(GONE)的時候調(diào)用invalidate是不會觸發(fā)Draw的虐唠。
所以,等到Tab1切回了可視(VISIBLE)重繪的時候才會跑到dispatchDraw惰聂,這個時候canvas.scale處理一個無窮大的值疆偿,你說會不會有問題?
3. 問題總結(jié)
1.橫豎屏切換的時候給View設(shè)置了一個非法數(shù)值(無窮大)庶近。
2.切tab觸發(fā)View的Draw的時候使用了這個非法數(shù)值進(jìn)行了canvas繪制翁脆。
4. 問題解決方法
- Tab1不可見的時候不監(jiān)聽處理onConfigurationChanged。
- 當(dāng)getWidth為0的時候不應(yīng)該做下一步處理鼻种。
- dispatchDraw中對mScaleRate做非法值校驗反番。
5. 總結(jié)
其實應(yīng)該也算是一個低級問題,不過這個低級問題下面也牽扯到一些高級知識。雖然這邊文章寫得云里霧里罢缸,不過總結(jié)一句話:問題都是可以解決篙贸,解決問題的同時要深究根源并從中總結(jié)知識。
最后枫疆,如果覺得我闡述的不夠詳細(xì)的爵川,歡迎補(bǔ)充。