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)

这将启动一个 Google 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 加载失败,我们将继续使用原生图像下载器。如果网络性能对您的应用至关重要,您可能需要安装或更新 Google 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下载器中添加一个小技巧,并为Sun图像(URL列表中的第一个)禁用缓存。

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

request.build().start()

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

d9d0163c96049081.png

12. 结论

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

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