Cronet 基础

1. 简介

1ee223bf9e1b75fb.png

上次更新 2022-05-06

Cronet 是 Chromium 网络栈,作为库提供给 Android 应用。Cronet 利用多种技术来减少应用工作所需的网络请求的延迟并提高其吞吐量。

Cronet 库处理每天数百万用户使用的应用的请求,例如 YouTubeGoogle 应用Google 相册地图 - 导航和交通。Cronet 是使用最广泛的具有 HTTP3 支持的 Android 网络库。

有关更多详细信息,请参阅 Cronet 功能 页面。

您将构建什么

在此 codelab 中,您将向图像显示应用添加 Cronet 支持。您的应用将

  • 从 Google Play 服务加载 Cronet,或者在 Cronet 不可用时安全回退。
  • 使用 Cronet 发送请求并接收和处理响应。
  • 在简单的 UI 中显示结果。

28b0fcb0fed5d3e0.png

您将学到什么

  • 如何将 Cronet 作为依赖项包含到您的应用中
  • 如何配置 Cronet 引擎
  • 如何使用 Cronet 发送请求
  • 如何编写 Cronet 回调以处理响应

此 codelab 侧重于使用 Cronet。大部分应用已预先实现,您只需具备很少的 Android 开发经验即可完成此 codelab。也就是说,为了充分利用此 codelab,您应该了解 Android 开发Jetpack Compose 库的基础知识。

您需要什么

  • Android Studio 2021.1 (Bumblebee) 或更高版本
  • 具有 Google Play 服务的 Android 10 或更高版本的设备,或具有 Play 商店的 Android 10+ 模拟器
  • Kotlin 的基本知识

2. 获取代码

我们将此项目所需的一切都放在一个 Git 存储库中。要开始,请克隆存储库并在 Android Studio 中打开代码。

git clone https://github.com/android/codelab-cronet-basics

3. 建立基线

我们的起点是什么?

我们的起点是一个为本 codelab 设计的基本图像显示应用。如果您点击**添加图像**按钮,您会看到一个新图像添加到列表中,以及从互联网获取图像所花费时间的详细信息。该应用使用 Kotlin 提供的内置 HTTP 库,该库不支持任何高级功能。

在本 codelab 的过程中,我们将扩展该应用以使用 Cronet 及其一些功能。

4. 将依赖项添加到您的 Gradle 脚本中

您可以将 Cronet 集成为您应用随附的独立库,也可以使用平台提供的 Cronet。Cronet 团队建议使用 Google Play 服务提供程序。通过使用 Google Play 服务提供程序,您的应用无需承担携带 Cronet 的二进制文件大小成本(约 5 MB),并且平台可确保交付最新的更新和安全修复程序。

无论您决定如何导入实现,您都需要添加 cronet-api 依赖项以包含 Cronet API。

打开您的 build.gradle 文件,并将以下两行添加到 dependencies 部分。

implementation 'com.google.android.gms:play-services-cronet:18.0.1'
implementation 'org.chromium.net:cronet-api:101.4951.41'

5. 安装 Google Play 服务 Cronet 提供程序

如上一节所述,Cronet 可以通过多种方式添加到您的应用中。每种方式都由一个 Provider 进行抽象,该提供程序可确保库和您的应用之间建立必要的链接。每次创建新的 Cronet 引擎时,Cronet 都会查看所有活动提供程序并选择最佳提供程序来实例化引擎。

Google Play 服务提供程序通常不可开箱即用,因此您需要先安装它。找到 MainActivity 中的 TODO 并粘贴以下代码段

val ctx = LocalContext.current
CronetProviderInstaller.installProvider(ctx)

这将启动一个 Play 服务 Task,该任务会异步安装提供程序。

6. 处理提供程序安装结果

您已成功安装提供程序...等等,您安装了吗?该 Task 是异步的,您尚未以任何方式处理结果。让我们解决这个问题。将 installProvider 调用替换为以下代码段

CronetProviderInstaller.installProvider(ctx).addOnCompleteListener {
   if (it.isSuccessful) {
       Log.i(LOGGER_TAG, "Successfully installed Play Services provider: $it")
       // TODO(you): Initialize Cronet engine
   } else {
       Log.w(LOGGER_TAG, "Unable to load Cronet from Play Services", it.exception)
   }
}

