LoginSignup
4
3

More than 1 year has passed since last update.

【Kotlin研修8日目】Executorを利用した非同期処理の実装、Web APIを利用したデータ取得およびJSON解析

Last updated at Posted at 2021-06-11

同期処理と非同期処理

同期処理

同じスレッド上で行われる処理。

同期処理のメソッドを呼び出した場合、
呼び出したメソッドの処理が終わるまで呼び出し元メソッド待機状態となる。

Kotlin/Javaでは、基本的にメイン処理が同期処理で行われる。

スレッド

処理の一連の流れの単位
Androidアプリでは、アクティビティを処理するUIスレッド非同期処理を行うワーカースレッドに分類される。

UIスレッド

アクティビティが実行されるメインスレッド

ワーカースレッド

UIスレッドとは異なるスレッドで非同期処理を行うスレッド

非同期処理

異なるスレッド上で行われるマルチスレッド処理

非同期処理のメソッドを呼び出した場合、
呼び出したメソッドの処理の終了を待たずに呼び出し元メソッドは続きの処理を実行するため、
時間がかかる処理は非同期処理で行うのが一般的。


Executorを利用した非同期処理

参考1: バックグラウンドスレッドでのAndroidタスクの実行
参考2: ExecutorService
参考3: Thread, Looper, Handler
Executorを用いた非同期処理を実装する手順は、以下の通り。

  1. Executorsクラスメソッドを用いてワーカースレッドを生成し、スレッドプールに追加
  2. ワーカースレッドでの処理を終えた後にUIスレッドで処理を続行する場合は、HandlerCompatクラスメソッドを用いてHandlerオブジェクトを生成
  3. Runnableインタフェースを実装したクラスを作成し、run()メソッドをオーバーライドして自動的に実行される処理を記述
  4. スレッドプールに、3.で作成したクラスのオブジェクト(=Runnableオブジェクト)を送信

Executorインタフェース

ワーカースレッドの生成やワーカースレッドでの処理を実行するメソッドを抽象的に定義したインタフェース

Executorsクラス

ExecutorServiceファクトリクラス

ExecutorServiceインタフェース

Executorインタフェースの抽象メソッドを拡張しており、
スレッドプール作成スレッドプールへのRunnableタスク送信など、
スレッドプールを管理するメソッドを抽象的に定義したインタフェース

スレッドプール

複数のスレッドを待機させ、処理するRunnableオブジェクト(=Runnableタスク)が到着すると、
待機状態のスレッドRunnableタスクを割り当て、処理を自動的に開始させる仕組み。

ファクトリ(クラス)

インスタンス(=オブジェクト)の生成」を目的に作られたクラス

Runnableインタフェース

特定のスレッド内での処理を自動的に実行するrun()メソッドを抽象的に定義したインタフェース

Handler

Looperオブジェクトを保持し、Looperから命令を受けるとメッセージキュー内の処理を実行するクラス。

Looperオブジェクトを保持していることから、
実質的に特定のスレッド内での処理を管理実行する。

UIスレッドで作成したHandlerオブジェクトをワーカースレッドRunnableオブジェクトに渡し、
ワーカースレッド側でUIスレッドで動作させる処理(=Runnableオブジェクト)を、
HandlerがもつLooperオブジェクトに送信(=post())してもらうことで、
ワーカースレッドでの処理を終えた後、UIスレッドでの処理続行が可能になる。

このことから、Handlerは「スレッド間の通信を行うクラス」とも言われる。

Looper

特定のスレッド内での処理(=Runnableオブジェクト)を先入先出法で管理し、
監視しているメッセージキューに処理がある限り、Handlerに実行するよう命令するクラス。


Executorを利用した非同期処理の実装

スレッドプールへのスレッド追加

定義

// 1スレッドの追加
Executors.newSingleThreadExecutor(): ExecutorService

// 指定したスレッド数のスレッドプールに変更
Executors.newFixedThreadPool(nThreads: Int): ExecutorService
// パラメータ
// nThreads: スレッド数

// 再利用可能なスレッドを必要数に応じて追加
Executors.newCachedThreadPool(): ExecutorService

// 指定したスレッド数をもち指定時間おきに処理するスレッドプールを追加
Executors.newScheduledThreadPool(corePoolSize: Int): ScheduledExecutorService
// パラメータ
// corePoolSize: スレッド数

サンプルコード

MainActivity.kt
// 1スレッド追加したスレッドプールをもつExecutorServiceプロパティ
val executeService = Executors.newSingleThreadExecutor()

Handlerオブジェクトの生成

定義

