1. 简介
上次更新 2022-05-06
Cronet 是 Chromium 网络堆栈,作为库提供给 Android 应用。Cronet 利用多种技术来降低延迟并提高应用所需网络请求的吞吐量。
Cronet 库处理每天数百万用户使用的应用的请求,例如 YouTube、Google 应用、Google 相册 和 地图 - 导航与交通。Cronet 是使用最广泛的具有 HTTP3 支持的 Android 网络库。
有关更多详细信息,请参阅 Cronet 功能 页面。
您将构建的内容
在这个 codelab 中,您将向图像显示应用添加 Cronet 支持。您的应用将
- 从 Google Play 服务加载 Cronet,如果 Cronet 不可用,则安全回退。
- 使用 Cronet 发送请求并接收和处理响应。
- 在一个简单的 UI 中显示结果。
您将学习的内容
- 如何将 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") } }
onRedirectReceived
、onSucceeded
和onFailed
方法不言自明,我们现在不详细介绍,我们将重点关注onResponseStarted
和onReadCompleted
。
Cronet发送请求并接收所有响应头后,但在开始读取正文之前,会调用onResponseStarted
。Cronet不会像其他一些库(例如Volley)那样自动读取整个正文。相反,使用UrlRequest.read() 将正文的下一块数据读取到你提供的缓冲区中。当Cronet完成响应正文块的读取时,它将调用onReadCompleted
方法。这个过程会重复进行,直到没有更多数据可读。
让我们开始实现读取循环。首先,实例化一个新的字节数组输出流和一个使用它的通道。我们将使用该通道作为响应正文的接收器。
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() }
最后,我们需要处理onSucceeded
和onFailed
方法。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请求。这很简单——调用你的CronetEngine
的newUrlRequestBuilder()
方法。此方法接收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.Builder 和CronetEngine.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图像没有被缓存。
12. 结论
恭喜你,你已经完成了代码实验室!在此过程中,你学习了如何使用Cronet的基础知识。
要了解有关Cronet的更多信息,请查看开发者指南 和源代码。此外,请订阅Android开发者博客,以便最先了解Cronet和一般的Android新闻。