出于本 codelab 的目的,如果 Cronet 加载失败,我们将继续使用本机图像下载器。如果网络性能对您的应用至关重要,您可能需要安装或更新 Play 服务。有关更多详细信息,请参阅 CronetProviderInstaller 文档。

现在运行该应用;如果一切正常,您应该会看到一条日志语句,表明提供程序已成功安装。

7. 创建 Cronet 引擎

Cronet 引擎是您将用于使用 Cronet 发送请求的核心对象。引擎是使用 构建器模式 构造的,它允许您配置各种 Cronet 选项。目前,我们将继续使用默认选项。通过将 TODO 替换为以下代码段来实例化新的 Cronet 引擎

val cronetEngine = CronetEngine.Builder(ctx).build()
// TODO(you): Initialize the Cronet image downloader

8. 实现 Cronet 回调

Cronet 的异步特性意味着响应处理是使用回调控制的,即 UrlRequest.Callback 的实例。在本节中,您将实现一个辅助回调,该回调将整个响应读入内存。

创建一个名为 ReadToMemoryCronetCallback 的新抽象类,使其扩展 UrlRequest.Callback,并让 Android Studio 自动生成方法存根。您的新类应类似于以下代码段

abstract class ReadToMemoryCronetCallback : UrlRequest.Callback() {
   override fun onRedirectReceived(
       request: UrlRequest,
       info: UrlResponseInfo,
       newLocationUrl: String?
   ) {
       TODO("Not yet implemented")
   }

   override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
       TODO("Not yet implemented")
   }

   override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: CronetException) {
       TODO("Not yet implemented")
   }

   override fun onResponseStarted(request: UrlRequest, info: UrlResponseInfo) {
       TODO("Not yet implemented")
   }

   override fun onReadCompleted(
       request: UrlRequest,
       info: UrlResponseInfo,
       byteBuffer: ByteBuffer
   ) {
       TODO("Not yet implemented")
   }
}

onRedirectReceivedonSucceededonFailed 方法是不言自明的,因此我们现在不再详细介绍,我们将重点介绍 onResponseStartedonReadCompleted

在 Cronet 发送请求并接收所有响应头之后,但在开始读取正文之前,会调用onResponseStarted。Cronet 不会像某些其他库(例如 Volley)那样自动读取整个正文。相反,使用UrlRequest.read()将正文的下一块内容读取到您提供的缓冲区中。当 Cronet 完成读取响应正文块时,它会调用onReadCompleted方法。此过程会重复,直到没有更多数据可读。

39d71a5e85f151d8.png

让我们开始实现读取循环。首先,实例化一个新的字节数组输出流和一个使用它的通道。我们将使用该通道作为响应正文的接收器。

private val bytesReceived = ByteArrayOutputStream()
private val receiveChannel = Channels.newChannel(bytesReceived)

接下来,实现onReadCompleted方法,将数据从字节缓冲区复制到我们的接收器并调用下一个读取操作。

// The byte buffer we're getting in the callback hasn't been flipped for reading,
// so flip it so we can read the content.
byteBuffer.flip()
receiveChannel.write(byteBuffer)

// Reset the buffer to prepare it for the next read
byteBuffer.clear()

// Continue reading the request
request.read(byteBuffer)

要完成正文读取循环,请从onResponseStarted回调方法中调用初始读取。请注意,您需要对 Cronet 使用直接字节缓冲区。虽然缓冲区的容量对于代码实验室的目的无关紧要,但对于大多数生产用途,16 KiB 是一个良好的默认值。

request.read(ByteBuffer.allocateDirect(BYTE_BUFFER_CAPACITY_BYTES))

现在让我们完成类的其余部分。重定向对您来说并不重要,因此只需像您的网络浏览器一样遵循重定向即可。

override fun onRedirectReceived(
   request: UrlRequest, info: UrlResponseInfo?, newLocationUrl: String?
) {
   request.followRedirect()
}

最后,我们需要处理onSucceededonFailed方法。onFailed匹配您希望为您的帮助程序回调用户提供的签名,因此您可以删除定义并让扩展类覆盖该方法。onSucceeded应该将正文作为字节数组传递到下游。添加一个新的抽象方法,并在其签名中包含正文。

abstract fun onSucceeded(
   request: UrlRequest, info: UrlResponseInfo, bodyBytes: ByteArray)

