Activity

[Activity](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn) 类是 Android 应用的关键组件,而 Activity 的启动和组合方式则是该平台应用模型的基本组成部分。在编程范式中,应用是通过 main() 方法启动的,而 Android 系统与此不同,它会调用与其生命周期特定阶段相对应的特定回调方法来启动 [Activity](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn) 实例中的代码。

Activity 的概念

移动应用体验与桌面体验的不同之处在于,用户与应用的互动并不总是在同一位置开始,而是经常以不确定的方式开始。例如,如果您从主屏幕打开电子邮件应用,可能会看到电子邮件列表,如果您通过社交媒体应用启动电子邮件应用,则可能会直接进入电子邮件应用的邮件撰写界面。
[Activity](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn) 类的目的就是促进这种范式的实现。当一个应用调用另一个应用时,调用方应用会调用另一个应用中的 Activity,而不是整个应用。通过这种方式,Activity 充当了应用与用户互动的入口点。您可以将 Activity 实现为 [Activity](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn) 类的子类。
Activity 提供窗口供应用在其中绘制界面。此窗口通常会填满屏幕,但也可能比屏幕小,并浮动在其他窗口上面。通常,一个 Activity 实现应用中的一个屏幕。例如,应用中的一个 Activity 实现“偏好设置”屏幕,而另一个 Activity 实现“选择照片”屏幕。
大多数应用包含多个屏幕,这意味着它们包含多个 Activity。通常,应用中的一个 Activity 会被指定为主 Activity,这是用户启动应用时出现的第一个屏幕。然后,每个 Activity 可以启动另一个 Activity,以执行不同的操作。例如,一个简单的电子邮件应用中的主 Activity 可能会提供显示电子邮件收件箱的屏幕。主 Activity 可能会从该屏幕启动其他 Activity,以提供执行写邮件和打开邮件这类任务的屏幕。
虽然应用中的各个 Activity 协同工作形成统一的用户体验,但每个 Activity 与其他 Activity 之间只存在松散的关联,应用内不同 Activity 之间的依赖关系通常很小。事实上,Activity 经常会启动属于其他应用的 Activity。例如,浏览器应用可能会启动社交媒体应用的“分享”Activity。
要在应用中使用 Activity,您必须在应用的清单中注册关于 Activity 的信息,并且必须适当地管理 Activity 的生命周期。本文的后续内容将介绍这些主题。

配置清单

要使应用能够使用 Activity,您必须在清单中声明 Activity 及其特定属性。

声明 Activity

  1. <manifest ... >
  2. <application ... >
  3. <activity android:name=".ExampleActivity" />
  4. ...
  5. </application ... >
  6. ...
  7. </manifest >

声明 intent 过滤器

Intent 过滤器是 Android 平台的一项非常强大的功能。借助这项功能,您不但可以根据显式请求启动 Activity,还可以根据隐式请求启动 Activity。例如,显式请求可能会告诉系统“在 Gmail 应用中启动‘发送电子邮件’ Activity”,而隐式请求可能会告诉系统“在任何能够完成此工作的 Activity 中启动‘发送电子邮件’屏幕”。当系统界面询问用户使用哪个应用来执行任务时,这就是 intent 过滤器在起作用。
要使用此功能,您需要在 元素中声明 属性。此元素的定义包括 元素,以及可选的 元素和/或 元素。这些元素组合在一起,可以指定 Activity 能够响应的 intent 类型。例如,以下代码段展示了如何配置一个发送文本数据并接收其他 Activity 的文本数据发送请求的 Activity:

  1. <activity android:name=".ExampleActivity" android:icon="@drawable/app_icon">
  2. <intent-filter>
  3. <action android:name="android.intent.action.SEND" />
  4. <category android:name="android.intent.category.DEFAULT" />
  5. <data android:mimeType="text/plain" />
  6. </intent-filter>
  7. </activity>

在此示例中, 元素指定该 Activity 会发送数据。将 元素声明为 DEFAULT 可使 Activity 能够接收启动请求。 元素指定此 Activity 可以发送的数据类型。以下代码段展示了如何调用上述 Activity:

  1. // Create the text message with a string
  2. Intent sendIntent = new Intent();
  3. sendIntent.setAction(Intent.ACTION_SEND);
  4. sendIntent.setType("text/plain");
  5. sendIntent.putExtra(Intent.EXTRA_TEXT, textMessage);
  6. // Start the activity
  7. startActivity(sendIntent);

如果您打算构建一个独立的应用,不允许其他应用激活其 Activity,则不需要任何其他 intent 过滤器。您不想让其他应用访问的 Activity 不应包含 intent 过滤器,您可以自己使用显式 intent 启动它们。如需详细了解 Activity 如何响应 Intent,请参阅 Intent 和 Intent 过滤器