HandlerCompat.createAsync(@NonNull looper: Looper): Handler
// パラメータ
// looper: HandlerにバインドするLooper

サンプルコード

MainActivity.kt
// UIスレッドで最初に実行する処理
// -> UIスレッドで動作することを明示的に記述(@UiThread)
@UiThread
private fun receiveWeatherInfo(urlFull: String) {
    // UIスレッドで動作する処理を管理するHandlerオブジェクトの生成
    // mainLooper: アクティビティ(=UIスレッド)がもつLooperオブジェクト
    val handler = HandlerCompat.createAsync(mainLooper)
    ...
}

Runnableオブジェクトの定義

Runnableインタフェースは「スレッドプールHandlerによって自動実行される処理」を抽象的に定義したrun()メソッドをもつため、
Runnableインタフェースを実装するクラスではオーバーライドして処理を記述する。
その際、動作するスレッドアノテーションで記述することで、指定したスレッドでの動作が保証される。

また、UIスレッドからHandlerオブジェクトを受け取る場合は、コンストラクタに組み込む必要がある。

サンプルコード

// 非同期処理を行うRunnableオブジェクトを定義するクラス
private inner class WeatherInfoBackgroundReceiver(handler: Handler, url: String): Runnable {
    // クラス内で扱うHandlerオブジェクト(=Handlerプロパティ)
    // -> クラス内での書き換えが発生しないよう、スレッドセーフ(読み込み専用のval)で定義
    private val _handler = handler
    ...
    // スレッドプールによって自動的に実行する処理
    // -> ワーカースレッドで動作することを明示的に記述(@WorkerThreadアノテーション)
    @WorkerThread
    override fun run() {

        ...   // 処理内容

        // 非同期処理の終了後にUIスレッドで行う処理(Runnableオブジェクト)
        val postExecutor = WeatherInfoPostExecutor(result)

        // UIスレッドでの続行処理をUIスレッドHandlerのLooperに送信
        _handler.post(postExecutor)
    }
}
MainActivity.kt
// ワーカースレッドでの処理後に、
// UIスレッドで動作するRunnableオブジェクトを定義するクラス
private inner class WeatherInfoPostExecutor(result: String) : Runnable {
    ...
    // Handlerによって自動的に実行される処理
    // -> UIスレッドで動作することを明示的に記述(@UiThreadアノテーション)
    @UiThread
    override fun run() {
        ...   // 処理内容
    }
}

スレッドプールに処理を送信

定義

ExecutorService.submit(task: Runnable): Future<?>
// パラメータ
// task: スレッドプールに送信するRunnableオブジェクト

サンプルコード

MainActivity.kt
// UIスレッドで最初に実行する処理
// -> UIスレッドで動作することを明示的に記述(@UiThread)
@UiThread
private fun receiveWeatherInfo(urlFull: String) {
    // スレッド数2のスレッドプールを管理するExecutorServiceプロパティ
    val executeService = Executors.newSingleThreadExecutor()

    // UIスレッドで動作する処理を管理するHandlerオブジェクトの生成
    // mainLooper: アクティビティ(=UIスレッド)がもつLooperオブジェクト
    val handler = HandlerCompat.createAsync(mainLooper)

    // ワーカースレッドで処理するRunnableオブジェクト
    val backgroundReceiver = WeatherInfoBackgroundReceiver(handler, urlFull)

    // スレッドプールにRunnableオブジェクトを送信
    // -> 非同期処理の開始
    executeService.submit(backgroundReceiver)
}

AndroidのWeb連携

Android端末からインターネット上のデータベースにアクセスする場合、
DBと直接データのやり取りを行うサーバーサイドWebアプリ(=Web API)を通じてDBにアクセスする。

Web APIを利用したデータ取得

Web APIにアクセスするためには、APIを利用するためのAPIキーが必要となる。
また、Web APIの利用にあたってHttpURLConnectionクラスを用いてHTTP接続を行う必要がある。

HTTP接続を行う手順は、以下の通り。

  1. AndroidManifest.xmlに、HTTP接続を許可するパーミッションタグを記述
  2. 接続先URLを定義したURLオブジェクトをHttpURLConnectionオブジェクトに変換
  3. HTTP接続に関する設定を記述し、HTTP接続を実行
  4. InputStream型のレスポンスデータString型に変換
  5. JSONデータに変換したレスポンスデータの解析(=JSON解析)
  6. HttpURLConnectionオブジェクトを解放

マニフェストファイルでHTTP接続を許可

参考1: マニフェストファイルの概要
参考2: ネットワークへの接続
参考3: 研修1日目
アプリの基本情報を記述するマニフェストファイルに、HTTP接続を許可するパーミッションを記述する。

