我们都知道,Android 在横竖屏切换、主题切换、语言等操作时,系统会 finish Activity ,然后重建,这样便可以重新加载配置变更后的资源。
如果你只有 Activity 的内容需要展示,那这样处理是没有问题的,但是如果界面在点击操作之后打开一个 Dialog,那在配置改变后这个 Dialog 还会在么?答案是不一定,我们来看看展示 Dialog 有几种方式。
Dilog#show()
这可能是大家比较常用的方法,创建一个 Dialog ,然后调用其 show 方法,就像这样。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<View>(R.id.tvDialog).setOnClickListener {
AlertDialog.Builder(this)
.setView(R.layout.test_dialog)
.show()
}
}
}
每次点击按钮会创建一个新的 Dialog 对象,然后调用 show 方法展示。我们来看看配置改变后,Dialog 的表现是怎样的。

通过视频我们可以看到,在切换横竖屏或主题时,Dialog 都没有恢复。这是因为Dialog#show这种方式是开发者自己管理 Dialog,所以在恢复 Activity 时,Activity 是不知道需要恢复 Dialog 的。那怎么让 Activity 知道当前展示了 Dialog 呢?那就需要用到下面的方式。
Activity#showDialog()
先来看看此方法的注释
Show a dialog managed by this activity. A call to onCreateDialog(int, Bundle) will be made with the same id the first time this is called for a given id. From thereafter, the dialog will be automatically saved and restored. If you are targeting Build.VERSION_CODES.HONEYCOMB or later, consider instead using a DialogFragment instead.
Each time a dialog is shown, onPrepareDialog(int, Dialog, Bundle) will be made to provide an opportunity to do any timely preparation.
简单来说这个方法会让 Activity 来管理需要展示的 Dialog,会跟 onCreateDialog(int, Bundle)成对出现,并且会保存这个 Dialog,在重复调用Activity#showDialog()时不会重复创建 Dialog 对象。Activity 自己管理 Dialog?那就能恢复了吗?我们来试试。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<View>(R.id.tvDialog).setOnClickListener {
showDialog(100) //自定义 id
}
}
override fun onCreateDialog(id: Int): Dialog? {
if(id == 100){ // id 与 showDialog 匹配
return AlertDialog.Builder(this)
.setView(R.layout.test_dialog)
.create()
}
return super.onCreateDialog(id)
}
代码很简单,调用 Activity#showDialog(int id)方法,然后重写 Activity#onCreateDialog(id:Int),匹配两边的 id 就可以了。我们来看看效果。

我们可以看到,确实切换主题后 Dialog 是恢复了的,不过还有个问题,就是这个 ScrollView 的状态没有恢复,滑动的位置被还原了,难道我们需要手动记住滑动的 position 然后再恢复?是的,不过这个操作 Android 已经替我们做了,我们需要做的就是给需要恢复的组件指定一个 id 就行。
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="200dp">
<ScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="300dp"
android:scrollbars="vertical"
android:scrollbarSize="10dp"
android:background="@color/primary_background">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tvContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:text="@string/test_content"
android:textAlignment="center"
android:textSize="30sp"
android:textColor="@color/primary_text"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
</FrameLayout>
刚刚 ScrollView 标签是没有 id 的,现在我们加了一个 id 再看看效果。

