对android:screenOrientation及android:configChanges的思考

对屏幕旋转而引发的Activity重新创建的问题想必所有从事android开发的人来说再熟悉不过了,大家可以通过测试来了解这整个过程。比如我的测试过程如下:

  1. 新建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, onStoponDestroy。大家仔细观察旋转后的日志输出可以发现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不重建?
    1. 简单处理可以配置参数如下:
      android:configChanges="orientation|keyboardHidden|keyboard|screenSize"
      但是这种方案在配置的参数之外的参数发生改变时同样会交给系统处理导致重建,所以这种方案需要考虑很多情况,如果没有特殊情况还是可以满足需求的
    2. 配置参数
      android:screenOrientation="portrait"
      固定屏幕方向,但是损失了横屏的体验(视实际情况酌情处理)
  • 当有可能发生重建情况时如果正确的保存重要数据
    做个简单测试,新建A和B两个Activity。
    1. 从A进入B,旋转屏幕改变B的方向,保持屏幕方向不变按返回键回到A观察日志输出会发现在A进入B的时候onSaveInstanceState方法被调用,从B返回A时onRestoreInstanceState被调用。
      这种现象完全可以理解,因为在B旋转屏幕时屏幕大小以及发生改变,保持屏幕方向不变再返回A时,A是需要重建的
    2. 测试步骤和1相同,但是在进入B后不旋转屏幕,直接返回A,发现onRestoreInstanceState并没有调用。
      这种现象是因为回到A时屏幕大小并没有发生改变,所以并不需要调用onRestoreInstanceState来恢复数据

所以最终我们还是需要配合onSaveInstanceStateonRestoreInstanceState来保持和恢复数据的

实际应用:

想必大家都处理过如下应用场景:
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将不能被回收,所以最好不要这样来解决问题

posted @ 2016-05-18 21:00  laogui0906  阅读(686)  评论(0编辑  收藏  举报