然后,确保在请求成功完成时正确调用新的onSucceeded方法。

final override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
   val bodyBytes = bytesReceived.toByteArray()
   onSucceeded(request, info, bodyBytes)
}

太棒了,您已经学会了如何实现 Cronet 回调!

9. 实现图像下载器

让我们使用我们在上一节中创建的回调来实现一个基于 Cronet 的图像下载器。

创建一个名为CronetImageDownloader的新类,实现ImageDownloader接口并接受CronetEngine作为其构造函数参数。

class CronetImageDownloader(val engine: CronetEngine) : ImageDownloader {
   override suspend fun downloadImage(url: String): ImageDownloaderResult {
       TODO("Not yet implemented")
   }
}

要实现downloadImage方法,您需要学习如何创建 Cronet 请求。这很容易 - 调用CronetEnginenewUrlRequestBuilder()方法。此方法接收 url、回调类的实例以及运行回调方法的执行器。

val request = engine.newUrlRequestBuilder(url, callback, executor)

我们从downloadImage参数中知道 URL。对于执行器,我们将创建一个实例范围的字段。

private val executor = Executors.newSingleThreadExecutor()

最后,我们使用上一节中的帮助程序回调实现来实现callback。我们不会详细介绍其实现,因为这更多的是Kotlin 协程主题。您可以将cont.resume视为downloadImage方法的return

将所有内容放在一起,您的downloadImage实现应类似于以下代码段。

override suspend fun downloadImage(url: String): ImageDownloaderResult {
   val startNanoTime = System.nanoTime()
   return suspendCoroutine {
       cont ->
       val request = engine.newUrlRequestBuilder(url, object: ReadToMemoryCronetCallback() {
       override fun onSucceeded(
           request: UrlRequest,
           info: UrlResponseInfo,
           bodyBytes: ByteArray) {
           cont.resume(ImageDownloaderResult(
               successful = true,
               blob = bodyBytes,
               latency = Duration.ofNanos(System.nanoTime() - startNanoTime),
               wasCached = info.wasCached(),
               downloaderRef = this@CronetImageDownloader))
       }

       override fun onFailed(
           request: UrlRequest,
           info: UrlResponseInfo,
           error: CronetException
       ) {
           Log.w(LOGGER_TAG, "Cronet download failed!", error)
           cont.resume(ImageDownloaderResult(
               successful = false,
               blob = ByteArray(0),
               latency = Duration.ZERO,
               wasCached = info.wasCached(),
               downloaderRef = this@CronetImageDownloader))
       }
   }, executor)
       request.build().start()
   }
}

10. 最终连接

让我们回到MainDisplay可组合项并通过使用我们刚刚创建的图像下载器来解决最后一个 TODO。

imageDownloader = CronetImageDownloader(cronetEngine)

我们完成了!尝试运行应用程序。您应该会看到您的请求通过 Cronet 图像下载器路由。

11. 自定义

您可以根据请求级别和引擎级别自定义请求行为。我们将通过缓存演示这一点,但还有更多选项。有关详细信息,请参阅UrlRequest.BuilderCronetEngine.Builder文档。

要在引擎级别启用缓存,请使用构建器的enableHttpCache方法。在下面的示例中,我们使用内存缓存。有关其他可用选项,请参阅文档。然后创建 Cronet 引擎将变为

val cronetEngine = CronetEngine.Builder(ctx)
   .enableHttpCache(CronetEngine.Builder.HTTP_CACHE_IN_MEMORY, 10 * 1024 * 1024)
   .build()

运行应用程序并添加一些图像。重复添加的图像应具有明显更短的延迟,并且 UI 应指示它们已缓存。

此功能可以在每个请求的基础上被覆盖。让我们在我们的 Cronet 下载器中添加一个小技巧,并禁用太阳图像(URL 列表中的第一个)的缓存。

if (url == CronetCodelabConstants.URLS[0]) {
   request.disableCache()
}

request.build().start()

现在再次运行应用程序。您应该会注意到太阳图像没有被缓存。

d9d0163c96049081.png

12. 结论

恭喜您完成了代码实验室!在此过程中,您学习了如何使用 Cronet 的基础知识。

要了解有关 Cronet 的更多信息,请查看开发者指南源代码。此外,订阅Android 开发者博客,以便最先了解 Cronet 和一般 Android 新闻。