サンプルコード

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest ...>
    <uses-permission
        android:name="android.permission.INTERNET"
    />

    <uses-permission
        android:name="android.permission.ACCESS_NETWORK_STATE"
    />
    ...
</manifest>

URLオブジェクトをHttpURLConnectionオブジェクトに変換

接続先URLの文字列から生成したURLオブジェクトを、HttpURLConnectionオブジェクトに変換する。

HttpURLConnection

HTTP接続に関する設定値(=プロパティ)をもち、HTTP接続を実行するメソッドを定義するクラス

定義

URL.openConnection(): URLConnection

サンプルコード

MainActivity.kt
// String型 → URL型への変換
val url = URL("<URL文字列>")

// URL型 → URLConnection型 → HttpURLConnection型への変換
val con = url.openConnection() as? HttpURLConnection

HTTP接続設定の定義・実行

HTTP接続に関する設定

(Http)URLConnectionの主なプロパティ

プロパティ 内容
connectTimeout HTTP接続がタイムアウトするまでの時間[ms]
readTimeout データ取得がタイムアウトするまでの時間[ms]
requestMethod HTTPリクエストのメソッド
responseCode HTTPステータスコード

HTTP接続の実行

HTTP接続がうまくできなかった場合は例外が発生するため、例外処理を用いて記述する必要がある。

定義

URLConnection.connect()
// Throws:
// SocketTimeoutException: タイムアウトに関する例外
// IOException: 接続エラーに関する例外

サンプルコード

MainActivity.kt
// URLオブジェクト
val url = URL("<URL文字列>")

// HttpURLConnectionオブジェクト
val con = url.openConnection() as? HttpURLConnection

// HttpURLConnectionオブジェクトがnullでないことを保証
// -> let関数ブロック内では、nullチェック対象(=con)がitで置換
con?.let {

    // 例外が発生する可能性があるブロック
    try {
    // -- HTTP接続設定の定義開始 --
        // 接続がタイムアウトするまでの時間[ミリ秒]
        // -> タイムアウトした場合はSocketTimeoutExceptionが発生
        it.connectTimeout = 1000

        // データ取得がタイムアウトするまでの時間[ミリ秒]
        // -> タイムアウトした場合はSocketTimeoutExceptionが発生
        it.readTimeout = 1000

        // HTTP接続メソッドの指定
        it.requestMethod = "GET"
    // -- HTTP接続設定の定義終了 --

        // HTTP接続の実行
        // ->場合によってSocketTimeoutExceptionまたはIOExceptionが発生
        // -> 接続時、HttpURLConnectionオブジェクトのinputStreamプロパティに
        //    InputStream型のレスポンスデータが自動的に格納
        it.connect()
    }
    // 例外が発生した場合のエラー処理
    catch (ex: SocketTimeoutException) {
        ...   // Logcatにログを出力
    }
}

InputStream型→String型の変換

InputStream型のレスポンスデータは、Byteデータであるため、InputStreamReaderBufferedReaderを用いて、
文字データとして読み込んでからString型に変換する必要がある。

また、String型に変換した後はInputStreamReaderラップしたBufferedReaderオブジェクトや、InputStreamオブジェクトを解放することでリソース効率を向上させる。

InputStream

読み込み専用Byteデータを抽象的に定義するクラス。

InputStreamReader

InputStream型のByteデータを読み込み、様々な文字コードに対応しながら文字データに変換し、
変換した文字データをオブジェクトとして保持できるクラス。

BufferedReader

バッファリングを行うことで読み込みの高速化を実現しながら、
読み込んだデータをオブジェクトとして保持できるクラス。

定義

// InputStreamオブジェクトの解放
InputStream.close()

サンプルコード(InputStream→Stringへの変換)

MainActivity.kt
// InputStream型 → String型 への変換処理
private fun is2String(stream: InputStream?): String {

    // 1行分の文字データを格納するStringBuilderオブジェクト
    // StringBuilder: 文字列を格納する(同一ポインタ内での)可変配列を定義するクラス
    val sb = StringBuilder()

    // Byteデータ → 文字データ への変換
    val reader = BufferedReader(InputStreamReader(stream, "UTF-8"))

    // 1行分の文字データ
    // -> 1行目の読み込み
    var line = reader.readLine()

    // 最終行までループさせる処理
    while (line != null) {

        // 読み込んだ1行分の文字データをStringBuilderオブジェクトに格納
        sb.append(line)

        // 次の行を読み込む
        line = reader.readLine()
    }

    // BufferedReaderオブジェクトの解放
    reader.close()

    // 読み込んだ文字データ(StringBuilder型)をString型に変換
    return sb.toString()
}

