Android屏幕旋转后的变更--ConfigChange

文章目录

  • 1. Activity生命周期的变化
    • 1.1 正常生命周期
    • 1.2 屏幕旋转后重建Activity
    • 1.3 解决数据丢失问题--onSaveInstanceState和onRestoreInstanceState
    • 1.4 整个屏幕旋转过程调用的生命周期方法
    • 1.5 onCreate方法中的savedInstanceState参数
    • 1.6 onSaveInstanceState的调用时机
    • 1.7 注意事项
  • 2. 存在Fragment时的生命周期变化
  • 3. 如何防止页面重建
    • 3.1 android:configChanges
    • 3.2 onConfigurationChanged
  • 4. 源码

在android开发中,有一个容易被忽视但其实很重要的问题:屏幕旋转后的页面重建。
本文将介绍下 当屏幕旋转后,页面生命周期的变化以及 如何防止页面重建带来的问题

1. Activity生命周期的变化

1.1 正常生命周期

一个Activity从创建到退出,正常的生命周期流程是:

onCreate-->onStart-->onResume-->onPause-->onStop-->onDestroy

1.2 屏幕旋转后重建Activity

当屏幕旋转时,Android系统会自动将当前屏幕方向Activity销毁,再重新创建一个适应新屏幕方向的Activity
很自然的,我们能猜到会执行onPause-->onStop-->onDestroy-->onCreate-->onStart-->onResume这些方法。但除了这些Android还为我们考虑到了数据丢失的问题。

试想一下这种场景:页面中有一个TextView和一个Button,每点击一次Button,TextView显示点击的次数。当点击了一些次数后,旋转屏幕,你会发现TextView上记录的次数恢复到了最开始的状态。
原因也不难解释:因为整个Activity销毁了,重新创建的Activity是一个最开始的状态。

1.3 解决数据丢失问题–onSaveInstanceState和onRestoreInstanceState

为了处理这种由于销毁重建导致的页面数据丢失的问题,Android提供了另外两个方法 onSaveInstanceStateonRestoreInstanceState,算是对生命周期方法的补充吧。

@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
    super.onSaveInstanceState(outState);
    Log.d(TAG, "onSaveInstanceState: ");

}

@Override
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    Log.d(TAG, "onRestoreInstanceState: ");
}

注意:onSaveInstanceState有两个重载方法,带outPersistentState参数的是针对那种重启手机的情况的,本文我们暂时不考虑,也不会回调。我们只关注第二个不带outPersistentState参数的就行了。
@Override
public void onSaveInstanceState(@NonNull Bundle outState, @NonNull PersistableBundle outPersistentState) {
super.onSaveInstanceState(outState, outPersistentState);
}

@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);

}

从名字上也可以看出,onSaveInstanceState是对状态的保存,onRestoreInstanceState是对状态的恢复。

1.4 整个屏幕旋转过程调用的生命周期方法

了解到这些,当屏幕旋转后回调的方法最终是这样的:

onPause-->onStop-->onSaveInstanceState-->onDestroy-->onCreate-->
onStart-->onRestoreInstanceState-->onResume

没错,onSaveInstanceState在onDestroy之前;onRestoreInstanceState在onResume之前。

1.5 onCreate方法中的savedInstanceState参数

除此之外,我们还应了解到,我们最常见的onCreate方法中其实有一个参数savedInstanceState。一直以来很少用到它,但现在终于轮到它发挥作用了。它就是之前销毁Activity时保存的数据,这样以来其实不用等到onRestoreInstanceState方法回调,在onCreate时我们就能拿到之前保存的数据了。

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_config_change_test);
    if (savedInstanceState != null) {
        // 页面重建了
        Log.d("TAG", "onCreate: savedInstanceState:" + savedInstanceState);
    }
}

再仔细观察下,savedInstanceState、outState这些参数都是Bundle类型的哦,也就是说他们互相传递毫无障碍,都是用来盛放键值对儿的。

1.6 onSaveInstanceState的调用时机

经过测试,以下情况都会调用onSaveInstanceState方法

  • 屏幕旋转
  • 锁屏
  • 按home键回到桌面
  • ActivityA 跳到 ActivityB,调ActivityA的onSaveInstanceState

而正常的退出Activity是不会调用onSaveInstanceState的

  • 按返回键退出Activity
  • 调用finish方法退出Activity

