1. 程式人生 > >3.2裝置旋轉時資料丟失解決方式之一

3.2裝置旋轉時資料丟失解決方式之一

啟動GeoQuiz應用,單擊NEXT按鈕顯示第
二道地理知識問題,然後旋轉裝置。發現又會回到第一道題,所以現在需要解決這個問題。
裝置旋轉時生命週期變化
在這裡插入圖片描述
裝置旋轉時,系統會銷燬當前Activity例項,然後建立一個新的Activity例項。再次旋轉裝置,又一次見證這個銷燬與再建立的過程。
這就是問題所在。每次旋轉裝置,當前Activity例項會完全銷燬,例項中的資料就會被被抹掉。旋轉後,Android重新建立了Activity新例項,一切重頭再來。

裝置配置與備選資源
旋轉裝置會改變裝置配置(device configuration)。裝置配置實際是一系列特徵組合,用來描述裝置當前狀態。這些特徵有:螢幕方向、螢幕畫素密度、螢幕尺寸、鍵盤型別、底座模式以及
語言等。通常,為匹配不同的裝置配置,應用會提供不同的備選資源。為適應不同解析度的螢幕,裝置的螢幕畫素密度是個固定的裝置配置,無法在執行時發生改變。然而,螢幕方向等特徵,可以在應用執行時改變。在執行時配置變更發生時,可能會有更合適的資源來匹配新
的裝置配置。於是,Android銷燬當前activity,為新配置尋找最佳資源,然後建立新例項使用這些資源。只要裝置旋轉至水平方位,Android就會自動發現並使用它。

儲存資料以應對裝置旋轉

protected void onSaveInstanceState(Bundle outState)

該方法通常在onStop()方法之前由系統呼叫,除非使用者按後退鍵。(記住,按後退鍵就是告訴Android,activity用完了。隨後,該activity就完全從記憶體中被抹掉,自然,也就沒有必要為重建儲存資料了。)
方法onSaveInstanceState(Bundle)的預設實現要求所有activity檢視將自身狀態資料儲存在Bundle物件中。Bundle是儲存字串鍵與限定型別值之間對映關係(鍵值對)的一種結構。可通過覆蓋onSaveInstanceState(Bundle)方法,將一些資料儲存在bundle中,然後在onCreate(Bundle)方法中取回這些資料。

關鍵程式碼:

  private static final String KEY_INDEX = "index";
  
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_quiz);
        if(savedInstanceState!=null) {
            mCurrentIndex=savedInstanceState.getInt(KEY_INDEX,0);
        }
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putInt(KEY_INDEX,mCurrentIndex);
    }

詳細程式碼

public class QuizActivity extends AppCompatActivity {

    private static final String TAG = "QuizActivity";
    private static final String KEY_INDEX = "index";
    private Button mTrueButton;
    private Button mFalseButton;

    private  Question[] mQuestionBank=new Question[]{
            new  Question(R.string.question_australia,true),
            new  Question(R.string.question_oceans,true),
            new  Question(R.string.question_mideast,false),
            new  Question(R.string.question_africa,false),
            new  Question(R.string.question_americas,true),
            new  Question(R.string.question_asia,true)
    };
    private TextView mQuestionTextView;
    private  int mCurrentIndex;
    private Button mNextButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_quiz);
        Log.d(TAG, "onCreate(Bundle) called");
        if(savedInstanceState!=null) {
            mCurrentIndex=savedInstanceState.getInt(KEY_INDEX,0);
        }
        initView();
        initEvent();
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putInt(KEY_INDEX,mCurrentIndex);
    }

    private void initView() {
        //例項化控制元件
        mTrueButton = findViewById(R.id.true_button);
        mFalseButton = findViewById(R.id.false_button);
        mNextButton = findViewById(R.id.next_button);
        mQuestionTextView = findViewById(R.id.question_text_view);

        mQuestionTextView.setText(mQuestionBank[mCurrentIndex].getTextResId());
    }

    private void initEvent() {
        //設定匿名內部類監聽器
        mTrueButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                checkAnswer(true);
            }
        });

        mFalseButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                checkAnswer(false);
            }
        });

        mNextButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                mCurrentIndex=(mCurrentIndex+1)%mQuestionBank.length;
                mQuestionTextView.setText(mQuestionBank[mCurrentIndex].getTextResId());
            }
        });
        mQuestionTextView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                mCurrentIndex=(mCurrentIndex+1)%mQuestionBank.length;
                mQuestionTextView.setText(mQuestionBank[mCurrentIndex].getTextResId());
            }
        });

    }

    /**
     * 將使用者輸入的結果與正確結果進行對比
     * @param userPressedTrue 使用者輸入的答案
     */
    public void checkAnswer(boolean userPressedTrue){
        boolean correctAnswer = mQuestionBank[mCurrentIndex].isAnswerTrue();
        int messageResId=0;
        if(userPressedTrue==correctAnswer) {
            messageResId=R.string.correct_toast;
        }else {
            messageResId=R.string.incorrect_toast;
        }
        Toast.makeText(QuizActivity.this,messageResId,Toast.LENGTH_SHORT).show();
    }

    /**
     * 注意,我們先是呼叫了超類的實現方法,然後才呼叫具體的日誌記錄方法。這些超類方法的
     呼叫不可或缺。從以上程式碼可以看出,在回撥覆蓋實現方法裡,超類實現方法總在第一行呼叫。
     也就是說,應首先呼叫超類實現方法,然後再呼叫其他方法。
     
     */
    @Override
    protected void onStart() {
        super.onStart();
        Log.d(TAG, "onStart called");
    }

    @Override
    protected void onResume() {
        super.onResume();
        Log.d(TAG, "onResume called");
    }

    @Override
    protected void onPause() {
        super.onPause();
        Log.d(TAG, "onPause called");
    }

    @Override
    protected void onStop() {
        super.onStop();
        Log.d(TAG, "onStop called");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy called");
    }
}