サンプルコード(InputStreamオブジェクトの解放)

MainActivity.kt
// HttpURLConnectionオブジェクトによるレスポンスデータの取得
val stream = <HttpURLConnectionオブジェクト>.inputStream

// InputStream型のレスポンスデータをString型に変換
result = is2String(stream)

// InputStreamオブジェクトの解放
stream.close()

JSON解析によるデータ取得

HTTP接続時にHttpURLConnectionオブジェクトに格納されたByteデータ(=InputStream)の中身は、
JSON(JavaScript Object Notation)で記述されたJSONデータであるため、
JSON解析によって取得するデータを含むマップを抽出し、
最終的に、get<T>()メソッドを用いてデータを取得する。

JSONデータ

キー(=name)とを保持するマップが、データ記述言語であるJSONによって記述された、階層構造をもつデータ。

String型に変換したJSON文字列
// JSONObject: {...}で囲まれたキー付きのマップ
{
    "<name>":{
        "<name>":<value>,
        "<name>":<value>,
    },
// JSONArray: [...]で囲まれたキー付きのリスト
    "<name>":[
        {
            "<name>":<value>,
            "<name>":<value>,
        },
    ],
    ...
    "<name>":<value>,
    "<name>":<value>
}

定義

/*
    いずれのメソッドも、条件に一致するJSONデータが見つからない場合、
    JSONExceptionが発生
*/

// JSONObjectの生成
val rootJSON = JSONObject(json: String): JSONObject
// パラメータ
// json: JSONによって記述された文字列

// JSONArrayの取得
rootJSON.getJSONArray(name: String): JSONArray
// name: JSONArrayのキー

// JSONObjectに含まれるJSONObjectの取得
rootJSON.getJSONObject(name: String): JSONObject
// name: JSONObjectのキー

// データの取得
rootJSON.get<T>(name: String): <T>
// name: マップのキー

WebAPIを利用したデータ格納

Web APIを通じてDBデータ更新処理を行う場合は、
HttpURLConnectionオブジェクトがもつOutputStreamオブジェクト(=outputStreamプロパティ)を通じて、ByteArray型に変換したリクエストパラメータを送信する。

サンプルコード

MainActivity.kt
// URLオブジェクト
val url = URL("<URL文字列>")

// HttpURLConnectionオブジェクト
val con = url.openConnection() as? HttpURLConnection

// 送信するリクエストパラメータ文字列
val postData = "name=${name}&comment=${comment}"

// HttpURLConnectionオブジェクトがnullでないことを保証
// -> let関数ブロック内では、nullチェック対象(=con)がitで置換
con?.let {

    // 例外が発生する可能性があるブロック
    try {
    // -- HTTP接続設定の定義開始 --
        // 接続がタイムアウトするまでの時間[ミリ秒]
        // -> タイムアウトした場合はSocketTimeoutExceptionが発生
        it.connectTimeout = 1000

        // データ取得がタイムアウトするまでの時間[ミリ秒]
        // -> タイムアウトした場合はSocketTimeoutExceptionが発生
        it.readTimeout = 1000

        // HTTP接続メソッドの指定
        it.requestMethod = "POST"

        // リクエストパラメータの出力を可能にする
        it.doOutput = true
    // -- HTTP接続設定の定義終了 --

        // HTTP接続の実行
        // ->場合によってSocketTimeoutExceptionまたはIOExceptionが発生
        // -> 接続時、HttpURLConnectionオブジェクトのinputStreamプロパティに
        //    InputStream型のレスポンスデータが自動的に格納
        it.connect()

        // OutputStreamオブジェクト
        val outStream = con.outputStream

        // String型のリクエストパラメータをByteArray型に変換し、
        // OutputStreamオブジェクトに書き込む
        // -> 出力エラー時はIOExceptionが発生
        outStream.write(postData.toByteArray())

        // バッファリングされたリクエストパラメータを
        // OutputStreamオブジェクトに強制的に書き込む
        // -> 基本的にはwrite()で書き込みが完了しているが、
        //    万が一に備えてflush()メソッドを記述
        // -> 出力エラー時はIOExceptionが発生
        outStream.flush()

        // OutputStreamオブジェクトの解放
        // -> 実行エラー時はIOExceptionが発生
        outStream.close()
    }
    catch {
        ...   // エラー処理を記述
    }

    // HttpURLConnectionオブジェクトの解放
    it.disconnect()
}
4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3