同期処理と非同期処理
同期処理
同じスレッド上で行われる処理。
同期処理のメソッドを呼び出した場合、
呼び出したメソッドの処理が終わるまで呼び出し元メソッドは待機状態となる。
Kotlin/Javaでは、基本的にメイン処理が同期処理で行われる。
スレッド
処理の一連の流れの単位。
Androidアプリでは、アクティビティを処理するUIスレッドと非同期処理を行うワーカースレッドに分類される。
UIスレッド
アクティビティが実行されるメインスレッド。
ワーカースレッド
UIスレッドとは異なるスレッドで非同期処理を行うスレッド。
非同期処理
異なるスレッド上で行われるマルチスレッド処理。
非同期処理のメソッドを呼び出した場合、
呼び出したメソッドの処理の終了を待たずに呼び出し元メソッドは続きの処理を実行するため、
時間がかかる処理は非同期処理で行うのが一般的。
Executorを利用した非同期処理
参考1: バックグラウンドスレッドでのAndroidタスクの実行
参考2: ExecutorService
参考3: Thread, Looper, Handler
Executorを用いた非同期処理を実装する手順は、以下の通り。
Executorsクラスメソッドを用いてワーカースレッドを生成し、スレッドプールに追加ワーカースレッドでの処理を終えた後にUIスレッドで処理を続行する場合は、HandlerCompatクラスメソッドを用いてHandlerオブジェクトを生成Runnableインタフェースを実装したクラスを作成し、run()メソッドをオーバーライドして自動的に実行される処理を記述スレッドプールに、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: スレッド数
サンプルコード
// 1スレッド追加したスレッドプールをもつExecutorServiceプロパティ
val executeService = Executors.newSingleThreadExecutor()
Handlerオブジェクトの生成
定義
HandlerCompat.createAsync(@NonNull looper: Looper): Handler
// パラメータ
// looper: HandlerにバインドするLooper
サンプルコード
// 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)
}
}
// ワーカースレッドでの処理後に、
// UIスレッドで動作するRunnableオブジェクトを定義するクラス
private inner class WeatherInfoPostExecutor(result: String) : Runnable {
...
// Handlerによって自動的に実行される処理
// -> UIスレッドで動作することを明示的に記述(@UiThreadアノテーション)
@UiThread
override fun run() {
... // 処理内容
}
}
スレッドプールに処理を送信
定義
ExecutorService.submit(task: Runnable): Future<?>
// パラメータ
// task: スレッドプールに送信するRunnableオブジェクト
サンプルコード
// 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接続を行う手順は、以下の通り。
AndroidManifest.xmlに、HTTP接続を許可するパーミッションタグを記述接続先URLを定義したURLオブジェクトをHttpURLConnectionオブジェクトに変換HTTP接続に関する設定を記述し、HTTP接続を実行InputStream型のレスポンスデータをString型に変換JSONデータに変換したレスポンスデータの解析(=JSON解析)HttpURLConnectionオブジェクトを解放
マニフェストファイルでHTTP接続を許可
参考1: マニフェストファイルの概要
参考2: ネットワークへの接続
参考3: 研修1日目
アプリの基本情報を記述するマニフェストファイルに、HTTP接続を許可するパーミッションを記述する。
サンプルコード
<?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
サンプルコード
// 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: 接続エラーに関する例外
サンプルコード
// 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データであるため、InputStreamReaderやBufferedReaderを用いて、
文字データとして読み込んでからString型に変換する必要がある。
また、String型に変換した後はInputStreamReaderをラップしたBufferedReaderオブジェクトや、InputStreamオブジェクトを解放することでリソース効率を向上させる。
InputStream
読み込み専用のByteデータを抽象的に定義するクラス。
InputStreamReader
InputStream型のByteデータを読み込み、様々な文字コードに対応しながら文字データに変換し、
変換した文字データをオブジェクトとして保持できるクラス。
BufferedReader
バッファリングを行うことで読み込みの高速化を実現しながら、
読み込んだデータをオブジェクトとして保持できるクラス。
定義
// InputStreamオブジェクトの解放
InputStream.close()
サンプルコード(InputStream→Stringへの変換)
// 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オブジェクトの解放)
// 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によって記述された、階層構造をもつデータ。
// 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型に変換したリクエストパラメータを送信する。
サンプルコード
// 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()
}