在 Android 开发中,经常会遇到这样一个问题:

为什么横竖屏切换时 Activity 会重新走生命周期?
为什么设置了 android:configChanges="orientation" 之后,有时候还是会重建?
既然重建这么麻烦,Android 为什么不默认不重建呢?

这篇文章就围绕横竖屏切换、Activity 重建、configChanges 的设计原因和实际使用场景做一次系统梳理。


一、横竖屏切换时,默认会发生什么?

默认情况下,如果 Activity 没有配置 android:configChanges

<activity android:name=".MainActivity" />

当手机从竖屏切到横屏,或者从横屏切回竖屏时,系统会认为当前设备配置发生了变化。

于是 Activity 通常会经历一次销毁和重建:

onPause()
onStop()
onSaveInstanceState()
onDestroy()

onCreate()
onStart()
onRestoreInstanceState()
onResume()

也就是说,横竖屏切换不是简单地“重新绘制一下界面”,而是:

旧 Activity 被销毁
新的 Activity 被创建
新的资源重新加载
新的界面重新生成

这就是为什么很多初学者会发现:

横竖屏一切换,onCreate() 又执行了

这不是 bug,而是 Android 的默认设计。


二、Android 为什么默认要重建 Activity?

很多人第一反应是:

直接默认不重建不就好了吗?
横竖屏切换时让界面自己重新布局不行吗?

表面上看,这样好像更简单,但实际上 Android 默认重建 Activity 是有原因的。

1. 横屏和竖屏可能不是同一套界面

Android 的资源系统支持根据不同配置加载不同资源。

比如布局文件可以这样写:

res/layout/activity_main.xml
res/layout-land/activity_main.xml

竖屏时加载:

res/layout/activity_main.xml

横屏时加载:

res/layout-land/activity_main.xml

也就是说,同一个 Activity 在横屏和竖屏下,可能根本不是同一套布局。

例如一个新闻 App:

竖屏时:

上面是标题
中间是文章列表
点进去进入详情页

横屏时:

左边是文章列表
右边是文章详情

这已经不是“把原来的界面旋转 90 度”这么简单了,而是 UI 结构发生了变化。

如果系统不重建 Activity,就需要开发者自己处理:

重新选择横屏布局
重新加载横屏资源
重新计算 View 尺寸
重新调整控件显示隐藏
重新适配屏幕宽高

这对普通页面来说反而更容易出错。

所以 Android 默认采用了一种更稳妥的方式:

配置变化了
旧界面销毁
按照新配置重新创建界面

这样系统就可以自动加载最适合当前配置的资源。


2. 横竖屏切换不仅仅是 orientation 变化

很多人以为横竖屏切换只是:

orientation 变化

但实际上现代 Android 中,横竖屏切换通常还会伴随:

screenSize 变化
screenLayout 变化
可用窗口大小变化

在折叠屏、平板、多窗口模式、大屏设备上,这种情况更加明显。

所以只写:

android:configChanges="orientation"

在很多情况下是不够的。

因为你只告诉系统:

方向变化我自己处理

但你没有告诉系统:

屏幕尺寸变化我也自己处理
屏幕布局变化我也自己处理

于是当 screenSizescreenLayout 变化时,Activity 仍然可能被重建。


3. 配置变化不只是横竖屏

Android 的 configuration change 不只有横竖屏,还包括很多类型:

屏幕方向变化
屏幕尺寸变化
字体大小变化
系统语言变化
深色模式 / 浅色模式变化
键盘可用性变化
外接显示器变化
折叠屏展开或折叠

这些变化都会影响资源选择。

例如:

values-zh/
values-en/
values-night/
layout-land/
layout-sw600dp/
drawable-hdpi/
drawable-xhdpi/

如果系统不重建 Activity,那么这些资源是否重新加载、何时重新加载、怎么重新应用,就都需要开发者自己保证。

这显然不适合默认行为。

所以 Android 的设计思路是:

默认让系统处理配置变化
开发者只负责保存和恢复状态

而不是:

默认让开发者手动处理所有配置变化

三、configChanges 的本质是什么?

android:configChanges 的本质是告诉系统:

某些配置变化不要帮我重建 Activity,我自己处理。

例如:

<activity
    android:name=".MainActivity"
    android:configChanges="orientation|screenSize|screenLayout" />

这表示:

orientation 变化,我自己处理
screenSize 变化,我自己处理
screenLayout 变化,我自己处理

配置之后,当这些配置发生变化时,Activity 不会因为这些变化销毁重建,而是回调:

override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)

    // 在这里自己处理配置变化
}

所以可以这样理解:

情况

谁处理配置变化

Activity 是否重建

不配置 configChanges

系统处理

会重建

配置了 configChanges

开发者自己处理

不会因为声明的配置项重建

注意,这里有一个重点:

configChanges 只对你声明的配置项生效。

如果你只写了:

android:configChanges="orientation"

那么只有 orientation 被你接管。

如果同时发生了:

screenSize 变化
screenLayout 变化
keyboardHidden 变化

而你没有声明这些变化,那么 Activity 仍然可能被系统重建。


四、为什么以前资料说切竖屏会执行两次?

有些老资料会说:

不设置 configChanges 时,切横屏执行一次,切竖屏执行两次
只设置 orientation 时,切横屏执行一次,切竖屏执行两次

这个说法在某些老版本、老设备、模拟器环境下确实可能出现,但不能简单理解成“所有 Android 设备都是这样”。

更准确的理解是:

横竖屏切换过程中,可能连续触发了多个 configuration change。

例如:

orientation 变化
screenSize 变化
keyboardHidden 变化

如果这些配置变化不是一次性合并处理,或者某些配置项没有被声明处理,就可能导致 Activity 重建多次。

这里经常被提到的 keyboardHidden,主要指的是:

硬件键盘可见性变化
外接键盘状态变化
可拆卸键盘状态变化

它不是普通输入框弹出的软键盘。

普通软键盘弹出,一般影响的是:

窗口可见区域
WindowInsets
adjustResize
adjustPan
布局重新测量

通常不会导致 Activity 走:

onDestroy()
onCreate()

所以,“切竖屏执行两次是因为键盘问题”这个说法要谨慎理解。

更准确地说:

可能和 keyboardHidden 有关
但本质是一次旋转过程中触发了多个配置变化

现代 Android 中,更常见需要关注的是:

orientation
screenSize
screenLayout

五、横竖屏不想重建,应该怎么配置?

如果确实希望横竖屏切换时 Activity 不重建,常见配置是:

<activity
    android:name=".MainActivity"
    android:configChanges="orientation|screenSize|screenLayout" />

如果你的页面还涉及外接键盘、硬件键盘、可拆卸键盘,可以再加:

<activity
    android:name=".MainActivity"
    android:configChanges="orientation|screenSize|screenLayout|keyboard|keyboardHidden" />

然后在 Activity 中重写:

override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)

    if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
        // 横屏处理
    } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
        // 竖屏处理
    }
}

但是要注意:

配置了 configChanges,不是说系统什么都不管了,而是 Activity 不会重建,开发者需要自己处理业务上的配置变化。

View 本身仍然可能重新测量、重新布局、重新绘制。

你真正需要处理的是:

横屏显示哪些控件
竖屏显示哪些控件
播放器是否全屏
地图面板放左边还是放下面
相机预览方向是否正确
游戏画布尺寸是否变化
WebView 是否需要保持状态

六、自己处理 configChanges 的实际场景

普通页面一般不建议随便配置 configChanges

真正适合自己处理配置变化的,通常是那些:

重建成本高
状态恢复复杂
用户体验容易被打断
底层资源初始化昂贵

的页面。

下面看几个典型场景。


1. 视频播放页

视频播放页是最典型的场景。

竖屏时可能是:

上面播放视频
下面显示标题、简介、评论

横屏时可能是:

视频全屏播放
隐藏标题、评论、状态栏、导航栏

如果横竖屏切换时 Activity 重建,可能导致:

播放器重新初始化
播放进度丢失
画面黑屏
重新缓冲
用户体验变差

所以视频页常常会配置:

<activity
    android:name=".VideoActivity"
    android:configChanges="orientation|screenSize|screenLayout" />

然后自己处理横竖屏 UI:

override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)

    if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
        enterLandscapeMode()
    } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
        enterPortraitMode()
    }
}

