浮點數引發的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. 總結

其實應該也算是一個低級問題,不過這個低級問題下面也牽扯到一些高級知識。雖然這邊文章寫得云里霧里,不過總結一句話:問題都是可以解決,解決問題的同時要深究根源并從中總結知識。

最后,如果覺得我闡述的不夠詳細的,歡迎補充。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容