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 库的基础知识。

您需要准备什么

2. 获取代码

我们已将本项目所需的所有内容放入 Git 仓库。要开始使用,请克隆仓库并在 Android Studio 中打开代码。

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

3. 建立基准

我们的起点是什么?

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

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

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

您可以将 Cronet 集成到您的应用中作为一个独立库,或者使用平台提供的 Cronet。Cronet 团队推荐使用 Google Play 服务提供商。通过使用 Google Play 服务提供商,您的应用无需承担携带 Cronet 的二进制文件大小成本(约 5MB),并且平台会确保交付最新的更新和安全补丁。

无论您决定如何导入实现,您还需要添加一个 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,该 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

onResponseStarted 在 Cronet 发送请求并接收所有响应头之后,但在开始读取正文之前调用。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。虽然缓冲区容量对于本 Codelab 的目的并不重要,但对于大多数生产用途来说,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)

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

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 下载器中添加一个小的修改,并禁用 Sun 图像的缓存,它是 URL 列表中的第一个。

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

request.build().start()

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

d9d0163c96049081.png

12. 结论

恭喜,您已完成本 Codelab!在此过程中,您学习了如何使用 Cronet 的基础知识。

要了解有关 Cronet 的更多信息,请查看开发者指南源代码。此外,订阅Android Developer 博客,第一时间了解 Cronet 及其他 Android 新闻。