接続喪失 — モバイルアプリで最も一般的で苛立たしい現象の一つです。ユーザーはデータへのアクセスを失い、操作が中断され、アプリがフリーズしたりクラッシュしたりします。Google Android Developer Blogによると、アプリが2回クラッシュまたはフリーズすると、70%のユーザーがアプリを削除します。接続喪失の原因と、耐障害性のある通信を構築する方法を解説します。
重要なポイント
接続切断 — アプリがサーバーとの接続を失い、操作に応答しなくなったり、エラーで終了したりする状況を表すユーザー用語です。技術的には、ネットワークエラー(タイムアウト、DNS障害)、ANR(UIスレッドのフリーズ)、クラッシュ(未処理の例外)、またはレースコンディションが考えられます。
ユーザーの視点からは、これらのシナリオはすべて同じに見えます:アプリが動作を停止します。開発者にとっての違いは、診断と修正のアプローチにあります。ネットワークエラーはリトライメカニズムで解決され、ANRはUIスレッドから操作を移すことで、クラッシュは例外処理で対処します。
Crittercism(現Apteligent)によると、モバイルアプリはクラッシュのたびに平均1〜2%のユーザーを失います。100万ユーザーのアプリの場合、1つのバグあたり1万〜2万のインストール損失になります。これは特に金融および医療セクターのアプリにとって重要です。
不安定なネットワーク — モバイルデバイスは常にWi-Fiとモバイルネットワークを切り替え、圏外エリア(地下鉄、エレベーター、地下室)に入ります。切り替えのたびに一時的な接続喪失が発生し、アプリは適切に処理する必要があります。
タイムアウト — サーバーが設定されたタイムアウト(通常10〜30秒)内に応答しない場合、クライアントはSocketTimeoutExceptionをスローします。フィードバックのない長時間のタイムアウトは、ユーザーにはフリーズとして認識されます。タイムアウトは15秒以内に設定することをお勧めします。
val client = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build()
レースコンディション — 複数のスレッドが同期なしに同じデータを同時に読み書きするときに発生します。例えば、UIスレッドでキャッシュからデータを読み込みながらネットワークからキャッシュを更新すると、古いデータや不正確なデータが表示される可能性があります。
Offline-first — ローカルストレージ(Room、CoreData)を唯一の信頼できる情報源とするアーキテクチャパターンです。ネットワークはバックグラウンドでのデータ同期に使用されます。ユーザーはネットワーク接続がなくても、ローカルキャッシュから常に最新のデータを表示できます。
Repositoryパターン — ネットワークからデータを取得するかキャッシュから取得するかを決定する、データの単一エントリポイントです。リポジトリはViewModelやUIからデータソースを抽象化します。ネットワークエラー時には、リポジトリは自動的にローカルソースに切り替わります。
class UserRepository(
private val api: UserApi,
private val dao: UserDao
) {
suspend fun getUsers(): Result<List<User>> {
return try {
val remote = api.fetchUsers()
dao.insertAll(remote)
Result.success(remote)
} catch (e: IOException) {
val cached = dao.getAll()
if (cached.isNotEmpty()) {
Result.success(cached) // return cache on network error
} else {
Result.failure(e)
}
}
}
}
サーキットブレーカー — サーバーが利用不可のときにリクエストの殺到から保護するパターンです。N回連続でエラーが発生するとサーキットブレーカーが開き、すべてのリクエストは接続を試みずに即座にエラーを返します。指定されたタイムアウト後、サーキットブレーカーはテストリクエストのために半開き状態に移行します。
指数バックオフ — 標準的なリトライメカニズムです。最初の失敗後は1秒待ち、2回目は2秒、その後4、8、16秒と待ちます。サーバーとバッテリーに過負荷をかけないよう、最大リトライ回数(通常3〜5回)を制限してください。
ユーザーフィードバック — ネットワークエラー時には、明確なメッセージを表示します:“接続がありません”、“サーバーが一時的に利用できません”、“インターネット接続を確認してください”。SnackbarやInline State Viewを使用します。絶対に技術的なエラー(HTTP 500、SocketException)をユーザーに表示しないでください。
ConnectivityManager — ネットワーク監視のためのAndroid APIです。アプリが変更に反応できるようにします:接続喪失時にプレースホルダーを表示し、復旧時に自動的にデータを更新します。iOSではNetworkフレームワークのNWPathMonitorを使用します。
class NetworkMonitor(private val context: Context) {
private val manager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
fun isOnline(): Boolean {
val network = manager.activeNetwork ?: return false
val caps = manager.getNetworkCapabilities(network) ?: return false
return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}
}
Crashlytics(Firebase)— モバイルアプリ向けの標準的なクラッシュレポートツールです。すべての未処理例外のスタックトレース、OSバージョン、デバイスモデル、クラッシュ時刻を収集します。エラーをグループ化し、修正責任者を割り当てることができます。
Sentry — パフォーマンス監視をサポートするCrashlyticsの代替ツールです。特定のトランザクション(例:“ユーザー認証”)をトレースし、どのステップでエラーが発生したかを確認できます。パフォーマンストレーシングは、ネットワークタイムアウトとアプリロジックのバグを区別するのに役立ちます。
Timber — クラスごとに自動的にタグを追加するAndroid用ロギングライブラリです。デバッグビルドではすべてのネットワークリクエストとレスポンスをログに記録します。リリースビルドでは、Crashlytics.setCustomLogを介してエラーと警告のみをログに記録します。
| ツール | 種類 | 使用時 |
|---|---|---|
| Crashlytics | クラッシュレポート | 常にリリースで — 自動クラッシュ収集 |
| Sentry | クラッシュ+パフォーマンス | 特定のユーザーシナリオをプロファイリングする必要がある場合 |
| Timber | ロギング | デバッグ:完全ロギング;リリース:エラーのみ |
| HTTP Toolkit | ネットワークデバッグ | ローカルHTTPトラフィックの傍受と分析 |
Firebase Summit 2023によると、Crashlytics + Performance Monitoringを実装したアプリは、重大なバグの検出と修正にかかる平均時間を3日から4時間に短縮しました。アクティブユーザーの0.1%を超える頻度のクラッシュごとにアラートを設定することをお勧めします。
よくある質問
クラッシュがCrashlyticsで捕捉されない場合、ネイティブクラッシュ(SIGSEGV、SIGABRT)を確認してください。これらはJava/Kotlinの例外ハンドラーでは処理されません。AndroidではJNIからのネイティブメモリリーク、iOSではEXC_BAD_ACCESSが考えられます。ネイティブクラッシュのスタックトレースを収集するには、Breakpad(Android)またはPLCrashReporter(iOS)を使用します。
Network Link Conditioner(iOSに内蔵、AndroidではFacebook Network Connection ClassまたはDeveloper Options > Network > Select network typeを使用)を利用します。遅延500〜3000ms、パケットロス5〜30%に設定します。Charles Proxyやmitmproxyを使用してネットワーク遅延や切断をシミュレートすることもできます。
ANRはUIスレッドが5秒以上ブロックされると発生します。ネットワークリクエストはバックグラウンドスレッドで実行する必要があります:コルーチン(viewModelScope.launch(Dispatchers.IO))、RxJava(subscribeOn(Schedulers.io))、または同期用のWorkManagerを使用します。HTTPクライアントには常にタイムアウトを設定してください — タイムアウトがないと永久ブロックの原因になります。
レースコンディション — 操作の結果がスレッド実行の順序に依存する状況です。例えば、ユーザーが“送信”ボタンを素早く2回押すと、リクエストが2回送信されます。解決策:Mutex、シングルスレッドエグゼキューター、またはステートマシン(最初のクリック後にボタンを無効化)を使用します。Kotlinでは、コルーチンのMutexまたは@Synchronizedアノテーションを使用します。
モバイルアプリにカオスエンジニアリングを適用します:操作中にネットワークを切断し、高レイテンシをシミュレートし、Wi-Fiとモバイルネットワークを切り替え、システム経由でプロセスを強制終了します。ツール:Facebook Network Connection Class、Charles Proxy、iOS Network Link Conditioner。CI/CDでは、AndroidTest Orchestratorを介して異なるネットワーク条件でのUIテストを追加します。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。