1. 简介
上次更新日期:2022 年 5 月 6 日
Cronet 是以库的形式提供给 Android 应用使用的 Chromium 网络堆栈。Cronet 利用多种技术来减少延迟和提高网络请求吞吐量,以满足您的应用的运行需要。
很多每日用户量达到数百万的应用(如 YouTube、Google 应用、Google 相册以及 Google 地图 - 导航和公交)都由 Cronet 库来处理请求。Cronet 支持 HTTP3,是使用极广的 Android 网络库。
如需了解详情,请参阅 Cronet 功能页面。
您将构建的内容
在此 Codelab 中,您将为一款图片显示应用添加 Cronet 支持。您的应用将:
- 从 Google Play 服务加载 Cronet;如果 Cronet 不可用,则安全地回退。
- 使用 Cronet 发送请求以及接收和处理响应。
- 在简单的界面中显示结果。
学习内容
- 如何将 Cronet 作为依赖项添加到应用中
- 如何配置 Cronet 引擎
- 如何使用 Cronet 发送请求
- 如何编写 Cronet 回调来处理响应
此 Codelab 将重点介绍如何使用 Cronet。这款应用的大部分内容已预先实现,因此,即使您之前没有多少 Android 开发经验,您也能完成此 Codelab。不过,为使您在此 Codelab 中获得更好的学习效果,您应了解 Android 开发和 Jetpack Compose 库的基础知识。
所需条件
- Android Studio 2021.1 (Bumblebee) 或更高版本
- 搭载 Android 10 或更高版本且装有 Google Play 服务的设备,或者搭载 Android 10 或更高版本且装有 Play 商店的模拟器
- Kotlin 基础知识
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") } }
onRedirectReceived
、onSucceeded
和 onFailed
方法均一目了然,因此我们暂不详细介绍,而要重点介绍 onResponseStarted
和 onReadCompleted
。
在 Cronet 发送请求并收到所有响应标头之后,但未开始读取正文之时,系统会调用 onResponseStarted
。与其他一些库(如 Volley)不同,Cronet 并不会自动读取整个正文。因此,请使用 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 使用直接字节缓冲区。在此 Codelab 的场景中,对缓冲区容量没有多大要求,不过对于大多数生产用例来说,默认的适宜容量为 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()
方法即可。此方法接受网址、回调类的实例以及运行回调方法的执行器。
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.Builder 和 CronetEngine.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()
现在,再次运行该应用。您会发现太阳图片没有缓存。
12. 总结
恭喜,您已完成了本 Codelab 的学习!在此过程中,您学习了关于如何使用 Cronet 的基础知识。
如需详细了解 Cronet,请查看开发者指南和源代码。此外,您可以订阅 Android 开发者博客,抢先了解 Cronet 以及 Android 的一般资讯。