声明权限

您可以使用清单的 <activity> 标记来控制哪些应用可以启动某个 Activity。父 Activity 和子 Activity 必须在其清单中具有相同的权限,前者才能启动后者。如果您为父 Activity 声明了 <uses-permission> 元素,则每个子 Activity 都必须具有匹配的 <uses-permission>元素。
例如,假设您的应用想要使用一个名为 SocialApp 的应用在社交媒体上分享文章,则 SocialApp 本身必须定义调用它的应用所需具备的权限:

  1. <manifest>
  2. <activity android:name="...."
  3. android:permission=”com.google.socialapp.permission.SHARE_POST”
  4. />

然后,为了能够调用 SocialApp,您的应用必须匹配 SocialApp 清单中设置的权限:

  1. <manifest>
  2. <uses-permission android:name="com.google.socialapp.permission.SHARE_POST" />
  3. </manifest>

如需详细了解权限和安全性,请参阅安全性和权限

生命周期

activity_lifecycle.png
Activity 类提供六个核心回调:[onCreate()](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn#onCreate(android.os.Bundle))[onStart()](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn#onStart())[onResume()](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn#onResume())[onPause()](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn#onPause())[onStop()](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn#onStop())[onDestroy()](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn#onDestroy())。当 Activity 进入新状态时,系统会调用每个回调。

保存和恢复界面瞬态

当 Activity 因系统限制遭到销毁时,您应组合使用 ViewModelonSaveInstanceState()) 和/或本地存储来保留用户的界面瞬态。

实例状态

系统用于恢复先前状态的已保存数据称为实例状态,是存储在 Bundle 对象中的键值对集合。默认情况下,系统使用 Bundle 实例状态来保存 Activity 布局中每个 View 对象的相关信息(例如在 EditText 微件中输入的文本值)。这样,如果您的 Activity 实例被销毁并重新创建,布局状态便会恢复为其先前的状态,且您无需编写代码。但是,您的 Activity 可能包含您要恢复的更多状态信息,例如追踪用户在 Activity 中的进程的成员变量。

*注意:为了使 Android 系统恢复 Activity 中视图的状态,每个视图必须具有 android:id 属性提供的唯一 ID。

Bundle 对象并不适合保留大量数据,因为它需要在主线程上进行序列化处理并占用系统进程内存。如果不是要保留少量的数据,正如保存界面状态所述,你应该组合使用 persistent local storage, onSaveInstanceState()) 和 ViewModel 类。

使用 onSaveInstanceState() 保存简单轻量的界面状态

当您的 Activity 开始停止时,系统会调用 onSaveInstanceState()) 方法,以便您的 Activity 可以将状态信息保存到实例状态 Bundle 中。此方法的默认实现保存有关 Activity 视图层次结构状态的瞬态信息,例如 EditText 微件中的文本或 ListView 微件的滚动位置。
如要保存 Activity 的其他实例状态信息,您必须替换 onSaveInstanceState()),并将键值对添加到您的 Activity 意外销毁时所保存的 Bundle 对象中。替换 onSaveInstanceState() 时,如果您希望默认实现保存视图层次结构的状态,则必须调用父类实现。

  1. static final String STATE_SCORE = "playerScore";
  2. static final String STATE_LEVEL = "playerLevel";
  3. // ...
  4. @Override
  5. public void onSaveInstanceState(Bundle savedInstanceState) {
  6. // Save the user's current game state
  7. savedInstanceState.putInt(STATE_SCORE, currentScore);
  8. savedInstanceState.putInt(STATE_LEVEL, currentLevel);
  9. // Always call the superclass so it can save the view hierarchy state
  10. super.onSaveInstanceState(savedInstanceState);
  11. }

*注意 当用户显式关闭 Activity 时,或者在其他情况下调用 finish() 时,系统不会调用 onSaveInstanceState()。
如要保存持久化数据(例如用户首选项或数据库中的数据),您应在 Activity 位于前台时抓住合适机会。如果没有这样的时机,您应在执行 onStop()) 方法期间保存此类数据。

使用保存的实例状态恢复 Activity 界面状态

