Cronet 基本概念

1. 簡介

1ee223bf9e1b75fb.png

上次更新時間:2022 年 5 月 6 日

Cronet 是 Chromium 網路堆疊,可做為供 Android 應用程式使用的程式庫。Cronet 利用多項技術縮短延遲時間,並提高應用程式運作所需的網路要求總處理量。

Cronet 程式庫可處理數以千萬計的使用者每天使用的應用程式,例如:YouTubeGoogle 應用程式Google 相簿Google 地圖 - 導航和大眾運輸。Cronet 是最常用以支援 HTTP3 的 Android 網路程式庫。

詳情請參閱 Cronet 功能頁面。

建構項目

在本程式碼研究室中,您將在圖片顯示應用程式中新增 Cronet 支援功能。您的應用程式將會:

  • 從 Google Play 服務載入 Cronet;如果 Cronet 無法使用,則可以安全復原。
  • 使用 Cronet 傳送要求,以及接收和處理回應。
  • 以簡潔的 UI 顯示結果。

28b0fcb0fed5d3e0.png

課程內容

  • 如何在應用程式中加入 Cronet 為依附元件
  • 如何設定 Cronet 引擎
  • 如何使用 Cronet 傳送要求
  • 如何編寫 Cronet 回呼以處理回應

本次的程式碼研究室著重於使用 Cronet。應用程式的大部分內容均已預先實作,因此即使您沒有多少 Android 開發經驗,也能完成本程式碼研究室的課程。話雖如此,為了能充分利用本次程式碼研究室,建議您瞭解 Android 開發Jetpack Compose 程式庫的基本概念。

軟硬體需求

2. 取得程式碼

我們已將本專案所需的資料都放到 Git 存放區中。如要開始使用,請複製存放區,然後在 Android Studio 中開啟程式碼。

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

3. 建立基準

從哪裡開始?

我們要從專為本次程式碼研究室所設計的基本圖片顯示應用程式開始。按一下「Add an image」按鈕,會看到清單中加入一張新圖片,以及從網際網路擷取圖片所需時間的詳細資料。應用程式使用 Kotlin 提供的內建 HTTP 程式庫,且不支援任何進階功能。

在本程式碼研究室的課程中,我們將擴充應用程式,以便使用 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 服務工作,並以非同步的方式安裝提供商。

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)
   }
}

以本次程式碼研究室為例,如果 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 才能使用直接位元組緩衝區。雖然緩衝區的容量對於本次程式碼研究室而言無關緊要,但對於大多數生產用途而言,適合將預設值設為 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()

執行應用程式並加入幾張圖片。就重複添加的圖片而言,延遲時間應該會大幅縮短,且 UI 也會顯示已快取這些圖片。

您可以根據個別要求覆寫此功能。現在我們要在 Cronet 下載工具中略為調整設定,然後為網址清單中第一張圖片「Sun」停用快取。

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

request.build().start()

現在,請再次執行應用程式。請注意,現在應用程式就不會快取太陽的圖片。

d9d0163c96049081.png

12. 結論

恭喜,您已經順利完成本程式碼研究室課程!在這個過程中,您已瞭解 Cronet 的基本使用概念。

如要進一步瞭解 Cronet,請參閱開發人員指南原始碼。此外,您也可以訂閱 Android 開發人員網誌,搶先掌握 Cronet 消息和 Android 的一般最新資訊。