您的第一个 Health Connect 集成应用

1. 简介

e4a4985ad1cdae8b.png

什么是 Health Connect?

Health Connect 是一个面向 Android 应用开发者的健康数据平台。它提供了单个综合性界面,可供开发者访问用户的健康与健身数据,并确保在所有设备上提供一致的功能行为。借助 Health Connect,用户可在设备端安全地存储健康与健身数据,还可以全权控制和清楚了解相应访问权限。

Health Connect 如何运作?

Health Connect 支持 50 多种常见的健康与健身数据类型和类别,包括:活动数据、睡眠数据、营养数据、身体测量数据,以及心率和血压等生命体征数据。

Health Connect 的工作原理

获得用户权限后,开发者可以通过标准化架构和 API 行为,在 Health Connect 中安全地读取和写入数据。用户可以全权控制其隐私设置,还可以使用精细的控制设置随时查看哪些应用在请求访问数据。Health Connect 中的数据会存储在设备端并进行加密。用户还可以在其设备上禁止应用访问数据或删除他们不想要的数据,还可以选择在使用多个应用时优先处理某个数据源。

Health Connect 架构

架构

下面介绍了 Health Connect 的主要方面和架构组件:

  • 客户端应用 - 为了与 Health Connect 集成,客户端应用会将相应 SDK 关联到其健康与健身应用。这提供了一个与 Health Connect API 进行交互的 API Surface。
  • 软件开发套件 - 该 SDK 可让客户端应用通过 IPC 与 Health Connect APK 进行通信。
  • Health Connect APK - 这是实现 Health Connect 的 APK。它包含权限管理和数据管理组件。Health Connect APK 直接在用户设备上提供,从而使 Health Connect 以设备为中心,而非以账号为中心。
  • 权限管理 - Health Connect 包含一个界面,应用可通过该界面请求用户授予显示数据的权限。它还提供现有用户权限的列表。这样一来,用户便可以管理他们针对各种应用授予或拒绝的访问权限。
  • 数据管理 - Health Connect 提供一个界面,其中显示了所记录数据的概览,包括用户的步数、骑车速度、心率或其他支持的数据类型。

构建内容

在此 Codelab 中,您将构建一个与 Health Connect 集成的简单健康与健身应用。您的应用将执行以下操作:

  • 获取和检查用户的数据访问权限。
  • 将数据写入 Health Connect。
  • 从 Health Connect 中读取汇总数据。

学习内容

  • 如何设置您的环境,以支持 Health Connect 集成开发。
  • 如何获取权限和执行权限检查。
  • 如何为 Health Connect 平台贡献健康与健身数据。
  • 如何从设备端数据存储功能中受益。
  • 如何使用 Google 提供的开发者工具验证您的应用。

所需条件

  • 最新的稳定版 Android Studio
  • 搭载 Android SDK 版本 28 (Pie) 或更高版本的 Android 移动设备。

2. 设置

安装 Health Connect

Health Connect 可在 Play 商店中获取。将 Health Connect 安装到您的移动设备,以使用 Health Connect SDK 处理您的应用发送的所有请求。扫描下方二维码即可安装 Health Connect。

633ed0490a74595d.png

获取示例代码

如果您已安装 git,则只需运行以下命令即可克隆此代码库中的代码。如需检查是否已安装 git,请在终端或命令行中输入 git --version,并验证其是否正确执行。

git clone https://github.com/android/android-health-connect-codelab.git
cd android-health-connect-codelab

如果您未安装 git,可以点击下方按钮下载此 Codelab 的全部代码:

示例目录中包含此 Codelab 的 startfinished 代码。在 Android Studio 的 Project 视图中,您将看到两个模块:

  • start - 该项目的起始代码;您将通过更改这些代码来完成此 Codelab。
  • finished:该 Codelab 完成后的代码;用于检查您的工作。

探索起始代码

此 Codelab 示例应用具有由 Jetpack Compose 构建的基本界面,其中包含以下屏幕:

  • WelcomeScreen:这是应用的着陆页,根据 Health Connect 的可用性(已安装、未安装或不受支持)显示不同的消息。
  • PrivacyPolicyScreen:该页面说明了当用户点击“健康数据共享”权限对话框中的隐私权政策链接时所显示的应用的权限使用情况。
  • InputReadingsScreen:该页面演示了如何读取和写入简单的体重记录。
  • ExerciseSessionScreen:该页面可供用户插入和列出锻炼时段。点击记录后,用户会转到 ExerciseSessionDetailScreen,从而查看更多与相应时段相关的数据。
  • DifferentialChangesScreen:该页面演示了如何从 Health Connect 获取更改令牌并获取新更改。