重建之前遭到销毁的 Activity 后,您可以从系统传递给 Activity 的 Bundle 中恢复保存的实例状态。onCreate()) 和 onRestoreInstanceState()) 回调方法均会收到包含实例状态信息的相同 Bundle
因为无论系统是新建 Activity 实例还是重新创建之前的实例,都会调用 onCreate()) 方法,所以在尝试读取之前,您必须检查状态 Bundle 是否为 null。如果为 null,系统将新建 Activity 实例,而不会恢复之前销毁的实例。
例如,以下代码段显示如何在 onCreate()) 中恢复某些状态数据:

  1. @Override
  2. protected void onCreate(Bundle savedInstanceState) {
  3. super.onCreate(savedInstanceState); // Always call the superclass first
  4. // Check whether we're recreating a previously destroyed instance
  5. if (savedInstanceState != null) {
  6. // Restore value of members from saved state
  7. currentScore = savedInstanceState.getInt(STATE_SCORE);
  8. currentLevel = savedInstanceState.getInt(STATE_LEVEL);
  9. } else {
  10. // Probably initialize members with default values for a new instance
  11. }
  12. // ...
  13. }

您可以选择实现系统在 onStart()) 方法之后调用的 onRestoreInstanceState()),而不是在 onCreate())期间恢复状态。仅当存在要恢复的已保存状态时,系统才会调用 onRestoreInstanceState()),因此您无需检查 Bundle 是否为 null:

  1. public void onRestoreInstanceState(Bundle savedInstanceState) {
  2. // Always call the superclass so it can restore the view hierarchy
  3. super.onRestoreInstanceState(savedInstanceState);
  4. // Restore state members from saved instance
  5. currentScore = savedInstanceState.getInt(STATE_SCORE);
  6. currentLevel = savedInstanceState.getInt(STATE_LEVEL);
  7. }

Activity之间的导航

根据您的 Activity 是否希望从即将启动的新 Activity 中获取返回结果,您可以使用 [startActivity()](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn#startActivity(android.content.Intent,%20android.os.Bundle))[startActivityForResult()](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn#startActivityForResult(android.content.Intent,%20int)) 方法启动新 Activity。这两种方法都需要传入一个 [Intent](https://developer.android.com/reference/android/content/Intent.html?hl=zh_cn) 对象。
生命周期回调的顺序已有明确定义,特别是当两个 Activity 在同一个进程(应用)中,并且其中一个要启动另一个时。以下是 Activity A 启动 Activity B 时的操作发生顺序:

  1. Activity A 的 onPause() 方法执行。
  2. Activity B 的 onCreate()、onStart() 和 onResume() 方法依次执行。(Activity B 现在具有用户焦点。)
  3. 然后,如果 Activity A 在屏幕上不再可见,则其 onStop() 方法执行。

您可以利用这种可预测的生命周期回调顺序管理从一个 Activity 到另一个 Activity 的信息转换。

启动模式

LaunchMode 说明
standard 系统在启动它的任务中创建 activity 的新实例
singleTop 如果activity的实例已存在于当前任务的顶部,则系统通过调用其onNewIntent(),否则会创建新实例
singleTask 系统创建新 task 并在 task 的根目录下实例化 activity。但如果 activity 的实例已存在于单独的任务中,则调用其 onNewIntent() 方法,其上面的实例会被移除栈。一次只能存在一个 activity 实例
singleInstance 相同 singleTask,activity始终是其task的唯一成员; 任何由此开始的activity 都在一个单独的 task 中打开

处理 Activity 状态更改

用户触发和系统触发的不同事件会导致 [Activity](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn) 从一个状态转换到另一个状态。本文档介绍了发生此类转换的一些常见情况,以及如何处理这些转换。

配置发生了更改

有很多事件会触发配置更改。最显著的例子或许是横屏和竖屏之间的屏幕方向变化。其他情况,如语言或输入设备的改变等,也可能导致配置更改。
当配置发生更改时,Activity 会被销毁并重新创建。原始 Activity 实例将触发 onPause()onStop()onDestroy() 回调。系统将创建新的 Activity 实例,并触发 onCreate())、onStart()onResume()) 回调。
结合使用 ViewModels、onSaveInstanceState() 方法和/或持久性本地存储,可使 Activity 的界面状态在配置发生更改后保持不变。在决定这些选项的组合方式时,需要考虑界面数据的复杂程度、应用的用例以及检索速度与内存使用的权衡。有关保存 Activity 界面状态的详情,请参阅保存界面状态

处理多窗口模式的情况

