Cronet 基础知识

1. 简介

1ee223bf9e1b75fb.png

上次更新日期:2022 年 5 月 6 日

Cronet 是以库的形式提供给 Android 应用使用的 Chromium 网络堆栈。Cronet 利用多种技术来减少延迟和提高网络请求吞吐量,以满足您的应用的运行需要。

很多每日用户量达到数百万的应用(如 YouTubeGoogle 应用Google 相册以及 Google 地图 - 导航和公交)都由 Cronet 库来处理请求。Cronet 支持 HTTP3,是使用极广的 Android 网络库。

如需了解详情,请参阅 Cronet 功能页面。

您将构建的内容

在此 Codelab 中,您将为一款图片显示应用添加 Cronet 支持。您的应用将:

  • 从 Google Play 服务加载 Cronet;如果 Cronet 不可用,则安全地回退。
  • 使用 Cronet 发送请求以及接收和处理响应。
  • 在简单的界面中显示结果。

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(大约 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。与其他一些库(如 Volley)不同,Cronet 并不会自动读取整个正文。因此,请使用 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() 方法即可。此方法接受网址、回调类的实例以及运行回调方法的执行器。

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

我们可通过 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()

运行应用并添加一些图片。对于重复添加的图片,延迟时间明显较短,界面提示系统已缓存这些图片。

此功能可按请求进行替换。我们在 Cronet 下载器中稍作自定义,为“Sun”(太阳)图片(网址列表中的第一个)停用缓存。

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

request.build().start()

现在,再次运行该应用。您会发现太阳图片没有缓存。

d9d0163c96049081.png

12. 总结

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

如需详细了解 Cronet,请查看开发者指南源代码。此外,您可以订阅 Android 开发者博客,抢先了解 Cronet 以及 Android 的一般资讯。