⚠️ この記事はアイデア出しを筆者が行い、文章の生成に生成AIを使用しています。内容のレビューは筆者が行っています。

前の記事同様に

Androidアプリ開発の極意 | 技術評論社の「ユーザーストレス軽減」まわりの実装Tipsと、並行処理の選択肢をざっくり整理してExpo周りと絡めたもの。細かい実装には踏み込まず、要点だけ。

はじめに

アプリが固まる原因の大半は、メインスレッド(UIスレッド)を長時間ブロックしていること。非同期処理の設計は結局「ユーザーを待たせない」という一点のためにある。

二部構成で一部は本を読んだ感想+α、二部がExpoだとどうなるかって話。

基本的にAIで出して、それを読んで調べたりしたもの。

第1部:Androidの話

Android(Java/Kotlin)側だけで完結する話。UXまわりの実装Tipsと、非同期処理の手段がsynchronizedからVirtual Threadsまでどう変遷してきたかを、Expoの話を挟まずに整理する。

UXまわりの小さな判断

  • 時間のかかる処理にはProgressBarで進捗を見せる
  • SharedPreferencesは永続化のためディスクI/Oが発生する
  • セルラー通信時は重い処理の前に一言断りを入れると親切
  • エラーはその場で表示しつつ、Crashlytics/ACRAのような開発者向けツールで後から追える形が理想
  • NDKは高速化と引き換えにデバッグの複雑さが増す。基本的には奥の手

非同期処理はなぜ増え続けているのか

synchronizedは「同時に入らない」ことしか保証しない。順番までは保証しない。 Threadを直接生成した場合も同様で、生成順と実行順は一致しない。順番を保証したいならExecutors.newSingleThreadExecutor()のような直列化の仕組みを使う。

ExecutorServiceは、スレッドを直接管理せず、プールとキューで管理する仕組み。スレッド数の制御や再利用をアプリ側のロジックから切り離せる。

AsyncTaskはAPI 30で非推奨になった。ライフサイクル管理が弱く、Activity/Fragment破棄後もタスクが動き続けてリークやクラッシュを招くのが理由。

今の主流はKotlin Coroutines。スレッドをブロックせず中断できる軽量な並行処理で、排他制御が必要ならMutexを使う。UIに紐づく短い処理はCoroutines、画面を離れても続けるべき処理はWorkManagerと使い分ける。

JVM側ではJDK 21でVirtual Threads(JEP 444)が正式機能になった。I/Oバウンドな処理を大量にさばくのに向くが、CPUバウンドには向かない。ただしこれはJDK/Java SE側の機能で、Android Runtime(ART)は非対応。Androidアプリでそのまま使えるわけではない。

Akkaはアクター単位でメッセージパッシングする設計思想。2022年にライセンスがApache 2.0からBSL(Business Source License)に変わり、2024年10月以降はproduction利用にライセンスキーが必須になった。個人・OSS・startupは無料キー、商用は原則サブスクという体系。汎用ツールというより大規模分散システム向けの専門ツールという位置づけに変わっている。

比較表(Android)

手段 排他制御 順序保証 位置づけ
synchronized あり なし 低レベルの排他制御プリミティブ
Thread直接生成 なし なし 基本的には避ける
ExecutorService 設計次第 直列化すれば可 CPUバウンド処理で有効
AsyncTask なし なし API 30で非推奨
Kotlin Coroutines Mutexで対応 設計次第 Android/Kotlinの主流
Virtual Threads 従来通り 設計次第 JVM側、I/Oバウンド向け(ART非対応)
Akka アクターモデル 条件付き 大規模分散システム向け

「排他制御」と「順序保証」は別物。ここを混同すると設計は破綻する。AsyncTaskはAPI 30で非推奨、今はCoroutines(短命処理)とWorkManager(永続処理)を使い分けるのが主流。Virtual Threads(JDK 21)はJVM側の話でARTは非対応。

第2部:Expoだとどうなるか

第1部で挙げたAndroidの各トピックが、Expo/React Nativeの世界では何に対応するか、あるいはそもそも関係なくなるかを、同じ順番でなぞっていく。

UXまわりの小さな判断

  • ProgressBarにあたるのはActivityIndicatorreact-native-progressのような表示ライブラリ。考え方自体は変わらない
  • SharedPreferencesに相当するのは、標準的なAsyncStorage(Expo Go対応・非同期API)。高頻度アクセスが絡む用途ではMMKV(同期API・高速だがネイティブビルド必須でExpo Go非対応)へ寄せる流れがある。認証トークンのような機密情報はexpo-secure-storeで分けるのが定石
  • ネットワーク状態の確認はexpo-network@react-native-community/netinfoが担う
  • エラー収集はSentryなどがCrashlytics/ACRA相当。ユーザー向け表示と開発者向け収集を分けて考えるのはAndroidと同じ
  • NDKに近いのはNew Architecture(JSI/TurboModules)のC++コア。パフォーマンスとデバッグ容易性のトレードオフという構図は共通

非同期処理はなぜ増え続けているのか(Expo視点)

JS自体は単一スレッドで動くので、synchronizedExecutorServiceのようなスレッドプール管理はJSコードの関心事にほぼならない。スレッドが登場するのはネイティブモジュール側の実装(Kotlin/Swift)で、そこでは第1部の話がそのまま使われる。

AsyncTask→Coroutinesという変遷も、JS側からはawait NativeModule.method()と書くだけで意識せずに済む。裏側がCoroutinesかGCDかはモジュール実装次第。つまり非同期処理の実装品質はネイティブモジュール側に依存していて、自作モジュールを書くなら第1部の知見(AsyncTaskは避ける、Coroutinesを使う等)がそのまま活きる。

Virtual Threads(JDK 21)は、ARTが非対応な以上RNアプリの文脈でもほぼ関係ない。ネイティブ側の非同期処理は引き続きCoroutines/ExecutorServiceが選択肢になる。

RN/ExpoのJSはシングルスレッド+イベントループなので、mutexのような排他制御は基本不要。ただし非同期処理の完了順による論理的なrace conditionは別問題として存在する(Promise.allと逐次awaitでは結果が変わる、など)。単一スレッド=常に安全、というわけではない。

比較表(Expo/RN)

Androidの概念 Expo/RNでの実質的な対応
SharedPreferences AsyncStorage / MMKV / expo-secure-store
Thread/ExecutorService JS側は基本無関係。ネイティブモジュール実装側の話
AsyncTask→Coroutines JS側はawait NativeModule.method()のみ
Virtual Threads ARTが非対応のため基本関係ない
NDK JSI/TurboModulesのC++コアが近い

まとめ

  • Android側の非同期処理の変遷(synchronized→Thread→ExecutorService→AsyncTask→Coroutines→Virtual Threads)は「排他制御」と「順序保証」の違い、そして抽象化レベルの引き上げの歴史として整理できる
  • Expo/RNではJSが単一スレッドなので、こうした話の多くがネイティブモジュールの裏側に隠れる。ただし非同期処理の完了順によるrace conditionは残るため、単一スレッド=無条件に安全ではない

「排他制御」と「順序保証」は似て非なるもの。この区別を誤ると非同期処理の設計は必ずどこかで破綻する。

参考