マルチプロセス地獄 〜SharedPreferences競合と謎のクラッシュ〜

※ChatGPTを使用して記事を作成しています。

Androidアプリは「ひとつのプロセスで動く」と思い込んでいたあの頃。

でも実際には、Manifestにたった一行 android:process=":remote" と書くだけで、アプリは複数のプロセスを抱え込むことができます。

そしてその瞬間から、SharedPreferencesの破壊、データのすれ違い、謎のクラッシュ地獄が始まったのです…。

今回は、僕が過去に体験した「マルチプロセス地獄」を3つのケースに分けて紹介し、そこから学んだ教訓をまとめます。

ケース1: SharedPreferencesの競合事件

最初にハマったのはSharedPreferencesでした。

当時のプロジェクトでは、ユーザーの認証トークンや設定値をSharedPreferencesで管理していました。

「Serviceを別プロセスにしたらメモリ節約できそう」という安易な考えで、Serviceに android:process=":remote" を付与。

すると、ある日を境にユーザーの設定が全部リセットされる事件が発生しました。

サポートから「ユーザーが毎回ログインを求められる」と怒涛の連絡。調べてみると、原因はSharedPreferencesの競合でした。

❌ 失敗例(NGコード)

// プロセスAで保存
val prefs = context.getSharedPreferences("user_prefs", Context.MODE_PRIVATE)
prefs.edit().putString("token", "abc123").apply()

// プロセスBで読み込み
val prefsB = context.getSharedPreferences("user_prefs", Context.MODE_PRIVATE)
val token = prefsB.getString("token", null)
// → プロセスごとにキャッシュされるため、時に初期化されてしまう

SharedPreferencesはプロセスごとにメモリキャッシュを持つため、別プロセスでアクセスすると整合性が崩れます。

昔は MODE_MULTI_PROCESS で回避できましたが、Android 6.0で廃止されました。

✅ 修正版(DataStoreへの移行)

val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "user_prefs")

suspend fun saveToken(context: Context, token: String) {
    context.dataStore.edit { prefs ->
        prefs[stringPreferencesKey("token")] = token
    }
}

val tokenFlow: Flow<String?> = context.dataStore.data
    .map { prefs -> prefs[stringPreferencesKey("token")] }

DataStoreはトランザクション的に処理され、マルチプロセスでも安全に動作します。SharedPreferencesに頼りすぎていた自分を、ここで反省しました。

ケース2: マルチプロセスServiceでのデータ不整合

次にやらかしたのは、バックグラウンドServiceを別プロセスで動かした時です。

理由は単純、「重たい処理を分ければメインプロセスが軽くなるだろう」と思ったから。

実際には、同じアプリでもプロセスが違えばメモリは完全に独立

結果として、片方のプロセスで保存した状態がもう片方に届かない。

ユーザーは「設定を変えたのに反映されない」と混乱し、僕は「なぜ同期されないんだ!」と頭を抱えることに…。

❌ 失敗例(NGコード)

// MainProcess側
object SessionManager {
    var token: String? = null
}

// RemoteProcess側(Service)
class RemoteService : Service() {
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.d("RemoteService", "Token: ${SessionManager.token}")
        return START_STICKY
    }
}

// → RemoteProcessではSessionManagerは初期化状態

シングルトンやオブジェクトを共有できると信じていた自分がバカでした。

プロセスが違えば、それぞれに別インスタンスが作られます。

✅ 修正版(IPCを使った通信)

// AIDLを利用する例
interface IRemoteService {
    fun getToken(): String
}
// Service側
class RemoteService : Service() {
    private val binder = object : IRemoteService.Stub() {
        override fun getToken(): String {
            return "abc123" // 本来はDataStoreやDBから取得
        }
    }

    override fun onBind(intent: Intent?): IBinder {
        return binder
    }
}
// Service側
class RemoteService : Service() {
    private val binder = object : IRemoteService.Stub() {
        override fun getToken(): String {
            return "abc123" // 本来はDataStoreやDBから取得
        }
    }

    override fun onBind(intent: Intent?): IBinder {
        return binder
    }
}
// Client側
val connection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
        val remote = IRemoteService.Stub.asInterface(service)
        val token = remote.token
        Log.d("Client", "Token from remote: $token")
    }
    override fun onServiceDisconnected(name: ComponentName?) {}
}

地味ですが、プロセスを分けるなら必ずIPC設計を組み込む必要があります。

僕はこれを怠って、半年近く「同期バグ」に苦しみました。

ケース3: WebView専用プロセスでのセッション切れ

最後の地獄は、WebViewを別プロセスで動かした時です。

セキュリティのために android:process=":web" を設定したのですが、これが大きな落とし穴でした。

アプリ本体に保存していたCookieやセッション情報が、WebViewプロセスには届かない。

結果、ユーザーはログインしてもすぐにセッションが切れる現象に直面しました。

❌ 失敗例(NGコード)

// MainProcessで保存したCookie
CookieManager.getInstance().setCookie("https://example.com", "token=abc123")

// WebProcess側のWebViewでは無効
webView.loadUrl("https://example.com")
// → セッションが共有されない

✅ 修正版(CookieSyncを明示的に利用)

// MainProcessでCookieを保存したら即同期
val cookieManager = CookieManager.getInstance()
cookieManager.setCookie("https://example.com", "token=abc123")
cookieManager.flush()

// WebProcessでも同じCookieManagerを利用
webView.settings.javaScriptEnabled = true
webView.loadUrl("https://example.com")

Cookieはプロセス間で自動的には同期されません。

flush() を呼び出して明示的に永続化する必要があることを、この時初めて知りました。

共通する原因

  • 「Androidアプリは基本シングルプロセス」という思い込み
  • SharedPreferencesやシングルトンを「どこでも同じ」と誤解
  • プロセス分離による独立したメモリ空間を無視していた

結局、マルチプロセス設計を導入する時点で、アプリ全体を「分散システム」として設計しなければならなかったのです。

ベストプラクティスまとめ

  • SharedPreferencesに依存せず、DataStoreやRoomを利用する
  • プロセスを分ける場合は、IPC(AIDL / Binder / ContentProvider)を前提にする
  • WebViewとのデータ共有は、CookieManagerやサーバーセッションを明示的に同期
  • 基本はシングルプロセス設計を優先。どうしても必要な時だけマルチプロセスを導入する

教訓

マルチプロセスは魔法の杖ではなく、むしろ地獄の扉でした。

「パフォーマンスが良くなりそう」「セキュリティが高まりそう」…そう思って導入すると、必ずその裏でデータ不整合や通信の設計が必要になります。

僕が学んだのは、「何も考えずにマルチプロセスを使うな」という単純だけど強烈な教訓でした。

まとめ

  • SharedPreferences → プロセスごとに独立、DataStore推奨
  • Serviceの分離 → シングルトンは共有されない、IPC必須
  • WebViewの別プロセス → Cookieやセッションは共有されない

プロセスを分けるなら、「それは本当に必要か?」を何度も問い直すべきです。

便利そうに見えても、設計を誤ると地獄が待っています。

タイトルとURLをコピーしました