可见,onSaveInstanceState 就是在那些用户期望保留数据的场景才会调用,这也是它存在的初衷。

1.7 注意事项

  • onSaveInstanceState方法只适合保存少量的、不复杂的数据,如少量String,boolean等。
    因为它涉及到序列化反序列化等操作,如果数据复杂会比较耗时,导致页面卡顿。

2. 存在Fragment时的生命周期变化

除了Activity,我们还应该了解到Fragment在屏幕旋转过程也会自动销毁重建。看过Fragment的基础与应用系列文章的伙伴都知道,Fragment是依附在Activity上的,可以说是一些视图控件View组成的“片段”页面。当Activity都销毁了,那它上面的View肯定也都销毁了。相应的,Activity的View树恢复了,它自然也得把上面的Fragment恢复。在Activity看来,Fragment就是它上面的一些View构成的集合而已,当然要一视同仁的恢复了。

这里直接给出依次调用的生命周期方法。文末会给出源码,你也可以自己写小例子测试。

 Fragment: onPause:
 TestActivity: onPause:
 Fragment: onStop:
 TestActivity: onStop:
 Fragment: onSaveInstanceState:
 TestActivity: onSaveInstanceState:
 Fragment: onDestroyView:
 Fragment: onDestroy:
 Fragment: onDetach:
 TestActivity: onDestroy:
 Fragment: Fragment 构造方法:
 Fragment: onAttach:
 Fragment: onCreate: savedInstanceState:非空
 TestActivity: onCreate: savedInstanceState:非空
 Fragment: onCreateView:
 Fragment: onViewCreated:
 Fragment: onActivityCreated:
 Fragment: onViewStateRestored:
 Fragment: onStart:
 TestActivity: onStart:
 TestActivity: onRestoreInstanceState:
 TestActivity: onResume:
 Fragment: onResume:

可见,在Activity销毁重建的同时,其上的Fragment也进行了类似的过程,也存在保存和恢复数据的方法:

Fragment: onSaveInstanceState:
Fragment: onViewStateRestored:

3. 如何防止页面重建

那么屏幕旋转后一定非要页面重建吗?当然不是,Android尽可能的为我们提供了选择。

3.1 android:configChanges

如果你不想让页面的Activity销毁重建的话,可以在AndroidManifest.xml文件的Activity节点里添加android:configChanges配置,如下:

<activity
    android:name=".config_change.ConfigChangeTestActivity"
    android:configChanges="orientation|screenSize" />

android:configChanges的值代表了哪些配置发生变化时页面不必重建。上述配置代码的orientation|screenSize意思是说,方向 | 屏幕大小 发生变化时页面不重建。

注意:经过本人测试,这里必须同时配置orientation|screenSize这两个值才能阻止页面重建,只配置一个orientation或者screenSize都是不行的。

另外,还有一些其他值:screenLayout|keyboardHidden等,有兴趣可以自行了解。

3.2 onConfigurationChanged

配置完android:configChanges后,旋转屏幕你会发现,虽然页面旋转了,但Activity的生命周期方法没有调用,也就是页面没有销毁重建。目的达到了,但真的万事大吉了吗?

事实上,Android官方是不推荐我们添加这个阻止自动重建的配置的。仔细想想,为什么它会设计成默认自动重建,就是因为在屏幕方向或者大小发生变化时,页面所依赖的尺寸等值也不一样了。如果不重建,就意味着你可能会将竖屏时候的值应用给旋转后的横屏Activity,这样很难说不会出问题。除非你自己完成屏幕旋转后的适配工作,而这个是比较难以考虑周全的。

但Android依然把这个选择留给了我们,只是提醒我们要小心使用。当配置了android:configChanges阻止了页面重建后,意味着我们要自己处理配置变化后的适配工作。这时屏幕旋转,会调用onConfigurationChanged方法。

@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
        Log.d("TAG", "onConfigurationChanged: landscape");
        Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show();
    } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
        Log.d("TAG", "onConfigurationChanged: portrait");
        Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show();
    }
}

可以看出,配置数据存放在参数newConfig中,我们可以从中获取到新的屏幕方向等配置信息,进而做适配我们页面的处理。

4. 源码

最后贴出本文用到的案例源码,以供参考

主界面ConfigChangeTestActivity.java

public class ConfigChangeTestActivity extends AppCompatActivity {

