Suspended — iOSアプリケーションライフサイクルの一時停止状態であり、アプリはメモリ内でフリーズされますが、コードは実行しません。Suspendedの仕組み、バックグラウンドでのアプリフリーズのリスク、iOSがSuspendedアプリのアンロードを管理する方法、およびSuspendedから戻った後のシームレスな復元のためのstate restorationの実装方法を説明します。
重要なポイント
Suspended は、iOSアプリケーションライフサイクルの状態であり、アプリはデバイスのRAMに存在しますが、コードを一切実行しません。これは完全終了前の最終状態です。アプリは、すべてのバックグラウンドタスクが完了した後、またはタイムアウトが経過した後に、BackgroundからSuspendedに遷移します。Suspendedでは、アプリは完全にフリーズされます — すべてのスレッドが一時停止され、タイマーは動作せず、ネットワークアクティビティはありません。
Suspended はiOSの独自機能であり、標準のAndroidライフサイクルには存在しません。その理由は、プロセス管理アーキテクチャの違いにあります。iOSはアプリのイメージをメモリに保持し(デスクトップのハイバネーションに類似)、ユーザーが戻ったときにコールドスタートなしでインターフェースを即座に復元できるようにします。AndroidにはSuspendedはありません — プロセスは存在してコードを実行できる(Background)か、終了している(Not Running)かのいずれかですが、AndroidはLMKを介してスレッド実行を一時停止できます。
ユーザーにとって、Suspendedは瞬時の復元のように見えます。App Switcherを使用してアプリを切り替えると、各アプリは中断した場所から開きます。これにより、すべてのアプリが同時に実行されているかのような錯覚が生まれます。実際には、そのほとんどがSuspendedでフリーズされています。Suspendedからのホットスタートは、Not Runningからのコールドスタートよりも何倍も高速です。コードが既にメモリにロードされているためです。
iOSはすべてのアプリケーションの状態を監視し、利用可能なメモリに基づいてSuspendedアプリのアンロードを決定します。メモリが不足すると、システムはSuspendedアプリのアンロードを開始し、この状態で最も長く留まっているものから始めます。それでもメモリが不足している場合、システムはBackgroundおよびInactiveのアプリをSuspendedに遷移させ、その後アンロードします。このプロセスはユーザーにとって完全に透過的です — App Switcherにアプリアイコンが表示され、タップするとコールドスタートが開始されるだけです。
| 特性 | Suspended (iOS) | Background (iOS) | Background (Android) |
|---|---|---|---|
| コード実行 | なし | あり(制限付き) | あり(制限付き) |
| メモリ内 | あり | あり | あり |
| CPU消費 | 0% | 低い | 低い |
| ホットスタート | あり — 即時復元 | あり — Inactive経由 | なし — プロセスが強制終了される可能性あり |
| タイムアウト | なし — メモリ内に何時間も留まることが可能 | 約30秒(beginBackgroundTask後) | APIバージョンに依存 |
| システムによるアンロード | メモリ不足時 | メモリが深刻に不足している場合 | LMK(Low Memory Killer) |
| 作業への復帰 | App Switcherから — 即座に | App Switcherから — Inactive経由 | コールドスタート |
| State Restoration | 推奨 | 不要 | SavedStateHandle |
iOSでは、すべてのバックグラウンドタスクが完了した後、Suspended に自動的に移行します。システムはapplicationDidEnterBackgroundを呼び出し、beginBackgroundTaskを実行する時間(約30秒)を与えた後、強制的にすべてのスレッドを一時停止し、アプリをSuspendedに遷移させます。メモリ内のオブジェクトは保持されますが、コードは実行されません — アプリは現在の状態でフリーズされます。
極めて重要なポイント:applicationDidEnterBackground は、Suspendedの前に確実に呼び出される最後のメソッドです。この後、アプリはメモリアンロードに関する通知を受け取りません。ユーザーまたはシステムがSuspended状態のアプリを強制終了しても、applicationWillTerminateもapplicationDidEnterBackgroundも再び呼び出されません。したがって、すべてのデータ保存はapplicationDidEnterBackgroundで行われるべきであり、applicationWillTerminateではありません。
import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
// Suspendedの前の最後の確実な呼び出し
func applicationDidEnterBackground(_ application: UIApplication) {
// メモリアンロードを生き残るために必要なすべてを保存
savePersistentState()
saveNavigationStack()
// 必要に応じて追加の時間を要求
let task = application.beginBackgroundTask {
application.endBackgroundTask(task)
}
}
// Suspendedからの復帰 — ホットスタート
func applicationWillEnterForeground(_ application: UIApplication) {
// アプリはSuspended状態にありました、作業を再開
print("SuspendedまたはBackgroundからの復帰")
}
// メモリアンロード後の完全な復元
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Suspendedからのアンロード後のコールドスタートの場合 —
// state restorationを復元
return true
}
private func savePersistentState() {
UserDefaults.standard.set(Date(), forKey: "lastActiveDate")
}
private func saveNavigationStack() {
guard let rootVC = window?.rootViewController else { return }
// 現在のナビゲーションスタックを保存
if let navController = rootVC as? UINavigationController {
let vcClasses = navController.viewControllers.map { type(of: $0) }
UserDefaults.standard.set(vcClasses.map { NSStringFromClass($0) }, forKey: "navStack")
}
}
}コードは、iOSでのSuspendedの極めて重要な処理を示しています。applicationDidEnterBackground は最後の確実な呼び出しです。すべてのデータ保存はここで行われるべきです:ユーザー状態、ナビゲーションスタック、下書き、タイマー。applicationWillEnterForegroundはSuspendedまたはBackgroundから戻るときに呼び出されます。didFinishLaunchingWithOptions — アプリがSuspended後にメモリからアンロードされた場合のコールドスタート時のみです。
Androidには、iOSのSuspendedの直接的な類似機能はありません。Androidは実行コンテキストを保持したままアプリをメモリ内でフリーズしません。代わりに、Androidはプロセスをバックグラウンドに保持するか、終了します。ただし、Android 11+(API 30)では、App Freezer と呼ばれるメカニズムが導入され、SIGSTOPシグナルを使用してバックグラウンドプロセスの実行を一時停止します。これは機能的にはSuspendedに似ていますが、重要な違いがあります。
App Freezer はAndroidのメモリ管理システムの一部です。アプリがアクティブな通知なしで長時間バックグラウンドにあると、システムはSIGSTOPを送信してすべてのスレッドを一時停止します。アプリがフォアグラウンドに戻ると、SIGCONTが送信され、実行が再開されます。iOSとの主な違い:App Freezerは状態の保持を保証しません — フリーズ中にプロセスが強制終了されると、メモリ内のデータが失われる可能性があります。
Androidでは、プロセス終了時の自動状態保存のために、ViewModelでSavedStateHandleを使用することが推奨されています。SavedStateHandleはonSaveInstanceStateを介してBundleにデータを保存し、App FreezerとProcess Deathの両方から生き残ります。iOSではSuspendedからのアンロードが例外的な状況であるのに対し、AndroidではProcess Deathは常に予想される通常の動作です。
// SavedStateHandle — AndroidでのProcess Deathからの救済
class CheckoutViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// App Freezer後でもプロセスを生き残る状態
var currentStep: MutableLiveData<Int> =
savedStateHandle.getLiveData("checkout_step", 1)
var cartItems: MutableLiveData<List<CartItem>> =
savedStateHandle.getLiveData("cart_items", emptyList())
fun proceedToNextStep() {
currentStep.value = (currentStep.value ?: 0) + 1
}
fun addToCart(item: CartItem) {
val updatedList = (cartItems.value ?: emptyList()) + item
cartItems.value = updatedList
savedStateHandle["cart_items"] = updatedList
}
}
// App Freezerに備えてonStopで保存
class MainActivity : AppCompatActivity() {
override fun onStop() {
super.onStop()
// フリーズを生き残る必要があるデータを保存
saveDraftData()
// 凍結状態で不要なリソースを解放
releaseHeavyResources()
// アプリがフリーズされることを警告
// (デバッグ用のロギング)
Log.d("Lifecycle", "Activity stopped — App Freezeの可能性")
}
}コードは、AndroidでSuspended類似機能を処理するアプローチを示しています。ViewModelのSavedStateHandle は、Process Death中にデータを自動的に保存および復元します。onStop は、App Freezerまたはプロセス終了の可能性がある前の最後の確実なイベントです。チェックアウトフォームの状態、カートのアイテムリスト — これらすべてのデータはSavedStateHandleのおかげでフリーズから生き残ります。重いリソース(ビットマップ、DBカーソル)については、onStopがメモリを解放する場所です。
State Restoration は、アプリがメモリからアンロードされた後にUI状態を保存および復元するためのiOSの組み込みメカニズムです。アプリがSuspended状態にあり、システムがそれをアンロードした場合、次のコールドスタート時にstate restorationがナビゲーションスタック、スクロール位置、フォーム状態、その他のUI要素を復元します。ユーザーは中断した同じ画面に戻ります。
State Restorationは、UIViewControllerRestoration および UIStateRestoring プロトコルを介して機能します。開発者は、復元したい各ViewControllerおよびViewにrestorationIdentifierを割り当てます。バックグラウンドに移動すると、iOSはこれらのオブジェクトの状態をエンコードします。アンロード後に戻ると、iOSは新しいオブジェクトを作成し、保存された状態をデコードします。State restorationがない場合、ユーザーはコールドスタート後に中断した場所ではなく、空白の画面を表示します。
import UIKit
class DetailViewController: UIViewController {
var itemID: String = ""
var scrollPosition: CGPoint = .zero
override func viewDidLoad() {
super.viewDidLoad()
restorationIdentifier = "DetailViewController"
restorationClass = type(of: self)
}
override func encodeRestorableState(with coder: NSCoder) {
super.encodeRestorableState(with: coder)
coder.encode(itemID, forKey: "itemID")
coder.encode(scrollPosition, forKey: "scrollPosition")
}
override func decodeRestorableState(with coder: NSCoder) {
super.decodeRestorableState(with: coder)
if let savedID = coder.decodeObject(forKey: "itemID") as? String {
itemID = savedID
loadItem()
}
if let savedPosition = coder.decodeCGPoint(forKey: "scrollPosition") {
scrollPosition = savedPosition
// データ読み込み後に位置を復元
}
}
}
// AppDelegate — State Restorationの有効化
func application(
_ application: UIApplication,
shouldSaveSecureApplicationState coder: NSCoder
) -> Bool {
return true
}
func application(
_ application: UIApplication,
shouldRestoreSecureApplicationState coder: NSCoder
) -> Bool {
return true
}コードは、iOSでのState Restorationの実装を示しています。復元可能な各ViewControllerには、restorationIdentifier と restorationClass が必要です。encodeRestorableState/decodeRestorableStateはNSCoderを介してデータを保存およびロードします。AppDelegateでは、shouldSaveSecureApplicationStateとshouldRestoreSecureApplicationStateが暗号化された状態保存を有効にします。iOS 12+では、データ保護のために安全なエンコーディング(NSSecureCoding)を使用することを推奨します。
最初のルール — アプリがSuspendedから戻ってくることを決して前提にしないでください。システムはいつでもアプリをアンロードできます。すべての極めて重要なデータは、Suspendedに遷移する前に永続ストレージに保存される必要があります — つまり、applicationDidEnterBackgroundまたはonStopで保存します。UserDefaults、Core Data、File Manager が適切なストレージオプションです。メモリ(変数、プロパティ)は、Suspendedを生き残る必要があるデータにとって信頼できないストレージです。
2番目のルール — Suspendedの前にリソースを解放します。ファイルディスクリプタを閉じ、GPUメモリ(Metal、Core Graphics)を解放し、ネットワーク接続を閉じます。アプリはSuspendedでCPUを消費しませんが、占有されたリソースは他のアプリケーションからブロックされます。iOSでは、Suspendedでソケットを開いたままにできません — Suspendedから戻るときに機能しなくなり、エラーが発生する可能性があります。
3番目のルール — Suspendedからの復帰を期待する時間依存のロジックを配置しないでください。タイマー、コールバック、およびネットワークアクティビティはSuspendedで停止します。アプリが数時間Suspendedにあった場合、復帰時にタイマーが誤って作動する可能性があります。復帰時にデータの有効性を確認してください — キャッシュが古くなっているか、認証トークンが期限切れになっている可能性があります。
4番目のルール — すべての画面でState Restorationを使用し、特に入力フォーム、スクロール可能なリスト、詳細画面で使用します。State restorationがないと、アンロードされたSuspendedから戻った後、ユーザーは中断した場所ではなくアプリの初期画面を表示します。これはユーザーエクスペリエンスを低下させ、ユーザーにアクションを繰り返させることになります。
import UIKit
// 確認:アプリはメモリからアンロードされましたか?
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// 保存された状態が存在するか確認
if UserDefaults.standard.object(forKey: "navStack") != nil {
// アプリはSuspendedからアンロードされました
// 状態を復元する必要があります
restoreNavigationStack()
} else {
// Not Runningからのクリーンなコールドスタート
showOnboardingIfNeeded()
}
return true
}
private func restoreNavigationStack() {
guard let savedStack = UserDefaults.standard.array(forKey: "navStack") as? [String],
let navController = window?.rootViewController as? UINavigationController
else { return }
for vcClassName in savedStack {
if let vcClass = NSClassFromString(vcClassName) as? UIViewController.Type {
let vc = vcClass.init()
navController.pushViewController(vc, animated: false)
}
}
}コードは、アプリがSuspendedからアンロードされたかどうかを判断する方法を示しています。保存されたナビゲーションスタックの有無についてUserDefaultsを確認することで、アンロード後のコールドスタートとクリーンなコールドスタートを区別できます。前者の場合はナビゲーションスタックが復元され、後者の場合はオンボーディングまたはメイン画面が表示されます。このアプローチは、NSCoderが不十分な場合に組み込みのState Restorationを補完します。
よくある質問
無制限です — 数秒から数日まで。iOSにはSuspendedのタイムアウトはありません。アプリは、システムがリソース不足のためにアンロードすることを決定するまで、メモリ内に留まります。実際には、アプリはデバイスのRAMとアクティブなアプリケーションの数に応じて、15分から数時間Suspended状態を維持します。
いいえ。SuspendedからアプリをアンロードするときにapplicationWillTerminateは呼び出されません。システムはアプリに通知せずに単にメモリを解放します。これも、すべてのデータ保存をapplicationDidEnterBackgroundで行うべき理由の1つです。applicationWillTerminateは、ユーザーがApp Switcherからアプリをスワイプして手動で終了した場合にのみ呼び出されます。
直接的な類似機能はありません。Android 11+では、App Freezer が導入され、SIGSTOPを介してバックグラウンドプロセスを一時停止します — これは機能的にはSuspendedに似ています。ただし、AndroidアプリはいつでもProcess Deathが発生することを前提に設計する必要があります。ViewModelでSavedStateHandleとonSaveInstanceStateを使用して、App FreezerとProcess Deathの両方から生き残る状態を保存してください。
iOSには確認するための直接的なAPIはありません。間接的な方法:didFinishLaunchingWithOptionsで保存された状態の有無についてUserDefaultsを確認します。状態が存在する場合 — アプリはSuspendedからアンロードされ、コールドスタートしています。状態が存在しない場合 — クリーンなコールドスタートです。SwiftUIでは、scenePhase.backgroundにフラグを保存し、次回の起動時に確認できます。
Suspendedに遷移するとき、iOSはスナップショット — 現在のアプリUIのスクリーンショットを撮ります。このスクリーンショットはApp Switcherとアプリに戻るとき(「解凍」アニメーションとして)表示されます。アプリに機密データが含まれている場合、スナップショットがそれを露出させる可能性があります。保護するには、UIApplication.shouldSnapshotSecureApp(iOS 16+)を使用するか、applicationDidEnterBackgroundでぼかしオーバーレイを適用します。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。