Suspended — สถานะหยุดชั่วคราวของวงจรชีวิตแอปพลิเคชัน iOS ซึ่งแอปถูกแช่แข็งในหน่วยความจำแต่ไม่ได้รันโค้ด เราแสดงให้เห็นว่า Suspended ทำงานอย่างไร ความเสี่ยงที่เกิดจากการแช่แข็งแอปในพื้นหลัง iOS จัดการการนำออกแอปที่ Suspended อย่างไร และวิธีการใช้ state restoration เพื่อการกู้คืนอย่างราบรื่นหลังจากกลับจาก Suspended
ประเด็นสำคัญ
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")
}
}
}โค้ดแสดงการจัดการที่สำคัญอย่างยิ่งของ Suspended บน iOS applicationDidEnterBackground เป็นการเรียกที่รับประกันครั้งสุดท้าย การบันทึกข้อมูลทั้งหมดควรเกิดขึ้นที่นี่: สถานะผู้ใช้ สแต็กนำทาง ร่าง ตัวจับเวลา applicationWillEnterForeground ถูกเรียกเมื่อกลับจาก Suspended หรือ Background didFinishLaunchingWithOptions — เฉพาะเมื่อสตาร์ทเย็น เมื่อแอปถูกนำออกจากหน่วยความจำหลัง Suspended
ใน Android ไม่มีสิ่งที่คล้ายกันโดยตรง กับ Suspended ของ iOS Android ไม่แช่แข็งแอปในหน่วยความจำขณะรักษาบริบทการรันไว้ แต่ Android จะเก็บโพรเซสไว้ในพื้นหลังหรือสิ้นสุดมัน อย่างไรก็ตาม บน Android 11+ (API 30) มีกลไกที่เรียกว่า App Freezer ซึ่งหยุดการรันโพรเซสพื้นหลังชั่วคราวโดยใช้สัญญาณ SIGSTOP สิ่งนี้คล้ายกับ Suspended ในเชิงฟังก์ชัน แต่มีความแตกต่างที่สำคัญ
App Freezer เป็นส่วนหนึ่งของระบบจัดการหน่วยความจำของ Android เมื่อแอปอยู่ในพื้นหลังเป็นเวลานานโดยไม่มีการแจ้งเตือนที่ทำงานอยู่ ระบบจะส่ง SIGSTOP หยุดเธรดทั้งหมด เมื่อแอปกลับมาที่เบื้องหน้า จะส่ง SIGCONT และการรันดำเนินต่อ ความแตกต่างหลักจาก iOS: App Freezer ไม่รับประกันการรักษาสถานะ — ข้อมูลในหน่วยความจำอาจสูญหายหากโพรเซสถูกฆ่าระหว่างการแช่แข็ง
บน Android แนะนำให้ใช้ SavedStateHandle ใน ViewModel เพื่อการรักษาสถานะอัตโนมัติระหว่างการสิ้นสุดโพรเซสใด ๆ SavedStateHandle บันทึกข้อมูลใน Bundle ผ่าน onSaveInstanceState ซึ่งรอดทั้งจาก App Freezer และ Process Death แตกต่างจาก iOS ที่การนำออกจาก Suspended เป็นสถานการณ์พิเศษ บน Android Process Death เป็นพฤติกรรมปกติที่ควรคาดหวังเสมอ
// SavedStateHandle — ความรอดจาก Process Death บน Android
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
}
}
// บันทึกใน onStop ในกรณี App Freezer
class MainActivity : AppCompatActivity() {
override fun onStop() {
super.onStop()
// บันทึกข้อมูลที่ต้องรอดจากการแช่แข็ง
saveDraftData()
// ปล่อยทรัพยากรที่ไม่จำเป็นในสถานะแช่แข็ง
releaseHeavyResources()
// เตือนว่าแอปจะถูกแช่แข็ง
// (การบันทึกสำหรับการดีบัก)
Log.d("Lifecycle", "Activity stopped — เป็นไปได้ App Freeze")
}
}โค้ดแสดงแนวทางการจัดการสิ่งที่คล้าย Suspended บน Android SavedStateHandle ใน ViewModel บันทึกและกู้คืนข้อมูลโดยอัตโนมัติระหว่าง Process Death onStop เป็นอีเวนต์ที่รับประกันครั้งสุดท้ายก่อน App Freezer หรือการสิ้นสุดโพรเซสที่อาจเกิดขึ้น สถานะฟอร์มชำระเงิน รายการสินค้าในตะกร้า — ข้อมูลทั้งหมดนี้รอดจากการแช่แข็งด้วย SavedStateHandle สำหรับทรัพยากรหนัก (บิตแมป เคอร์เซอร์ DB) onStop คือที่สำหรับปล่อยหน่วยความจำ
State Restoration เป็นกลไกในตัวของ iOS สำหรับบันทึกและกู้คืนสถานะ UI หลังจากแอปถูกนำออกจากหน่วยความจำ หากแอปอยู่ใน Suspended และระบบนำออก ในการสตาร์ทเย็นครั้งถัดไป state restoration จะกู้คืนสแต็กนำทาง ตำแหน่งเลื่อน สถานะฟอร์ม และองค์ประกอบ UI อื่น ๆ ผู้ใช้กลับไปยังหน้าจอเดียวกันที่พวกเขาค้างไว้
State Restoration ทำงานผ่านโปรโตคอล UIViewControllerRestoration และ UIStateRestoring นักพัฒนากำหนด restorationIdentifier ให้กับแต่ละ ViewController และ View ที่ต้องการกู้คืน เมื่อไปที่พื้นหลัง 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
}โค้ดแสดงการใช้งาน State Restoration บน iOS restorationIdentifier และ restorationClass จำเป็นสำหรับแต่ละ ViewController ที่สามารถกู้คืนได้ encodeRestorableState/decodeRestorableState บันทึกและโหลดข้อมูลผ่าน NSCoder ใน AppDelegate shouldSaveSecureApplicationState และ shouldRestoreSecureApplicationState เปิดใช้งานการบันทึกสถานะที่เข้ารหัส บน iOS 12+ แนะนำให้ใช้การเข้ารหัสที่ปลอดภัย (NSSecureCoding) เพื่อป้องกันข้อมูล
กฎข้อที่หนึ่ง — อย่าคิดว่าแอปจะกลับมาจาก Suspended ระบบสามารถนำออกแอปได้ทุกเมื่อ ข้อมูลที่สำคัญอย่างยิ่งทั้งหมดต้องถูกบันทึกไปยังที่เก็บถาวรก่อนการเปลี่ยนไปยัง Suspended — นั่นคือใน applicationDidEnterBackground หรือ onStop UserDefaults, Core Data, File Manager เป็นตัวเลือกพื้นที่เก็บข้อมูลที่เหมาะสม หน่วยความจำ (ตัวแปร คุณสมบัติ) เป็นพื้นที่เก็บข้อมูลที่ไม่น่าเชื่อถือสำหรับข้อมูลที่ต้องรอดจาก Suspended
กฎข้อที่สอง — ปล่อยทรัพยากรก่อน Suspended ปิดตัวอธิบายไฟล์ ปล่อยหน่วยความจำ GPU (Metal, Core Graphics) ปิดการเชื่อมต่อเครือข่าย แม้ว่าแอปจะไม่ใช้ CPU ใน Suspended แต่ทรัพยากรที่ถูกครอบครองจะถูกบล็อกจากแอปอื่น บน iOS ไม่สามารถเก็บซ็อกเก็ตที่เปิดไว้ใน Suspended — เมื่อกลับจาก Suspended ซ็อกเก็ตอาจไม่ทำงานทำให้เกิดข้อผิดพลาด
กฎข้อที่สาม — อย่าวางตรรกะที่ขึ้นกับเวลาโดยคอยการกลับจาก Suspended ตัวจับเวลา คอลแบ็ก และกิจกรรมเครือข่ายหยุดใน Suspended หากแอปอยู่ใน Suspended หลายชั่วโมง ตัวจับเวลาอาจทำงานผิดพลาดเมื่อกลับมา ตรวจสอบความถูกต้องของข้อมูลเมื่อกลับมา — แคชอาจล้าสมัยและโทเคนอนุญาตอาจหมดอายุ
กฎข้อที่สี่ — ใช้ 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 ว่ามีสแต็กนำทางที่บันทึกไว้หรือไม่ช่วยให้แยกแยะสตาร์ทเย็นหลังการนำออกจากสตาร์ทเย็นปกติได้ ในกรณีแรก สแต็กนำทางถูกกู้คืน ในกรณีที่สอง หน้าจอแนะนำหรือหน้าจอหลักจะแสดงขึ้น แนวทางนี้เสริม State Restoration ในตัวสำหรับกรณีที่ NSCoder ไม่เพียงพอ
คำถามที่พบบ่อย
ไม่จำกัด — จากไม่กี่วินาทีถึงหลายวัน iOS ไม่มีการหมดเวลาสำหรับ Suspended แอปจะอยู่ในหน่วยความจำจนกว่าระบบจะตัดสินใจนำออกเนื่องจากทรัพยากรไม่เพียงพอ ในทางปฏิบัติ แอปอยู่ใน Suspended ตั้งแต่ 15 นาทีถึงหลายชั่วโมง ขึ้นอยู่กับ RAM ของอุปกรณ์และจำนวนแอปพลิเคชันที่ทำงานอยู่
ไม่ applicationWillTerminate ไม่ถูกเรียก เมื่อแอปถูกนำออกจาก Suspended ระบบเพียงปล่อยหน่วยความจำโดยไม่แจ้งให้แอปทราบ นี่เป็นอีกสาเหตุหนึ่งที่การบันทึกข้อมูลทั้งหมดต้องเกิดขึ้นใน applicationDidEnterBackground applicationWillTerminate ถูกเรียกเฉพาะเมื่อผู้ใช้สิ้นสุดแอปด้วยตนเองโดยการปัดออกจาก App Switcher
ไม่มีสิ่งที่คล้ายกันโดยตรง บน Android 11+ มีการนำ App Freezer ซึ่งหยุดโพรเซสพื้นหลังผ่าน SIGSTOP — สิ่งนี้คล้ายกับ Suspended ในเชิงฟังก์ชัน อย่างไรก็ตาม แอป Android ควรออกแบบโดยถือว่า Process Death สามารถเกิดขึ้นได้ทุกเมื่อ ใช้ SavedStateHandle ใน ViewModel และ onSaveInstanceState เพื่อบันทึกสถานะที่จะรอดทั้งจาก App Freezer และ Process Death
ใน iOS ไม่มี API โดยตรงสำหรับตรวจสอบ วิธีการทางอ้อม: ตรวจสอบ UserDefaults ว่ามีสถานะที่บันทึกไว้ใน didFinishLaunchingWithOptions หรือไม่ หากมีสถานะ — แอปถูกนำออกจาก Suspended และกำลังสตาร์ทเย็น หากไม่มีสถานะ — สตาร์ทเย็นปกติ ใน SwiftUI สามารถบันทึกแฟล็กใน scenePhase.background และตรวจสอบในการเริ่มครั้งถัดไป
เมื่อเปลี่ยนไป Suspended iOS จะถ่าย สแนปชอต — ภาพหน้าจอของ UI ปัจจุบันของแอป ภาพหน้าจอนี้แสดงใน App Switcher และเมื่อกลับไปยังแอป (เป็นอนิเมชัน “ละลาย”) หากแอปมีข้อมูลที่เป็นความลับ สแนปชอตอาจเปิดเผยได้ เพื่อป้องกัน ให้ใช้ UIApplication.shouldSnapshotSecureApp (iOS 16+) หรือใช้การซ้อนทับเบลอใน applicationDidEnterBackground
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