HealthConnectManager 会存储与 Health Connect 交互的所有功能。在此 Codelab 中,我们将逐步引导您实现基本功能。start build 中的 <!-- TODO: 字符串在此 Codelab 中有对应的部分,您可以将其中提供的示例代码插入该项目中。

首先,将 Health Connect 添加到项目中!

添加 Health Connect 客户端 SDK

如需开始使用 Health Connect SDK,您需要向 build.gradle 文件添加一个依赖项。如需查找最新版本的 Health Connect,请查看 Jetpack 库版本

dependencies {
    // Add a dependency of Health Connect SDK
    implementation "androidx.health.connect:connect-client:1.0.0-alpha11"
}

声明 Health Connect 可见性

如需在应用内与 Health Connect 互动,请在 AndroidManifest.xml 中声明 Health Connect 软件包名称:

<!-- TODO: declare Health Connect visibility -->
<queries>
   <package android:name="com.google.android.apps.healthdata" />
</queries>

运行项目

全部设置完毕后,运行 start 项目。此时,您应该能够看到显示“Health Connect is installed on this device”这段文字的欢迎屏幕,以及一个菜单抽屉式导航栏。我们将在接下来的几个部分添加与 Health Connect 互动的功能。

d54773774e4dc9f.png 462cd7b6cf553ad.png

3. 权限控制

Health Connect 建议开发者仅针对应用中要用的数据类型发出权限请求。一揽子权限请求会降低用户对应用的信心,并可能降低用户信任度。如果权限遭拒超过两次,您的应用将被锁定,因此,不再显示权限请求。

在此 Codelab 中,我们只需要与下列数据相关的权限:

  • 锻炼时段
  • 心率
  • 步数
  • 消耗的总卡路里
  • 体重

声明权限

应用读取或写入的每种数据类型都需要使用 AndroidManifest.xml 中的权限进行声明。从版本 alpha10 开始,Health Connect 会使用标准 Android 权限声明格式。

如需为所需数据类型声明权限,请使用 <uses-permission> 元素并为其分配包含相关权限的相应名称。将它们嵌套在 <manifest> 标记中。如需查看权限及其对应数据类型的完整列表,请参阅数据类型列表

<!-- TODO: declare Health Connect permissions -->
  <uses-permission android:name="android.permission.health.READ_HEART_RATE"/>
  <uses-permission android:name="android.permission.health.WRITE_HEART_RATE"/>
  <uses-permission android:name="android.permission.health.READ_STEPS"/>
  <uses-permission android:name="android.permission.health.WRITE_STEPS"/>
  <uses-permission android:name="android.permission.health.READ_EXERCISE"/>
  <uses-permission android:name="android.permission.health.WRITE_EXERCISE"/>
  <uses-permission android:name="android.permission.health.READ_TOTAL_CALORIES_BURNED"/>
  <uses-permission android:name="android.permission.health.WRITE_TOTAL_CALORIES_BURNED"/>
  <uses-permission android:name="android.permission.health.READ_WEIGHT"/>
  <uses-permission android:name="android.permission.health.WRITE_WEIGHT"/>

AndroidManifest.xml 中声明一个 intent 过滤器,以处理可说明应用如何使用这些权限的 intent。您的应用需要处理此 intent,并显示一份隐私权政策,说明如何使用和处理用户数据。用户点按 Health Connect 权限对话框中的隐私权政策链接后,系统会向应用发送此 intent。

<!-- TODO: Add intent filter to handle permission rationale intent -->
<intent-filter>
    <action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
</intent-filter>

现在,重新打开该应用,查看声明的权限。点击菜单抽屉式导航栏中的设置前往 Health Connect 设置屏幕。然后,点击应用权限,您应在列表中看到 Health Connect Codelab。点击 Health Connect Codelab,以显示针对该应用的读取和写入权限的数据类型列表。

fbed69d871f92178.png 1b9c7764c1dbdfac.png

请求权限