覆蓋onSaveInstanceState(Bundle)方法並不僅僅用於處理與裝置旋轉相關的問題。使用者離開當前activity使用者介面,或Android需要回收記憶體時,activity也會被銷燬。(例如,使用者按了主螢幕鍵,然後播放視訊或玩起遊戲。)基於使用者體驗考慮,Android從不會為了回收記憶體,而去銷燬可見的activity(處於暫停或執行狀態)。只有在呼叫過onStop()並執行完成後,activity才會被標為可銷燬。系統隨時會銷燬掉已停止的activity 。不用擔心資料丟失, activity 停止時, 會呼叫onSaveInstanceState(Bundle)方法的。所以,解決旋轉資料丟失問題,就是搶在系統銷燬activity之前儲存資料。儲存在onSaveInstanceState(Bundle)的資料該如何倖免於難呢?呼叫該方法時,使用者資料隨即被儲存在Bundle物件中,然後作業系統將Bundle物件放入activity記錄中。activity暫存後,Activity物件不再存在,但作業系統會將activity記錄物件儲存起來。這樣,
在需要恢復activity時,作業系統可以使用暫存的activity記錄重新啟用activity。注意,activity進入暫存狀態並不一定需要呼叫onDestroy()方法。不過,onStop()和onSaveInstanceState(Bundle)是兩個可靠的方法(除非裝置出現重大故障)。因而,常見的做法是,覆蓋onSaveInstanceState(Bundle)方法,在Bundle物件中,儲存當activity的小的或暫存狀態的資料;覆蓋onStop()方法,儲存永久性資料,如使用者編輯的文字等。onStop()方法呼叫完,activity隨時會被系統銷燬,所以用它儲存永久性資料。
在這裡插入圖片描述
那麼暫存的activity記錄到底可以保留多久?前面說過,使用者按了後退鍵後,系統會徹底銷燬
當前的activity。此時,暫存的activity記錄同時被清除。此外,系統重啟的話,暫存的activity記錄
也會被清除。

activity 記憶體清理現狀
低記憶體狀態下,Android直接從記憶體清除整個應用程序,連帶應用的所有activity。目前,Android還做不到只銷毀單個activity。
相比其他程序,有前臺(執行狀態)或可見(暫停狀態)activity的程序的優先順序更高。需要釋放資源時,Android系統的首選目標是低優先順序程序。使用者體驗至上,理論上,作業系統不會殺掉帶有可見activity的程序。當然出現重啟或宕機這樣的大故障就難說了。

日誌記錄的級別與方法
使用android.util.Log類記錄日誌,不僅可以控制日誌的內容,還可以控制用來區分資訊重要程度的日誌級別。Android支援如表3-2所示的五種日誌級別。每一個級別對應一個Log類方法。要輸出什麼級別的日誌,呼叫對應的Log類方法就可以了。
在這裡插入圖片描述

需要說明的是,所有的日誌記錄方法都有兩種引數簽名:string型別的tag引數和msg引數;除tag和msg引數外再加上Throwable例項引數。附加的Throwable例項引數為應用丟擲異常時記錄異常資訊提供了方便。對於輸出的日誌資訊,可使用常用的Java字串連線操作拼接出需要的資訊,或者使用String.format對輸出日誌資訊進行格式化操作,以滿足個性化的使用要求。
兩種方法不同引數簽名的使用例項

// Log a message at "debug" log level
Log.d(TAG, "Current question index: " + mCurrentIndex);
Question question;
try {
question = mQuestionBank[mCurrentIndex];
} catch (ArrayIndexOutOfBoundsException ex) {
// Log a message at "error" log level, along with an exception stack trace
Log.e(TAG, "Index was out of bounds", ex);
}

Demo下載地址:
https://download.csdn.net/download/weixin_43953649/10851072