private fun enterLandscapeMode() {
    titleView.visibility = View.GONE
    commentRecyclerView.visibility = View.GONE

    val params = playerView.layoutParams
    params.width = ViewGroup.LayoutParams.MATCH_PARENT
    params.height = ViewGroup.LayoutParams.MATCH_PARENT
    playerView.layoutParams = params
}

private fun enterPortraitMode() {
    titleView.visibility = View.VISIBLE
    commentRecyclerView.visibility = View.VISIBLE

    val screenWidth = resources.displayMetrics.widthPixels
    val params = playerView.layoutParams
    params.width = ViewGroup.LayoutParams.MATCH_PARENT
    params.height = screenWidth * 9 / 16
    playerView.layoutParams = params
}

这个场景的核心是:

不销毁播放器
只调整播放器容器和周围 UI

2. 相机 / 扫码页面

相机页面、扫码页面、录像页面也经常自己处理配置变化。

如果 Activity 被重建,可能导致:

Camera 关闭再打开
预览黑屏
扫码中断
录像中断
重新对焦

所以可以配置:

<activity
    android:name=".CameraActivity"
    android:configChanges="orientation|screenSize|screenLayout" />

然后在配置变化时重新计算预览方向和布局:

override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)

    updateCameraPreview()
}

private fun updateCameraPreview() {
    val rotation = windowManager.defaultDisplay.rotation

    when (rotation) {
        Surface.ROTATION_0 -> {
            previewView.rotation = 0f
        }
        Surface.ROTATION_90 -> {
            previewView.rotation = 90f
        }
        Surface.ROTATION_270 -> {
            previewView.rotation = 270f
        }
    }
}

实际项目中,相机页面还可能需要处理:

预览尺寸
传感器方向
裁剪区域
取景框位置
扫码框位置

核心思想是:

不重启相机
只调整预览方向和界面布局

3. WebView 页面

WebView 页面也经常不希望 Activity 因横竖屏重建。

例如用户正在填写一个 H5 表单,如果一旋转屏幕 Activity 重建,WebView 可能重新加载,导致用户输入丢失。

可以这样配置:

<activity
    android:name=".WebActivity"
    android:configChanges="orientation|screenSize|screenLayout|keyboard|keyboardHidden" />

然后:

override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)

    // 很多 WebView 页面这里不需要做太多事情
    // 主要目的是避免 WebView 被销毁和重新加载
}

这个场景的重点不是复杂处理横竖屏布局,而是:

保持 WebView 实例
避免页面刷新
避免表单状态丢失

4. 地图页面

地图页面同样适合自己处理配置变化。

如果横竖屏切换导致 Activity 重建,可能出现:

地图重新初始化
定位状态丢失
缩放级别变化
Marker 重新加载
路线规划重新绘制

可以这样配置:

<activity
    android:name=".MapActivity"
    android:configChanges="orientation|screenSize|screenLayout" />

然后根据方向调整 UI:

override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)

    if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
        sidePanel.visibility = View.VISIBLE
        bottomPanel.visibility = View.GONE
    } else {
        sidePanel.visibility = View.GONE
        bottomPanel.visibility = View.VISIBLE
    }

    mapView.requestLayout()
}

横屏时:

左边地图,右边路线详情

竖屏时:

上面地图,下面路线详情

Activity 不重建,地图对象也不需要重新初始化。


5. 游戏页面

游戏页面通常使用:

SurfaceView
GLSurfaceView
OpenGL ES
Unity
Cocos
自研渲染引擎

这类页面如果 Activity 重建,可能导致:

游戏暂停
场景重新加载
纹理资源重新创建
音频中断
网络状态异常

所以游戏页面经常会配置:

<activity
    android:name=".GameActivity"
    android:configChanges="orientation|screenSize|screenLayout|keyboard|keyboardHidden" />

然后通知游戏引擎窗口大小变了:

override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)

    val width = resources.displayMetrics.widthPixels
    val height = resources.displayMetrics.heightPixels

    gameView.onScreenSizeChanged(width, height)
}

这个场景中,Activity 不重建,游戏引擎继续运行,只是更新渲染区域和输入区域。


七、普通页面为什么不建议滥用 configChanges?

既然 configChanges 可以避免 Activity 重建,那是不是所有页面都应该加?

答案是不建议。

比如这些页面:

登录页
设置页
列表页
详情页
普通表单页
普通业务页

通常没有必要自己接管配置变化。

因为默认重建虽然看起来麻烦,但它有一个重要好处:

系统会自动加载当前配置下最合适的资源

比如:

layout-land
layout-sw600dp
values-night
values-zh
drawable-xhdpi

如果你用了 configChanges,这些变化就不会通过 Activity 重建自动完成。

你需要自己考虑:

横屏布局是否要切换
夜间模式资源是否要刷新
语言变化是否要重新应用
字体大小变化是否要重新适配
屏幕尺寸变化是否要重新计算

如果处理不完整,就可能出现:

界面布局错乱
资源没有更新
语言切换不生效
字体大小不适配
深色模式显示异常

所以 configChanges 不是性能优化万能药。

它更像是一种声明:

系统,这些配置变化我自己负责,你不要重建我。

如果你没有真的处理这些变化,就不要轻易声明。


八、configChanges 和 onSaveInstanceState 的关系

默认重建 Activity 时,开发者通常需要通过状态保存机制恢复数据。

例如:

override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)

    outState.putString("keyword", searchEditText.text.toString())
    outState.putInt("scrollY", recyclerView.computeVerticalScrollOffset())
}

在重新创建时恢复:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val keyword = savedInstanceState?.getString("keyword")
}

如果使用 ViewModel,也可以在配置变化中保留页面数据。

也就是说,普通页面更推荐:

让系统重建 Activity
用 ViewModel 保存业务状态
用 onSaveInstanceState 保存临时 UI 状态

而不是为了避免重建,盲目使用 configChanges


九、面试中怎么回答?

如果面试官问:

Android 横竖屏切换为什么 Activity 会重建?

可以这样回答:

Android 把横竖屏切换看作一种配置变化。配置变化会影响资源选择,比如 layout-land、不同屏幕尺寸资源、不同语言资源、深色模式资源等。为了保证 Activity 使用的是当前配置下最合适的资源,系统默认会销毁并重建 Activity。

如果不希望系统重建,可以在 Manifest 中配置 android:configChanges,声明哪些配置变化由开发者自己处理。这样对应配置变化发生时,Activity 不会重建,而是回调 onConfigurationChanged()。但这意味着开发者需要自己处理布局、资源、尺寸、方向等变化。


如果继续追问:

为什么只写 orientation 有时还会重建?

可以这样回答:

因为现代 Android 中横竖屏切换不只是 orientation 变化,还可能伴随 screenSizescreenLayout 等变化。只声明 orientation 只能接管方向变化,其他未声明的配置变化仍然可能导致 Activity 重建。所以常见写法是:

android:configChanges="orientation|screenSize|screenLayout"

如果涉及硬件键盘或外接键盘,还可以加上:

keyboard|keyboardHidden

如果继续追问:

为什么不所有 Activity 都配置 configChanges?

可以这样回答:

因为配置 configChanges 后,系统不会自动通过 Activity 重建来重新加载适配资源,开发者需要自己处理配置变化。如果处理不完整,容易导致布局、资源、语言、字体、深色模式等状态不一致。普通页面让系统重建更安全,只有视频、相机、WebView、地图、游戏等重建成本高、状态恢复复杂的页面才适合自己处理。


十、总结

Android 横竖屏切换默认重建 Activity,不是设计缺陷,而是为了保证资源和界面状态与当前设备配置一致。

可以用一句话总结:

默认重建,是系统帮你处理配置变化;
配置 configChanges,是开发者接管配置变化。

普通页面建议让系统处理:

配置变化 -> Activity 重建 -> 自动加载新资源

特殊页面可以自己处理:

配置变化 -> onConfigurationChanged() -> 手动调整 UI 和业务状态

实际开发中,configChanges 适合这些场景:

场景

使用原因

视频播放页

避免播放器重建、播放中断

相机 / 扫码页

避免相机重新打开、预览黑屏

WebView 页面

避免网页刷新、表单丢失

地图页面

避免地图重新初始化

游戏页面

避免渲染引擎和资源重建

最后记住一点:

configChanges 不是为了偷懒,而是为了在特殊场景下接管系统默认行为。
如果你声明自己处理配置变化,就真的要把这些变化处理完整。