除了直接将用户引导至 Health Connect 设置页面来管理权限外,您还可以通过 Health Connect API 从您的应用请求权限。请注意,用户可能会随时更改权限,因此请确保您的应用会检查是否具备所需权限。在该 Codelab 项目中,我们会先检查和发送权限请求,然后读取或写入数据。

HealthConnectClient 是 Health Connect API 的入口点。在 HealthConnectManager.kt 中,获取 HealthConnectClient 实例。

private val healthConnectClient by lazy { HealthConnectClient.getOrCreate(context) }

如需在您的应用内启动请求权限对话框,请先为必需的数据类型构建一组权限。您必须请求对仅使用的数据类型的权限。

例如,在“Record weight”屏幕中,您只需授予对 Record weight 的读写权限。我们已在 InputReadingsViewModel.kt 中创建权限集,如以下代码所示。

  val permissions = setOf(
    HealthPermission.getReadPermission(WeightRecord::class),
    HealthPermission.getWritePermission(WeightRecord::class),
  )

然后,在启动权限请求之前,检查用户是否已授予相应权限。在 HealthConnectManager.kt 中,使用 getGrantedPermissions 检查用户是否已授予对所需数据类型的权限。如需启动权限请求,您必须使用 PermissionController.createRequestPermissionResultContract() 创建 ActivityResultContract,如果未获得所需权限,则需要启动它。

  suspend fun hasAllPermissions(permissions: Set<String>): Boolean {
    return healthConnectClient.permissionController.getGrantedPermissions().containsAll(permissions)
  }

  fun requestPermissionsActivityContract(): ActivityResultContract<Set<String>, Set<String>> {
    return PermissionController.createRequestPermissionResultContract()
  }

在该 Codelab 示例应用中,如果您未针对所需的数据类型授予权限,您可能会在屏幕上看到 Request permissions 按钮。点击请求权限以打开 Health Connect 权限对话框。授予所需的权限,并返回 Codelab 应用。

626eedcec23659ce.png 6df6cf0e5c4a1a9e.png

4. 写入数据

让我们开始向 Health Connect 中写入记录。如需写入 Weight 记录,请创建带有体重输入值的 WeightRecord 对象。请注意,Health Connect SDK 支持各种单位类。例如,可以使用 Mass.kilograms(weightInput) 将用户体重设置为以公斤为单位。

写入 Health Connect 的所有数据都应指定时区偏移量信息。通过在写入数据时指定时区偏移量信息,可以在读取 Health Connect 中的数据时提供时区信息。

创建体重记录后,使用 healthConnectClient.insertRecords 将数据写入 Health Connect 中。

/**
* TODO: Writes [WeightRecord] to Health Connect.
*/
suspend fun writeWeightInput(weightInput: Double) {
   val time = ZonedDateTime.now().withNano(0)
   val weightRecord = WeightRecord(
       weight = Mass.kilograms(weightInput),
       time = time.toInstant(),
       zoneOffset = time.offset
   )
   val records = listOf(weightRecord)
   try {
      healthConnectClient.insertRecords(records)
      Toast.makeText(context, "Successfully insert records", Toast.LENGTH_SHORT).show()
   } catch (e: Exception) {
      Toast.makeText(context, e.message.toString(), Toast.LENGTH_SHORT).show()
   }
}

现在,我们运行这个应用。点击 Record weight,并以公斤为单位输入新的体重记录。如需验证体重记录是否已成功写入 Health Connect,请在“设置”中打开 Health Connect 应用,然后依次前往数据和访问权限 -> 身体测量数据 -> 体重 -> 查看所有条目。您应该会看到从 Health Connect Codelab 写入的新体重记录。

写入锻炼时段

时段是指用户执行活动的时间间隔。“健康数据共享”中的锻炼时段包括跑步、打羽毛球,等等。通过时段,用户可以衡量基于时间的表现。此数据记录一段时间内测量的一系列瞬时样本,例如活动期间的连续心率或位置样本。

