對屏幕旋轉而引發的Activity重新創建的問題想必所有從事android開發的人來說再熟悉不過了,大家可以通過測試來了解這整個過程。比如我的測試過程如下:
新建BaseAcitivity作為父類(方便添加測試類)
BaseAcitivity.java
public class BaseAcitivity extends AppCompatActivity {
protected final String TAG ;
public BaseAcitivity() {
TAG = this.getClass().getSimpleName();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate");
}
@Override
protected void onStart() {
super.onStart();
Log.d(TAG,"onStart");
}
@Override
protected void onResume() {
super.onResume();
Log.d(TAG, "onResume");
}
@Override
protected void onPause() {
super.onPause();
Log.d(TAG, "onPause");
}
@Override
protected void onStop() {
super.onStop();
Log.d(TAG, "onStop");
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy");
}
}
代碼略多,只是為了演示activity整個生命周期的回調,如果大家比較熟悉了可以略過這段代碼。-
建立子類
ScreenChangeActivity.java
public class ScreenChangeActivity extends BaseAcitivity {
private StringBuffer text = new StringBuffer();
private TextView textView;@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_screen_change); textView = (TextView)findViewById(R.id.text); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); updateTextView("onSaveInstanceState\n"); Log.d(TAG,"onSaveInstanceState"); } @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); updateTextView("onRestoreInstanceState\n"); Log.d(TAG,"onRestoreInstanceState"); } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); updateTextView("onConfigurationChanged\n"); updateTextView("newConfig:" + newConfig.toString()); Log.d(TAG,"onConfigurationChanged"); } private void updateTextView(String str){ text.append(str); textView.setText(text.toString()); } }
運行程序觀察打印日志如下:
ScreenChangeActivity: onCreate
ScreenChangeActivity: onStart
ScreenChangeActivity: onResume
接著旋轉屏幕觀察打印日志如下:
ScreenChangeActivity: onPause
ScreenChangeActivity: onSaveInstanceState
ScreenChangeActivity: onStop
ScreenChangeActivity: onDestroy
ScreenChangeActivity: onCreate
ScreenChangeActivity: onStart
ScreenChangeActivity: onRestoreInstanceState
正常運行程序的流程不多講了,通過日志可以看出如果屏幕旋轉了確實發生Activity銷毀并重新創建的情況,銷毀的流程告訴我們必然會調用onPause
, onStop
及onDestroy
。大家仔細觀察旋轉后的日志輸出可以發現onSaveInstanceState
,onRestoreInstanceState
會在銷毀之前的onPause
以及重建后的onStart
方法之后調用,說明在銷毀之前你可以在onSaveInstanceState
方法中做些數據保存等操作,在銷毀之后需要恢復數據的操作放在在onSaveInstanceState
方法中。
在日志中沒有看到onConfigurationChanged
方法被調用過,這是我們在AndroidManifest.xml
文件中沒有對activity的android:configChanges=""
配置參數。現在配置上參數如下:
<activity android:name=".ScreenChangeActivity" android:configChanges="screenSize"></activity>
表示當屏幕大小變化時Activity自己來處理這種情況而不是交給系統來處理(默認就是銷毀再重建)
運行并旋轉屏幕再次測試觀察日志輸出:
ScreenChangeActivity: onCreate
ScreenChangeActivity: onStart
ScreenChangeActivity: onResume
ScreenChangeActivity: onConfigurationChanged
發現確實如我們所料onSaveInstanceState
,onRestoreInstanceState
并沒有被調用并且Activity也沒有銷毀。
帶給我們的思考:
- 如何可以保證Activity不重建?
- 簡單處理可以配置參數如下:
android:configChanges="orientation|keyboardHidden|keyboard|screenSize"
但是這種方案在配置的參數之外的參數發生改變時同樣會交給系統處理導致重建,所以這種方案需要考慮很多情況,如果沒有特殊情況還是可以滿足需求的 - 配置參數
android:screenOrientation="portrait"
固定屏幕方向,但是損失了橫屏的體驗(視實際情況酌情處理)
- 簡單處理可以配置參數如下:
- 當有可能發生重建情況時如果正確的保存重要數據
做個簡單測試,新建A和B兩個Activity。- 從A進入B,旋轉屏幕改變B的方向,保持屏幕方向不變按返回鍵回到A觀察日志輸出會發現在A進入B的時候
onSaveInstanceState
方法被調用,從B返回A時onRestoreInstanceState
被調用。
這種現象完全可以理解,因為在B旋轉屏幕時屏幕大小以及發生改變,保持屏幕方向不變再返回A時,A是需要重建的 - 測試步驟和1相同,但是在進入B后不旋轉屏幕,直接返回A,發現
onRestoreInstanceState
并沒有調用。
這種現象是因為回到A時屏幕大小并沒有發生改變,所以并不需要調用onRestoreInstanceState
來恢復數據
- 從A進入B,旋轉屏幕改變B的方向,保持屏幕方向不變按返回鍵回到A觀察日志輸出會發現在A進入B的時候
所以最終我們還是需要配合onSaveInstanceState
和onRestoreInstanceState
來保持和恢復數據的
實際應用:
想必大家都處理過如下應用場景:
ActivityA用來展示相冊中的圖片,有個入口可以調用系統相機用來拍照片,調起系統相機進入拍照界面(暫時假定是ActivityB),拍攝完畢后回到ActivityA,我們需要掃描制定的文件路徑來更新ActivityA實時展示新拍的照片。有些機型比如典型的三星機器,在進入系統相機界面后會強制橫屏,如果不做任何處理的典型代碼如下:
TestActivity.java
private File file;//成員變量file用來保存拍照后的圖片文件
private void openCamera() {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
String dirPath = Environment.getExternalStorageDirectory().getAbsolutePath();
String dateStr = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.CHINA).format(new Date());
String imageFileName = dateStr + "wenwan_.jpg";
file = null;
file = new File(dirPath, imageFileName);
Uri imageUri = Uri.fromFile(file);
intent.putExtra(MediaStore.Images.Media.ORIENTATION, 0);
//指定拍照完成后的照片存放位置
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent, CAMERA_RESULT);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK && estCodreque == CAMERA_RESULT) {
//掃描制定位置的文件
String scanPath = file.getAbsolutePath();
MediaScannerConnection.scanFile(this,
new String[]{scanPath}, null,
new MediaScannerConnection.OnScanCompletedListener() {
public void onScanCompleted(String path, Uri uri) {
//掃描完畢后通知可以刷新UI了
Message message = scanfileHandler.obtainMessage(100);
scanfileHandler.sendMessage(message);
}
});
}
}
運行程序進入拍照界面,橫屏拍完照片返回講發生NullPointerException的異常。原因很簡單,因為ActivityA重建了,成員變量file將重新初始化為默認值null,而onActivityResult方法中使用了file.getAbsolutePath()
。當然有人認為是不是做個判空的安全操作就完事了呢,當然不可,如果判空不執行掃描代碼,則雖然不會發生異常,但是新拍的照片將不能事實展示出來。
解決方案就是加入如下代碼:
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if(file == null)
return;
outState.putSerializable("file",file);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
file = null;
file = (File) savedInstanceState.getSerializable("file");
}
在切換Activity的時候在onSaveInstanceState
中保持file到Bundle中,在需要恢復的時候從Bundle中獲取。
還有一種暴力解決辦法應該是將file定義為static成員,這樣對象重新創建時static成員將不會重新初始化而是保留上次的值,但是這無意中延長了file的生命周期,在Activity結束后file將不能被回收,所以最好不要這樣來解決問題