    private static final String TAG = "ConfigChangeTestActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_config_change_test);
        if (savedInstanceState != null) {
            // 恢复之前保存的数据
            Log.d(TAG, "onCreate: savedInstanceState:" + savedInstanceState);
        } else {
            // 
            Log.d(TAG, "onCreate: savedInstanceState:" + savedInstanceState);
        }
    }

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

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

    @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: ");
    }

    @Override
    public void onSaveInstanceState(@NonNull Bundle outState, @NonNull PersistableBundle outPersistentState) {
        super.onSaveInstanceState(outState, outPersistentState);
        Log.d(TAG, "onSaveInstanceState:2 ");
    }

    @Override
    protected void onSaveInstanceState(@NonNull Bundle outState) {
        super.onSaveInstanceState(outState);
        Log.d(TAG, "onSaveInstanceState: ");

    }

    @Override
    protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        Log.d(TAG, "onRestoreInstanceState: ");
    }

    @Override
    public void onConfigurationChanged(@NonNull Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
            Log.d(TAG, "onConfigurationChanged: landscape");
            Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show();
        } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
            Log.d(TAG, "onConfigurationChanged: portrait");
            Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show();
        }
    }

    public void jumpToTest2(View view) {
        Intent intent = new Intent(this, ConfigTest2Activity.class);
        startActivity(intent);
    }

    public void addConfigFragment(View view) {
        getSupportFragmentManager().beginTransaction().replace(R.id.fragment_config, ConfigChangeFragment.class, null).commit();
    }
}

布局文件activity_config_change_test.xml


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/fragment_config"
        android:layout_width="match_parent"
        android:layout_height="200dp" />

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="29dp"
        android:onClick="addConfigFragment"
        android:text="添加Fragment" />

    <Button
        android:id="@+id/button4"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="29dp"
        android:onClick="jumpToTest2"
        android:text="跳到第二个页面" />

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
LinearLayout>

用到的Fragment: ConfigChangeFragment.java

public class ConfigChangeFragment extends Fragment {

    private static final String TAG = "ConfigChangeFragment";

    private static final String ARG_PARAM1 = "param1";
    private static final String ARG_PARAM2 = "param2";

    private String mParam1;
    private String mParam2;

    public ConfigChangeFragment() {
        Log.d(TAG, "ConfigChangeFragment: ");
    }

    public static ConfigChangeFragment newInstance(String param1, String param2) {
        ConfigChangeFragment fragment = new ConfigChangeFragment();
        Bundle args = new Bundle();
        args.putString(ARG_PARAM1, param1);
        args.putString(ARG_PARAM2, param2);
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onAttach(@NonNull Context context) {
        super.onAttach(context);
        Log.d(TAG, "onAttach: ");
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            mParam1 = getArguments().getString(ARG_PARAM1);
            mParam2 = getArguments().getString(ARG_PARAM2);
        }
        Log.d(TAG, "onCreate: savedInstanceState:" + savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        Log.d(TAG, "onCreateView: ");
        return inflater.inflate(R.layout.fragment_config_change, container, false);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        Log.d(TAG, "onViewCreated: ");
    }

    @Override
    public void onStart() {
        super.onStart();
        Log.d(TAG, "onStart: ");
    }

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

    @Override
    public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
        super.onViewStateRestored(savedInstanceState);
        Log.d(TAG, "onViewStateRestored: ");
    }

    @Override
    public void onSaveInstanceState(@NonNull Bundle outState) {
        super.onSaveInstanceState(outState);
        Log.d(TAG, "onSaveInstanceState: ");
    }


    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        Log.d(TAG, "onActivityCreated: ");
    }

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

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

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        Log.d(TAG, "onDestroyView: ");
    }

    @Override
    public void onDetach() {
        super.onDetach();
        Log.d(TAG, "onDetach: ");
    }

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

    @Override
    public void onConfigurationChanged(@NonNull Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
        Log.d(TAG, "onConfigurationChanged: ");
    }
}

Fragment布局:fragment_config_change.xml


<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >
    
    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="Hello Fragment" />

FrameLayout>

跳转到的第二个Activity页面就是一个空Activity,这里就不列了,就是为了测一下跳转到其他Activity时生命周期方法的调用而已。

你可能感兴趣的:(android开发学习,android,屏幕旋转,configChanges,onSaveInstance)