以下示例展示了如何写入锻炼时段。使用 healthConnectClient.insertRecords 插入与时段关联的多条数据记录。此示例中的插入请求包括带有 ExerciseTypeExerciseSessionRecord、带有步数的 StepsRecord、带有 EnergyTotalCaloriesBurnedRecord 和一系列 HeartRateRecord 示例。

  /**
   * TODO: Writes an [ExerciseSessionRecord] to Health Connect.
   */
  suspend fun writeExerciseSession(start: ZonedDateTime, end: ZonedDateTime) {
    healthConnectClient.insertRecords(
      listOf(
        ExerciseSessionRecord(
          startTime = start.toInstant(),
          startZoneOffset = start.offset,
          endTime = end.toInstant(),
          endZoneOffset = end.offset,
          exerciseType = ExerciseSessionRecord.EXERCISE_TYPE_RUNNING,
          title = "My Run #${Random.nextInt(0, 60)}"
        ),
        StepsRecord(
          startTime = start.toInstant(),
          startZoneOffset = start.offset,
          endTime = end.toInstant(),
          endZoneOffset = end.offset,
          count = (1000 + 1000 * Random.nextInt(3)).toLong()
        ),
        TotalCaloriesBurnedRecord(
          startTime = start.toInstant(),
          startZoneOffset = start.offset,
          endTime = end.toInstant(),
          endZoneOffset = end.offset,
          energy = Energy.calories((140 + Random.nextInt(20)) * 0.01)
        )
      ) + buildHeartRateSeries(start, end)
    )
  }

  /**
   * TODO: Build [HeartRateRecord].
   */
  private fun buildHeartRateSeries(
    sessionStartTime: ZonedDateTime,
    sessionEndTime: ZonedDateTime,
  ): HeartRateRecord {
    val samples = mutableListOf<HeartRateRecord.Sample>()
    var time = sessionStartTime
    while (time.isBefore(sessionEndTime)) {
      samples.add(
        HeartRateRecord.Sample(
          time = time.toInstant(),
          beatsPerMinute = (80 + Random.nextInt(80)).toLong()
        )
      )
      time = time.plusSeconds(30)
    }
    return HeartRateRecord(
      startTime = sessionStartTime.toInstant(),
      startZoneOffset = sessionStartTime.offset,
      endTime = sessionEndTime.toInstant(),
      endZoneOffset = sessionEndTime.offset,
      samples = samples
    )
  }

5. Health Connect Toolbox

Health Connect Toolbox 简介

Health Connect Toolbox 是一款配套的开发者工具,可帮助您测试应用与 Health Connect 的集成。它可以直接在 Health Connect 中轻松读取和写入数据,支持您测试应用的 CRUD 操作。

在此 Codelab 中,我们将使用 Health Connect Toolbox 测试您刚刚实现的读写功能。

设置 Health Connect Toolbox

解压缩该 ZIP 文件以获取 APK 文件。然后,使用 adb 在已连接的设备上安装 Toolbox APK。遍历到 APK 所在的文件夹,然后运行以下命令:

$ adb install HealthConnectToolbox-{Version Number}.apk

首次打开 Health Connect Toolbox 应用时,系统会带您转到应用 > 特殊应用权限 > 显示在其他应用的上层下的权限设置。借助此权限,Health Connect Toolbox 可在其他应用上方显示叠加层,让您无需离开正在开发的应用即可测试数据读写。

如需管理测试的读写权限,您可以从 Toolbox 应用的主屏幕打开 Health Connect 应用,也可以直接进入权限流程。

c3e6fd40b03b408a.png

读取和写入健康记录

Health Connect Toolbox 支持读取和写入所有 Health Connect 数据类型。在此 Codelab 的上次会话中,您已成功向 Health Connect 写入了体重和锻炼时段记录。我们来看看您是否可以从 Health Connect Toolbox 中读取数据。

在 Health Connect 中读写数据之前,必须先从用户处获取权限;对于 Health Connect Toolbox 也是如此。首先,接受 Health Connect Toolbox 的权限请求。接下来,点击叠加层菜单中的搜索图标 1f407c55884bb8c3.png 打开对话框,选择数据类型(例如“Weight”)并点击 READ HEALTH RECORD 按钮。您应该会在 Codelab 示例应用中看到您刚才写入 Health Connect 的记录。

如需将记录插入到 Health Connect 中,请点击叠加层菜单中的修改图标 10c524823c596aea.png 打开对话框,然后选择相应数据类型。从 Toolbox 插入一条体重记录。在下一部分,我们将介绍如何通过 Health Connect API 读取记录,并在您的应用中显示数据。

