浮動小数点数によるCanvas描画血案

3408 ワード

浮動小数点数による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は、縦画面切り替えの傍受を行い、Tab 1の一部のViewのサイズと位置を変更し、再描画をトリガーします.
  • WindowAでは初期は4つのTabがGONEで、直接Tab 4に入ったときはTab 4だけがVISIBLEでした.
  • WindowBから帰ってきたらTab 1をクリックするだけで潰れがトリガーされます.
  • Tab 1をクリックすると、VisibilityをVISIBLEに切り替える処理が1つしか行われません.

  • ??????なぜ、1つのViewのVisibilityだけをセットするとフラッシュバックするのでしょうか???どうして闪退时に奔溃日志が见えないのですか???
    1.2.2一部のキーコードの簡単な回顧:
  • WindowAにおけるTab 1の縦横に対する傍受処理コードは以下の通りである(例のみ):
  • .
    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
        //...
    }
    
  • Tab 1の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();//Tab 1はGONEに初期化され、ここでは直接Tab 4に入り、ここでgetWidthは0である.
  • leftScrollX * 2f/width;//ここをwidthで割って、0の時に異常を投げますか?

  • では、問題はgetWidth()=0で除したときに異常エネルギーを投げたからではないでしょうか.答えは決まっているに違いないが、除いたときに異常を投げたら、縦画面の切り替え時に潰れてしまうのではなく、Tab 1のsetVisibilityを待って潰すのではない.
    ここでは浮動小数点数計算に関する問題があります.浮動小数点数計算の場合、ここで0を除いて、実際に得られた結果は正無限または負無限です.
    したがって、0で割った異常ではありません.(実は異常ではありませんが、正の無限または負の無限の値が得られ、後で使用するときにも問題があるに違いありません)
    2.2次にmCurrentView.setScaleRate出発コード:
  • setScaleRateはinvalidata
  • をトリガーします.
  • dispatchDraw中canvas.scale(mScaleRate, mScaleRate, getWidth()/2f, getHeight()/2f);

  • 実際には、mScaleRateが無限である場合、この文はcanvasで描画されると必ず問題が発生することがわかります.しかし、なぜ縦横に画面を切り替えたときにinvalidateがトリガーされたのにカードが詰まって潰れなかったのか.
    ここでは、ビューが表示されない場合にinvalidateを呼び出すとDrawはトリガーされないというViewペイントメカニズムの問題があります.
    だから、Tab 1がビジュアル(VISIBLE)を切り取って再描画するまでdispatchDrawに走って、この時canvas.scaleは無限の値を処理していますが、問題があるのではないでしょうか.
    3.質問のまとめ
    1.縦横画面切り替え時にViewに不正な数値を設定します(無限大).2.タブを切ってViewのDrawをトリガするときにこの不正な数値を使ってcanvas描画を行いました.
    4.問題解決方法
  • Tab 1が見えない場合はonConfigurationChangedを傍受しない.
  • getWidthが0の場合、次の処理を行うべきではありません.
  • dispatchDrawでmScaleRateに対して不正値チェックを行います.

  • 5.まとめ
    実は低級な問題でもあるはずですが、この低級な問題は以下にもいくつかの高級知識に関連しています.こちらの文章は霧の中に書かれていますが、問題はすべて解決できるので、問題を解決すると同時に根源を深く究明し、そこから知識をまとめなければなりません.
    最後に、私が詳しく述べていないと思ったら、補充を歓迎します.