Cronet の基本

1. はじめに

1ee223bf9e1b75fb.png

最終更新日: 2022 年 5 月 6 日

Cronet は Android アプリがライブラリとして利用できる Chromium ネットワーク スタックです。Cronet は、アプリの動作に必要なネットワーク リクエストのレイテンシを抑え、スループットを向上させる複数のテクノロジーを利用します。

Cronet Library は、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 ライブラリの基本を理解する必要があります。

必要なもの

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 チームは、Google Play 開発者サービス プロバイダの利用をおすすめします。Google Play 開発者サービス プロバイダを使用することで、アプリのバイナリサイズを Cronet の分だけ縮小でき(約 5 メガバイト)、プラットフォームにより最新のアップデートとセキュリティ修正が確実に配信されます。

実装をインポートする方法に関係なく、cronet-api 依存関係を追加して Cronet API を追加する必要があります。

build.gradle ファイルを開き、次の 2 行を 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)
   }
}

この Codelab の趣旨に従って、Cronet の読み込みが失敗した場合には、ネイティブの画像ダウンローダを使用して次に進みます。ネットワークのパフォーマンスがアプリにとって重要である場合は、Play 開発者サービスをインストールするか更新することをおすすめします。詳細については、CronetProviderInstaller に関するドキュメントをご覧ください。

ここでアプリを実行します。すべて正常に動作すると、プロバイダが正常にインストールされたことを示すログメッセージが表示されます。

7. Cronet エンジンを作成する

Cronet エンジンとは、Cronet でリクエストを送信するために使用するコア オブジェクトのことです。エンジンは Builder パターンを使用して構築され、これにはさまざまな 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 にはダイレクト バイトバッファを使用する必要があります。この 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 ベースの画像ダウンローダを実装しましょう。

ImageDownloader インターフェースを実装し、コンストラクタ パラメータとして CronetEngine を受け取る CronetImageDownloader という新しいクラスを作成します。

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)

この URL は 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 ダウンローダに少し手を加え、URL リストの最初にある太陽の画像のキャッシュを無効にします。

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

request.build().start()

アプリを再度実行します。太陽の画像はキャッシュに保存されていないはずです。

d9d0163c96049081.png

12. まとめ

お疲れさまでした。これでこの Codelab は終了です。今回は Cronet の基本的な使用方法を学習しました。

Cronet について詳しくは、デベロッパー ガイドソースコードをご覧ください。また、Android デベロッパー ブログに登録して、Cronet や Android 一般のニュースをいち早くキャッチしましょう。