cac9d4c249a1d107.png

6. 读取数据

现在,您已使用 Codelab 示例应用或 Toolbox 应用写入体重和锻炼时段记录。接下来,我们使用 Health Connect API 读取这些记录。首先,创建一个 ReadRecordsRequest,并指定记录类型和要读取的时间范围。ReadRecordsRequest 还可以设置 dataOriginFilter,以指定您要从哪个来源应用读取记录。

    /**
     * TODO: Reads in existing [WeightRecord]s.
     */
    suspend fun readWeightInputs(start: Instant, end: Instant): List<WeightRecord> {
        val request = ReadRecordsRequest(
            recordType = WeightRecord::class,
            timeRangeFilter = TimeRangeFilter.between(start, end)
        )
        val response = healthConnectClient.readRecords(request)
        return response.records
    }
  /**
   * TODO: Obtains a list of [ExerciseSessionRecord]s in a specified time frame.
   */
  suspend fun readExerciseSessions(start: Instant, end: Instant): List<ExerciseSessionRecord> {
    val request = ReadRecordsRequest(
      recordType = ExerciseSessionRecord::class,
      timeRangeFilter = TimeRangeFilter.between(start, end)
    )
    val response = healthConnectClient.readRecords(request)
    return response.records
  }

现在,我们运行这个应用,看看您能否看到一系列体重记录和锻炼时段。

a08af54eef6bc832.png 3b0781389f1094a1.png

7. 读取差分数据

Health Connect Differential Changes API 有助于跟踪一组数据类型在特定时间点的更改。例如,如果您想了解用户是否已更新或删除应用外的任何现有记录,以便相应地更新数据库,就可以使用该 API。

只有在前台运行的应用才能通过 Health Connect 读取数据。此限制旨在进一步加强用户隐私保护。它会通知用户并使其确信 Health Connect 对他们的数据没有后台读取访问权限,只能在前台读取和访问数据。当应用在前台运行时,借助 Differential Changes API,开发者可以通过部署更改令牌来检索对 Health Connect 所做的更改。

HealthConnectManager.kt 中有两个函数:getChangesToken()getChanges()。我们将向这些函数添加 Differential Changes API,以获取数据更改。

初始更改令牌设置

只有当您的应用使用更改令牌请求数据更改时,系统才会从 Health Connect 中检索这些更改。该更改令牌表示提交历史记录中将要发生差分数据获取操作的点。

若要获取更改令牌,请发送 ChangesTokenRequest,其中含有您要跟踪数据更改的一组数据类型。保存令牌,以便在想要从 Health Connect 检索任何更新时使用。

  /**
   * TODO: Obtains a Changes token for the specified record types.
   */
  suspend fun getChangesToken(): String {
    return healthConnectClient.getChangesToken(
      ChangesTokenRequest(
        setOf(
          ExerciseSessionRecord::class,
          StepsRecord::class,
          TotalCaloriesBurnedRecord::class,
          HeartRateRecord::class,
          WeightRecord::class
        )
      )
    )
  }

包含更改令牌的数据更新

如果您想获取上次您的应用与 Health Connect 同步时所做的更改,请使用之前获得的更改令牌并发送包含该令牌的 getChanges 调用。ChangesResponse 会返回 Health Connect 中观察到的变化(例如 UpsertionChangeDeletionChange)。

  /**
   * TODO: Retrieve changes from a Changes token.
   */
  suspend fun getChanges(token: String): Flow<ChangesMessage> = flow {
    var nextChangesToken = token
    do {
      val response = healthConnectClient.getChanges(nextChangesToken)
      if (response.changesTokenExpired) {
        throw IOException("Changes token has expired")
      }
      emit(ChangesMessage.ChangeList(response.changes))
      nextChangesToken = response.nextChangesToken
    } while (response.hasMore)
    emit(ChangesMessage.NoMoreChanges(nextChangesToken))
  }

现在,运行应用,并转到Changes屏幕。首先,点击启用 Track changes,以获取更改令牌。然后,插入来自 Toolbox 或 Codelab 应用的体重或锻炼时段。返回 Changes 屏幕,然后选择 Get new changes。现在,您应该可以看到插入的更改。

f3aded8ae5487e9c.png 437d69e3e000ce81.png

8. 汇总数据