是不是很方便?这是什么原理呢?主要是两个方法,如下:
public void saveHierarchyState(SparseArray<Parcelable> container) {
dispatchSaveInstanceState(container);
}
protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
if (mID != NO_ID && (mViewFlags & SAVE_DISABLED_MASK) == 0) {
mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
Parcelable state = onSaveInstanceState();
if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {
throw new IllegalStateException(
"Derived class did not call super.onSaveInstanceState()");
}
if (state != null) {
// Log.i("View", "Freezing #" + Integer.toHexString(mID)
// + ": " + state);
container.put(mID, state);
}
}
}
public void restoreHierarchyState(SparseArray<Parcelable> container) {
dispatchRestoreInstanceState(container);
}
protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
if (mID != NO_ID) {
Parcelable state = container.get(mID);
if (state != null) {
// Log.i("View", "Restoreing #" + Integer.toHexString(mID)
// + ": " + state);
mPrivateFlags &= ~PFLAG_SAVE_STATE_CALLED;
onRestoreInstanceState(state);
if ((mPrivateFlags & PFLAG_SAVE_STATE_CALLED) == 0) {
throw new IllegalStateException(
"Derived class did not call super.onRestoreInstanceState()");
}
}
}
}
在 Actvity 执行 onSaveInstance 时,会保存 View 的层级状态,View 的 id 为 key,状态为 value,这样的一个SparseArray,View 的状态是在 View 的 onSaveInstance 方法生成的,所以,如果 View 没有重写 onSaveInstance时,就算指定了 id 也不会被恢复。我们来看看 ScrollView#onSaveInstance做了什么工作。
protected Parcelable onSaveInstanceState() {
if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
// Some old apps reused IDs in ways they shouldn't have.
// Don't break them, but they don't get scroll state restoration.
return super.onSaveInstanceState();
}
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.scrollPosition = mScrollY;
return ss;
}
ss.scrollPosition = mScrollY关键代码就是这一句,保存了 scrollPosition,恢复的逻辑就是在onRestoreInstance大家可以自己看看,逻辑比较简单,我这边就不列了。
Activity 如何恢复 Dialog?
配置变化后的恢复都会依赖onSaveInstance和onRestoreInstance,Dialog 也不例外,不过 Dialog 这两个流程都依赖 Activity,我们来完整过一遍 onSaveInstance 的流程,saveInstance从 Activity#performSaveInstanceState开始.
Activity.java
/**
* The hook for {@link ActivityThread} to save the state of this activity.
*
* Calls {@link #onSaveInstanceState(android.os.Bundle)}
* and {@link #saveManagedDialogs(android.os.Bundle)}.
*
* @param outState The bundle to save the state to.
*/
final void performSaveInstanceState(@NonNull Bundle outState) {
dispatchActivityPreSaveInstanceState(outState);
onSaveInstanceState(outState);
saveManagedDialogs(outState);
mActivityTransitionState.saveState(outState);
storeHasCurrentPermissionRequest(outState);
if (DEBUG_LIFECYCLE) Slog.v(TAG, "onSaveInstanceState " + this + ": " + outState);
dispatchActivityPostSaveInstanceState(outState);
}
/**
* Save the state of any managed dialogs.
*
* @param outState place to store the saved state.
*/
@UnsupportedAppUsage
private void saveManagedDialogs(Bundle outState) {
if (mManagedDialogs == null) {
return;
}
final int numDialogs = mManagedDialogs.size();
if (numDialogs == 0) {
return;
}
Bundle dialogState = new Bundle();
int[] ids = new int[mManagedDialogs.size()];
// save each dialog's bundle, gather the ids
for (int i = 0; i < numDialogs; i++) {
final int key = mManagedDialogs.keyAt(i);
ids[i] = key;
final ManagedDialog md = mManagedDialogs.valueAt(i);
dialogState.putBundle(savedDialogKeyFor(key), md.mDialog.onSaveInstanceState());
if (md.mArgs != null) {
dialogState.putBundle(savedDialogArgsKeyFor(key), md.mArgs);
}
}
dialogState.putIntArray(SAVED_DIALOG_IDS_KEY, ids);
outState.putBundle(SAVED_DIALOGS_TAG, dialogState);
}
saveManagedDialogs这个方法就是处理 Dialog 的流程,我们可以看到它会调用 md.mDialog.onSaveInstanceState(),来保存 Dialog 的状态,而这个md.mDialog就是在showDialog时保存的
public final boolean showDialog(int id, Bundle args) {
if (mManagedDialogs == null) {
mManagedDialogs = new SparseArray<ManagedDialog>();
}
ManagedDialog md = mManagedDialogs.get(id);
if (md == null) {
md = new ManagedDialog();
md.mDialog = createDialog(id, null, args);
if (md.mDialog == null) {
return false;
}
mManagedDialogs.put(id, md);
}
md.mArgs = args;
onPrepareDialog(id, md.mDialog, args);
md.mDialog.show();
return true;
}
这样流程就能串起来了吧,用Activity#showDialog关联 Activity 与 Dialog,在 Activity onSaveInstance 时会调用 Dialog#onSaveInstance保存状态,而不管在 Activity 或 Dialog 的 onSaveInstance 里都会执行View#saveHierarchyState来保存视图层级状态,这样不管是 Activity 还是 Dialog 亦或是 View 便都可以恢复啦。
不过以上描述的恢复,恢复的都是 Android 原生数据,如果你需要恢复业务数据,那就需要自己保存啦,不过 Google 也为我们提供了解决方案,就是 Jetpack ViewModel,对吧?
这样通过 ViewModel 和 SaveInstance 就可以恢复所有业务和视图状态了!
总结
到这边,关于如何恢复 Dialog 的主要内容就分享完了,需要多说一句的是,Activity#showDialog方法已被标记为废弃。
Use the new DialogFragment class with FragmentManager instead; this is also available on older platforms through the Android compatibility package.
原理都是一样,大家可以根据自己的需要选择。










































































若不是最近看到这样一篇新闻。
当时机哥身边的亲朋好友,只要是有智能手机的。
原因倒是不复杂,在那个流量资费偏高的年代。
WiFi万能钥匙,总是能如它的名字般神奇,帮用户成功连上WiFi。
关键是,这软件还免费使用。
WiFi万能钥匙发布不到三年,就拥有超过5亿的激活用户。
什么叫风头无两啊,


再加上App内部,出现了各种离谱的广告。
不知道的,还以为下了个病毒软件呢...
但它这些年,积攒起来的崩坏口碑。
当然啦,如果只是广告讨人嫌。
但它能帮咱们连上各种场合的WiFi,原理并不是暴力破解。
听起来,似乎是个不错的模式对吧。
你帮我,我帮你,天下就没有难办的事儿了。
但理想很丰满,现实很骨感。

我凭什么无缘无故,给一个陌生人一块钱?
他表示,App可以直接从用户手机拿到WiFi密码。
可谓是明文存放,点开就送。
可很凑巧的是,早期的安卓手机获取权限非常简单。
紧接着,最关键的问题来了。
作为一个,只会输入“Hello World”的代码废柴。

嗯?难道说...
如果实在遇到一些,数据库里配对不上的WiFi。
机友们都懂的,其实很多家庭路由器,密码都设置得很简单。


“我们可是有9亿用户总量的,欢迎来合作啊喂。
不能说克制,只能说处处皆是广告位。
而WiFi万能钥匙,对于广告的内容筛选,更是让人汗流浃背。

讲道理,以它如此庞大的用户总量。
更何况,现在早就不是,流氓App能随意践踏手机的时代了。


结果发现,它现在往App塞了个短剧板块。
emmm...机哥也没啥好说的,祝它一切顺利吧。




















































































