一旦应用进入多窗口模式(适用于 Android 7.0(API 级别 24)及更高级别),系统会向当前运行的 Activity 发送配置更改通知,从而完成上述生命周期转换。如果已经处于多窗口模式的应用调整了大小,也会出现这种行为。您的 Activity 可以自行处理配置更改,也可以让系统销毁 Activity 并使用新维度重新创建一个。
有关多窗口模式生命周期的详情,请参阅多窗口模式支持页的多窗口模式生命周期部分。
在多窗口模式下,虽然用户可以看到两个应用,但只有与用户交互的应用位于前台且具有焦点。该 Activity 处于“已恢复”状态,而另一个窗口中的应用则处于“已暂停”状态。
当用户从应用 A 切换到应用 B 时,系统会对应用 A 调用 [onPause()](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn#onPause()),对应用 B 调用 [onResume()](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn#onResume())。每当用户在应用之间切换时,系统就会在这两种方法之间切换。
有关多窗口模式的详情,请参阅多窗口模式支持

Activity 或对话框显示在前台

如果有新的 Activity 或对话框出现在前台,并且局部覆盖了正在进行的 Activity,则被覆盖的 Activity 会失去焦点并进入“已暂停”状态。然后,系统会调用 [onPause()](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn#onPause())
当被覆盖的 Activity 返回到前台并重新获得焦点时,会调用 [onResume()](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn#onResume())
如果有新的 Activity 或对话框出现在前台,夺取了焦点且完全覆盖了正在进行的 Activity,则被覆盖的 Activity 会失去焦点并进入“已停止”状态。然后,系统会快速地接连调用 [onPause()](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn#onPause())[onStop()](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn#onStop())
当被覆盖的 Activity 的同一实例返回到前台时,系统会对该 Activity 调用 [onRestart()](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn#onRestart())[onStart()](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn#onStart())[onResume()](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn#onResume())。如果被覆盖的 Activity 的新实例进入后台,则系统不会调用 onRestart(),而只会调用 [onStart()](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn#onStart())[onResume()](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn#onResume())
注意:当用户点按“概览”或主屏幕按钮时,系统的行为就好像当前 Activity 已被完全覆盖一样。

用户点按“返回”按钮

如果 Activity 位于前台,并且用户点按了返回按钮,Activity 将依次经历 [onPause()](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn#onPause())[onStop()](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn#onStop())[onDestroy()](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn#onDestroy()) 回调。活动不仅会被销毁,还会从返回堆栈中移除。
需要注意的是,在这种情况下,默认不会触发 [onSaveInstanceState()](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn#onSaveInstanceState(android.os.Bundle)) 回调。此行为基于的假设是,用户点按返回按钮时不期望返回 Activity 的同一实例。不过,您可以通过替换 [onBackPressed()](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn#onBackPressed()) 方法实现某种自定义行为,例如“confirm-quit”对话框。
如果您替换 [onBackPressed()](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn#onBackPressed()) 方法,我们仍然强烈建议您从被替换的方法调用 [super.onBackPressed()](https://developer.android.com/reference/android/app/Activity.html?hl=zh_cn#onBackPressed())。否则,返回按钮的行为可能会让用户感觉突兀。

系统终止应用进程

如果某个应用处于后台并且系统需要为前台应用释放额外的内存,则系统可能会终止后台应用以释放更多内存。要详细了解系统如何确定要销毁哪些进程,请阅读 Activity 状态和从内存中弹出以及进程和应用生命周期
要了解如何在系统终止您的应用进程时保存 Activity 界面状态,请参阅保存和恢复 Activity 状态

启动过程

68747470733a2f2f696d672d626c6f672e6373646e2e6e65742f3230313830343237313733353034393033.png

Activity启动涉及到的类

首先要简单介绍一下Activity启动过程涉及到的类,以便于更好的理解这个启动过程。

  • ActivityThread:App启动的入口
  • ApplicationThread:ActivityThread的内部类,继承Binder,可以进程跨进程通信。
  • ApplicationThreadProxy:ApplicationThread的一个本地代理,其它的client端通过这个对象调用server端ApplicationThread中方法。
  • Instrumentation:负责发起Activity的启动、并具体负责Activity的创建以及Activity生命周期的回调。一个应用进程只会有一个Instrumentation对象,App内的所有Activity都持有该对象的引用。
  • ActivityManagerService:简称AMS,是service端对象,负责管理系统中所有的Activity
  • ActivityManagerProxy:是ActivityManagerService的本地代理
  • ActivityStack:Activity在AMS的栈管理,用来记录已经启动的Activity的先后关系,状态信息等。通过ActivityStack决定是否需要启动新的进程。
  • ActivityRecord:ActivityStack的管理对象,每个Activity在AMS对应一个ActivityRecord,来记录Activity的状态以及其他的管理信息。其实就是服务器端的Activity对象的映像。
  • TaskRecord:AMS抽象出来的一个“任务”的概念,是记录ActivityRecord的栈,一个“Task”包含若干个ActivityRecord。AMS用TaskRecord确保Activity启动和退出的顺序。

    相关源码

    ActivityThread.java

    1. private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    2. ...
    3. ActivityInfo aInfo = r.activityInfo;
    4. if (r.packageInfo == null) {
    5. //step 1: 创建LoadedApk对象
    6. r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
    7. Context.CONTEXT_INCLUDE_CODE);
    8. }
    9. ... //component初始化过程
    10. java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
    11. //step 2: 创建Activity对象
    12. Activity activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
    13. ...
    14. //step 3: 创建Application对象
    15. Application app = r.packageInfo.makeApplication(false, mInstrumentation);
    16. if (activity != null) {
    17. //step 4: 创建ContextImpl对象
    18. Context appContext = createBaseContextForActivity(r, activity);
    19. CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
    20. Configuration config = new Configuration(mCompatConfiguration);
    21. //step5: 将Application/ContextImpl都attach到Activity对象
    22. activity.attach(appContext, this, getInstrumentation(), r.token,
    23. r.ident, app, r.intent, r.activityInfo, title, r.parent,
    24. r.embeddedID, r.lastNonConfigurationInstances, config,
    25. r.referrer, r.voiceInteractor);
    26. ...
    27. int theme = r.activityInfo.getThemeResource();
    28. if (theme != 0) {
    29. activity.setTheme(theme);
    30. }
    31. activity.mCalled = false;
    32. if (r.isPersistable()) {
    33. //step 6: 执行回调onCreate
    34. mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
    35. } else {
    36. mInstrumentation.callActivityOnCreate(activity, r.state);
    37. }
    38. r.activity = activity;
    39. r.stopped = true;
    40. if (!r.activity.mFinished) {
    41. activity.performStart(); //执行回调onStart
    42. r.stopped = false;
    43. }
    44. if (!r.activity.mFinished) {
    45. //执行回调onRestoreInstanceState
    46. if (r.isPersistable()) {
    47. if (r.state != null || r.persistentState != null) {
    48. mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state,
    49. r.persistentState);
    50. }
    51. } else if (r.state != null) {
    52. mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state);
    53. }
    54. }
    55. ...
    56. r.paused = true;
    57. mActivities.put(r.token, r);
    58. }
    59. return activity;
    60. }

Fragment

[Fragment](https://developer.android.google.cn/reference/androidx/fragment/app/Fragment.html) 表示 [FragmentActivity](https://developer.android.google.cn/reference/androidx/fragment/app/FragmentActivity.html) 中的行为或界面的一部分。您可以在一个 Activity 中组合多个片段,从而构建多窗格界面,并在多个 Activity 中重复使用某个片段。您可以将片段视为 Activity 的模块化组成部分,它具有自己的生命周期,能接收自己的输入事件,并且您可以在 Activity 运行时添加或移除片段(这有点像可以在不同 Activity 中重复使用的“子 Activity”)。

特点

  • Fragment 解决 Activity 间的切换不流畅,轻量切换。
  • 可以从 startActivityForResult 中接收到返回结果,但是View不能。
  • 只能在 Activity 保存其状态(用户离开 Activity)之前使用 commit() 提交事务。如果您试图在该时间点后提交,则会引发异常。 这是因为如需恢复 Activity,则提交后的状态可能会丢失。 对于丢失提交无关紧要的情况,请使用 commitAllowingStateLoss()。

    生命周期

onCreate()
系统会在创建时调用此方法。当Fragmant经历暂停或者停滞状态继而恢复后,执行。
onCreateView()
首次绘制界面时调用此方法。
onPause()
用户离开的第一个信号进行调用
fragment_lifecycle.png

Activity对Fragment生命周期影响

fragment_lifecycle2.png
与 Activity 一样,您也可使用 onSaveInstanceState(Bundle))、ViewModel 和持久化本地存储的组合,在配置变更和进程终止后保留片段的界面状态。

与Activity通信

执行此操作的一个好方法是,在片段内定义一个回调接口,并要求宿主 Activity 实现它。

  1. public static class FragmentA extends ListFragment {
  2. ...
  3. // Container Activity must implement this interface
  4. public interface OnArticleSelectedListener {
  5. public void onArticleSelected(Uri articleUri);
  6. }
  7. ...
  8. }
  9. public static class FragmentA extends ListFragment {
  10. OnArticleSelectedListener mListener;
  11. ...
  12. @Override
  13. public void onAttach(Activity activity) {
  14. super.onAttach(activity);
  15. try {
  16. mListener = (OnArticleSelectedListener) activity;
  17. } catch (ClassCastException e) {
  18. throw new ClassCastException(activity.toString());
  19. }
  20. }
  21. ...
  22. }