Health Connect 还通过汇总 API 提供汇总数据。以下示例展示了如何从 Health Connect 获取累计的统计数据。

使用 healthConnectClient.aggregate 发送 AggregateRequest。在汇总请求中,指定一组汇总指标以及您希望获取的时间范围。例如,ExerciseSessionRecord.EXERCISE_DURATION_TOTALStepsRecord.COUNT_TOTAL 提供累计数据,而 WeightRecord.WEIGHT_AVGHeartRateRecord.BPM_MAXHeartRateRecord.BPM_MIN 提供统计数据。

    /**
     * TODO: Returns the weekly average of [WeightRecord]s.
     */
    suspend fun computeWeeklyAverage(start: Instant, end: Instant): Mass? {
        val request = AggregateRequest(
            metrics = setOf(WeightRecord.WEIGHT_AVG),
            timeRangeFilter = TimeRangeFilter.between(start, end)
        )
        val response = healthConnectClient.aggregate(request)
        return response[WeightRecord.WEIGHT_AVG]
    }

以下示例展示了如何获取特定锻炼时段的关联汇总数据。首先,使用带有 uidhealthConnectClient.readRecord 读取一条记录。然后,将锻炼时段的 startTimeendTime 作为时间范围,将 dataOrigin 作为过滤条件,读取关联汇总数据。

  /**
   * TODO: Reads aggregated data and raw data for selected data types, for a given [ExerciseSessionRecord].
   */
  suspend fun readAssociatedSessionData(
      uid: String,
  ): ExerciseSessionData {
    val exerciseSession = healthConnectClient.readRecord(ExerciseSessionRecord::class, uid)
    // Use the start time and end time from the session, for reading raw and aggregate data.
    val timeRangeFilter = TimeRangeFilter.between(
      startTime = exerciseSession.record.startTime,
      endTime = exerciseSession.record.endTime
    )
    val aggregateDataTypes = setOf(
      ExerciseSessionRecord.EXERCISE_DURATION_TOTAL,
      StepsRecord.COUNT_TOTAL,
      TotalCaloriesBurnedRecord.ENERGY_TOTAL,
      HeartRateRecord.BPM_AVG,
      HeartRateRecord.BPM_MAX,
      HeartRateRecord.BPM_MIN,
    )
    // Limit the data read to just the application that wrote the session. This may or may not
    // be desirable depending on the use case: In some cases, it may be useful to combine with
    // data written by other apps.
    val dataOriginFilter = setOf(exerciseSession.record.metadata.dataOrigin)
    val aggregateRequest = AggregateRequest(
      metrics = aggregateDataTypes,
      timeRangeFilter = timeRangeFilter,
      dataOriginFilter = dataOriginFilter
    )
    val aggregateData = healthConnectClient.aggregate(aggregateRequest)
    val heartRateData = readData<HeartRateRecord>(timeRangeFilter, dataOriginFilter)

    return ExerciseSessionData(
      uid = uid,
      totalActiveTime = aggregateData[ExerciseSessionRecord.EXERCISE_DURATION_TOTAL],
      totalSteps = aggregateData[StepsRecord.COUNT_TOTAL],
      totalEnergyBurned = aggregateData[TotalCaloriesBurnedRecord.ENERGY_TOTAL],
      minHeartRate = aggregateData[HeartRateRecord.BPM_MIN],
      maxHeartRate = aggregateData[HeartRateRecord.BPM_MAX],
      avgHeartRate = aggregateData[HeartRateRecord.BPM_AVG],
      heartRateSeries = heartRateData,
    )
  }

现在,我们运行这个应用,看看您能否在 Record weight 屏幕上看到平均体重。此外,如要查看某个锻炼时段的详细数据,请打开 Exercise sessions 屏幕,然后选择某个锻炼时段记录。

af1fe646159d6a60.png

9. 恭喜

恭喜,您已成功构建您的第一个 Health Connect 集成式健康与健身应用。

该应用可以声明对应用中所用数据类型的权限以及请求用户权限,还可以从 Health Connect 数据存储中读取和写入数据。您还学习了如何使用 Health Connect Toolbox 在 Health Connect 数据存储中创建模拟数据,从而为应用开发提供支持。

现在,您已了解让您的健康与健身应用加入 Health Connect 生态系统所需的主要步骤。

深入阅读