对android:screenOrientation及android:configChanges的思考
对屏幕旋转而引发的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整个生命周期的回调,如果大家比较熟悉了可以略过这段代码。
2. 建立子类
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将不能被回收,所以最好不要这样来解决问题