Not Running — สถานะเริ่มต้นของวงจรชีวิตของแอปพลิเคชันมือถือเมื่อยังไม่ได้เปิดใช้งานหรือสิ้นสุดการทำงานแล้ว เรียนรู้วิธีที่ iOS และ Android จัดการสถานะนี้ เหตุการณ์ใดที่นำไปสู่การเปลี่ยนจาก Not Running และวิธีจัดการการเริ่มต้นและการสิ้นสุดของแอปพลิเคชันใน Swift และ Kotlin อย่างถูกต้อง
ประเด็นสำคัญ
Not Running คือสถานะพื้นฐานของวงจรชีวิตของแอปพลิเคชันมือถือที่ไม่ได้โหลดใน RAM ของอุปกรณ์และไม่ได้ใช้ทรัพยากรระบบ ใน iOS และ Android สถานะนี้หมายถึงการไม่มีกระบวนการและเธรดที่เกี่ยวข้องกับแอปพลิเคชันโดยสมบูรณ์ ผู้ใช้เห็นไอคอนแอปบนหน้าจอหลัก แต่แอปพลิเคชัน本身ไม่ได้ทำงานและไม่อยู่ในรายการแอปล่าสุด
เมื่อผู้ใช้แตะไอคอนแอป ระบบจะสร้างกระบวนการใหม่ โหลดโค้ดที่ปฏิบัติการได้ลงในหน่วยความจำ และเริ่มต้นโครงสร้างข้อมูลที่จำเป็นทั้งหมด กระบวนการนี้เรียกว่าการเริ่มต้นแบบเย็น (cold start) และเป็นกระบวนการที่ใช้ทรัพยากรมากที่สุดในแง่ของเวลาโหลด
ระบบสามารถย้ายแอปพลิเคชันไปยัง Not Running จากสถานะอื่นใดก็ได้ หากแอปพลิเคชันอยู่ในพื้นหลังหรือถูกระงับ ระบบปฏิบัติการมีสิทธิ์ยกเลิกโหลดเมื่อ RAM ไม่เพียงพอสำหรับงานที่มีลำดับความสำคัญสูงกว่า — ตัวอย่างเช่น สำหรับแอปพลิเคชันที่ทำงานอยู่เบื้องหน้า
นักพัฒนาต้องพิจารณาว่า แอปพลิเคชันสามารถถูกสิ้นสุดโดยระบบได้ทุกเมื่อ เมื่ออยู่ในพื้นหลัง ซึ่งหมายความว่าข้อมูลที่ไม่ได้บันทึกทั้งหมดอาจสูญหายได้ ดังนั้น การบันทึกสถานะในที่เก็บแบบคีย์-ค่า (UserDefaults, SharedPreferences) หรือฐานข้อมูลท้องถิ่นระหว่างการเปลี่ยนจาก Active ไปยัง Background จึงมีความสำคัญอย่างยิ่ง
iOS ใช้ลำดับความสำคัญตามสถานะปัจจุบันของแอปพลิเคชัน: Active มีลำดับความสำคัญสูงสุด ตามด้วย Inactive, Background, Suspended และสุดท้าย Not Running — ลำดับความสำคัญต่ำสุด Android ใช้ลำดับชั้นกระบวนการที่คล้ายกัน: กระบวนการเบื้องหน้ามีลำดับความสำคัญ OOM_ADJ = 0, กระบวนการ Visible = 100, กระบวนการ Service = 200, กระบวนการ Background = 300, กระบวนการ Empty = 400 ยิ่งค่าสูง โอกาสที่กระบวนการจะถูกสิ้นสุดเมื่อหน่วยความจำต่ำก็ยิ่งมากขึ้น
| แพลตฟอร์ม | สถานะ | ลำดับความสำคัญในการยกเลิกโหลด | คำอธิบาย |
|---|---|---|---|
| iOS | Not Running | สูงที่สุด | แอปพลิเคชันไม่ได้โหลด — ไม่ใช้ทรัพยากรระบบ |
| iOS | Suspended | สูง | แอปพลิเคชันในหน่วยความจำแต่ไม่ได้รันโค้ด — เป้าหมายแรกสำหรับการยกเลิกโหลด |
| iOS | Background | ปานกลาง | แอปพลิเคชันกำลังทำงานพื้นหลัง — ถูกยกเลิกโหลดหลังจากหมดเวลา |
| iOS | Active | ต่ำ | แอปพลิเคชันที่ทำงานอยู่ — ถูกยกเลิกโหลดเมื่อหน่วยความจำวิกฤตเท่านั้น |
| Android | Empty Process | สูงที่สุด | กระบวนการที่ไม่มีส่วนประกอบที่ทำงานอยู่ — ถูกลบเป็นอันดับแรก |
| Android | Background Process | สูง | กระบวนการพื้นหลังที่ไม่มี Activity ที่มองเห็น |
| Android | Foreground Service | ต่ำ | บริการที่มีการแจ้งเตือน — ไม่ค่อยถูกสิ้นสุด |
| Android | Foreground Process | น้อยที่สุด | Activity ที่ทำงานอยู่ — ถูกสิ้นสุดเป็นลำดับสุดท้าย |
การเริ่มต้นแบบเย็น (cold start) เกิดขึ้นเมื่อแอปพลิเคชันเปลี่ยนจาก Not Running ไปยัง Active โดยตรง ระบบสร้างกระบวนการใหม่ โหลดคลาส เริ่มต้นฟิลด์แบบสแตติก สร้างเธรดหลัก และเริ่มต้นเฟรมเวิร์ก UI บน iOS หมายถึงการเรียก application(_:didFinishLaunchingWithOptions:) บน Android — การเรียก Application.onCreate() และ Activity.onCreate() เวลาในการเริ่มต้นแบบเย็นอาจอยู่ในช่วง 200 ms ถึงหลายวินาที ขึ้นอยู่กับความซับซ้อนของแอปพลิเคชัน
การเริ่มต้นแบบร้อน (warm start หรือ hot start) — แอปพลิเคชันอยู่ในสถานะ Suspended และกลับมาทำงานต่อโดยไม่ต้องโหลดซ้ำทั้งหมด ระบบกู้คืนสแต็ก UI ล่าสุดจากหน่วยความจำ และผู้ใช้ทำงานต่อจากจุดเดิม การเริ่มต้นแบบร้อนเร็วกว่าการเริ่มต้นแบบเย็นอย่างมาก เนื่องจากโค้ดส่วนใหญ่โหลดอยู่ในหน่วยความจำแล้ว บน iOS การเริ่มต้นแบบร้อนจะไม่เรียก application(_:didFinishLaunchingWithOptions:) แต่จะเรียกเฉพาะ applicationWillEnterForeground และ applicationDidBecomeActive
ความแตกต่างระหว่างการเริ่มต้นแบบเย็นและแบบร้อนมีความสำคัญต่อประสบการณ์ผู้ใช้ ในระหว่างการเริ่มต้นแบบเย็น นักพัฒนาต้องมั่นใจว่าการเริ่มต้นเกิดขึ้นเร็วที่สุดเท่าที่จะเป็นไปได้ — การเริ่มต้นแบบขี้เกียจของโมดูล การโหลดทรัพยากรหนักแบบเลื่อนเวลา การลดงานในเธรดหลักเมื่อเริ่มต้น Google แนะนำ ให้การเริ่มต้นแบบเย็นไม่เกิน 500 ms, Apple — ไม่เกิน 400 ms สำหรับ iOS
// การวัดเวลาเริ่มต้นแบบเย็นใน Android
class App : Application() {
private var startTime: Long = 0L
override fun onCreate() {
super.onCreate()
startTime = System.currentTimeMillis()
}
fun getStartupTime(): Long {
return System.currentTimeMillis() - startTime
}
}
// เริ่ม Activity ด้วยการเริ่มต้นแบบขี้เกียจ
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by lazy {
ViewModelProvider(this).get(MainViewModel::class.java)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// เฉพาะขั้นต่ำที่จำเป็นสำหรับเฟรมแรก
setupNavigation()
}
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
// การเริ่มต้นหนักหลังการเรนเดอร์
initializeHeavyModules()
}
}ตัวอย่างแสดงการวัดเวลาเริ่มต้นแบบเย็นใน Android Application.onCreate() ถูกเรียกเมื่อเปลี่ยนจาก Not Running ไปยัง Active การประทับเวลาจะถูกบันทึกเมื่อเริ่มกระบวนการ Activity ใช้การเริ่มต้นแบบขี้เกียจผ่านตัวแทน lazy เพื่อหลีกเลี่ยงการบล็อกเฟรมแรก onPostCreate เป็นตำแหน่งที่เหมาะสมที่สุดในการเริ่มต้นโมดูลหนักเนื่องจาก UI ถูกเรนเดอร์แล้ว
ใน iOS Not Running ถูกจัดการผ่านโปรโตคอล UIApplicationDelegate วิธีการหลัก: application(_:didFinishLaunchingWithOptions:) ถูกเรียกหลังจากเริ่มต้นแบบเย็น applicationWillTerminate(_:) ถูกเรียกก่อนที่ผู้ใช้จะสิ้นสุดแอปพลิเคชัน อย่างไรก็ตาม ระบบสามารถสิ้นสุดแอปพลิเคชันโดยไม่เรียก applicationWillTerminate — ตัวอย่างเช่น ระหว่างการสิ้นสุดฉุกเฉินหรือแรงดันหน่วยความจำ iOS ไม่รับประกันว่าวิธีการนี้จะถูกเรียก ดังนั้นควรบันทึกข้อมูลใน applicationDidEnterBackground
ผู้ใช้สามารถสิ้นสุดแอปพลิเคชันด้วยตนเองโดยการปัดใน App Switcher ระบบสามารถยกเลิกโหลดแอปพลิเคชันจากหน่วยความจำขณะอยู่ในพื้นหลัง แอปพลิเคชันอาจขัดข้อง ในทุกกรณี ออบเจกต์ทั้งหมดที่สร้างขึ้นระหว่างการเริ่มต้นจะถูกทำลาย สถานะที่ไม่ได้บันทึกจะสูญหายอย่างถาวร ใน iOS 13+ แนะนำให้ใช้ NSUserActivity หรือกลไกการกู้คืนสถานะผ่าน UIApplication.stateRestorationIdentifier เพื่อรักษาสถานะ
import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
// การเริ่มต้นแบบเย็น: แอปพลิเคชันเปลี่ยนจาก Not Running
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// การเริ่มต้นชุดบริการขั้นต่ำ
setupAnalytics()
configureAppearance()
return true
}
// แอปพลิเคชันสิ้นสุด — เฉพาะการปิดด้วยตนเอง
func applicationWillTerminate(
_ application: UIApplication
) {
saveCriticalData()
}
// บันทึกข้อมูลก่อนไปที่พื้นหลัง
func applicationDidEnterBackground(
_ application: UIApplication
) {
saveApplicationState()
}
private func saveCriticalData() {
UserDefaults.standard.synchronize()
}
private func saveApplicationState() {
let state = ["lastScreen": "main", "timestamp": Date()]
try? NSKeyedArchiver.archivedData(
withRootObject: state,
requiringSecureCoding: true
)
}
}โค้ดแสดงการจัดการ Not Running ที่ถูกต้องบน iOS applicationWillTerminate จะถูกเรียกเมื่อผู้ใช้สิ้นสุดแอปด้วยตนเองเท่านั้น การบันทึกข้อมูลสำคัญจะถูกทำซ้ำใน applicationDidEnterBackground เนื่องจากวิธีการนี้รับประกันว่าจะถูกเรียกก่อนไปที่พื้นหลัง การกู้คืนสถานะช่วยให้บันทึกสแต็ก UI สำหรับการกู้คืนในภายหลังระหว่างการเริ่มต้นแบบเย็น
ใน Android Not Running หมายความว่ากระบวนการของแอปพลิเคชันไม่มีอยู่ ระบบ Linux ที่เป็นพื้นฐานของ Android จัดการกระบวนการผ่านกลไก Zygote เมื่อแอปพลิเคชันถูกเริ่มต้น Zygote จะ fork กระบวนการใหม่ โหลด Dalvik/ART และเรียก Application.onCreate() Android ไม่มีสิ่งที่เทียบเท่าโดยตรงของ applicationWillTerminate — ระบบสามารถสิ้นสุดกระบวนการได้ทุกเมื่อโดยไม่มีการเตือนล่วงหน้า
เมื่อ Activity ถูกเรียกเป็นครั้งแรก ระบบจะสร้างกระบวนการ Application และ Activity ผ่าน chain onCreate → onStart → onResume หากผู้ใช้กดกลับ Activity จะถูกทำลาย (onDestroy) และกระบวนการอาจถูกสิ้นสุดโดยระบบ ความแตกต่างหลักจาก iOS: ใน Android กระบวนการสามารถดำรงอยู่ต่อไปได้แม้ไม่มี Activity ที่ทำงานอยู่ — ตัวอย่างเช่น หาก Foreground Service กำลังทำงานหรือมี BroadcastReceiver ที่ทำงานอยู่
// การจัดการ Not Running ผ่าน SavedStateHandle ใน ViewModel
class MainViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
companion object {
private const val KEY_LAST_SCREEN = "last_screen"
private const val KEY_USER_DATA = "user_data"
}
fun saveCurrentState(screen: String, data: String) {
savedStateHandle[KEY_LAST_SCREEN] = screen
savedStateHandle[KEY_USER_DATA] = data
}
fun restoreState(): AppState? {
val screen = savedStateHandle.get<String>(KEY_LAST_SCREEN)
val data = savedStateHandle.get<String>(KEY_USER_DATA)
return if (screen != null && data != null) {
AppState(screen, data)
} else null
}
}
// Application — การเรียกกลับครั้งแรกหลังจาก Not Running
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
initCrashReporter()
initDependencyInjection()
}
}SavedStateHandle เป็นส่วนประกอบของ Android Architecture Components ที่บันทึกสถานะโดยอัตโนมัติระหว่างการเปลี่ยนไปยัง Not Running และกู้คืนเมื่อเริ่มต้นแบบเย็น ViewModel ที่สร้างผ่าน ViewModelProvider จะอยู่รอดจากการหมุนหน้าจอและการทำลาย Activity เมื่อกระบวนการสิ้นสุด ข้อมูลจาก SavedStateHandle จะถูกทำให้เป็นอนุกรมใน Bundle และบันทึกในสถานะอินสแตนซ์ที่บันทึกไว้
Not Running เกิดขึ้นจากหลายสาเหตุ ผู้ใช้ปิดแอปพลิเคชันด้วยตนเอง ระบบยกเลิกโหลดแอปพลิเคชันเนื่องจากหน่วยความจำไม่เพียงพอ แอปพลิเคชันขัดข้องด้วยข้อยกเว้น บน Android ระบบสามารถสิ้นสุดกระบวนการระหว่างการอัปเดตแอปจำนวนมากหรือการรีบูตอุปกรณ์ iOS สามารถสิ้นสุดแอปพลิเคชันเมื่องานพื้นหลังหมดเวลา (โดยปกติ 30 วินาที)
| สาเหตุ | iOS | Android | สามารถป้องกันได้ |
|---|---|---|---|
| การปิดด้วยตนเองโดยผู้ใช้ | ปัดใน App Switcher | ปัดจาก Recents | ไม่ — การกระทำของผู้ใช้ |
| หน่วยความจำไม่เพียงพอ | การเตือนหน่วยความจำทำงาน | onTrimMemory / LMK | บางส่วน — การเพิ่มประสิทธิภาพหน่วยความจำ |
| การขัดข้องของแอปพลิเคชัน | NSException / สัญญาณ | UncaughtException / ANR | ใช่ — การจัดการข้อผิดพลาดและการรายงานการขัดข้อง |
| หมดเวลางานพื้นหลัง | 30 วินาทีสำหรับงานพื้นหลัง | 10 นาทีสำหรับ JobScheduler | ใช่ — การจัดตารางงานที่ถูกต้อง |
| การรีบูตระบบปฏิบัติการ | เรียก applicationWillTerminate | Broadcast ACTION_SHUTDOWN | ไม่ — เหตุการณ์ระบบ |
| การอัปเดตแอป | ไม่เกิดขึ้น (iOS Sandbox) | กระบวนการสิ้นสุดเมื่ออัปเดต APK | ไม่ — การอัปเดตระบบ |
สำหรับ iOS ให้ใช้การบันทึกคอนโซลใน applicationWillTerminate และ applicationDidFinishLaunching เพิ่มแฟล็กใน UserDefaults ทุกครั้งที่เริ่มต้น — หากแฟล็กหายไปในการเริ่มต้นครั้งถัดไป แสดงว่าแอปพลิเคชันถูกสิ้นสุดอย่างไม่ถูกต้อง บน Android ให้ใช้ ActivityManager.isBackgroundRestricted() เพื่อตรวจสอบว่าแอปพลิเคชันสามารถรันงานพื้นหลังได้หรือไม่ นอกจากนี้ ให้ตรวจสอบ onTrimMemory(TRIM_MEMORY_COMPLETE) — นี่เป็นสัญญาณว่ากระบวนการจะถูกสิ้นสุด
กฎข้อแรก — อย่าคิดว่า applicationWillTerminate หรือ onDestroy จะถูกเรียก บันทึกข้อมูลสำคัญอย่างยิ่งทุกครั้งที่เปลี่ยนจาก Active ไปยัง Background ใช้ที่เก็บแบบคีย์-ค่าสำหรับการตั้งค่าอย่างง่าย และ SQLite/Room สำหรับข้อมูลที่มีโครงสร้าง
กฎข้อที่สอง — วัดเวลาเริ่มต้นแบบเย็นและเพิ่มประสิทธิภาพ การเริ่มต้นแบบขี้เกียจ การลดงานในเธรดหลัก การโหลดทรัพยากรล่วงหน้า การใช้ SplashScreen API — ทั้งหมดนี้ช่วยปรับปรุงการรับรู้เวลาเริ่มต้น Google แนะนำ ให้การเริ่มต้นแบบเย็นน้อยกว่า 200 ms สำหรับประสบการณ์ผู้ใช้ที่ยอดเยี่ยม
กฎข้อที่สาม — ใช้การกู้คืนสถานะ บน iOS ใช้ UIApplication.stateRestorationIdentifier และ NSUserActivity บน Android ใช้ SavedStateHandle ใน ViewModel ร่วมกับ onSaveInstanceState ซึ่งจะช่วยให้ผู้ใช้ทำงานต่อจากจุดเดิมหลังจากรีสตาร์ทแอป
กฎข้อที่สี่ — จัดการ launchOptions และ Intent ที่แอปพลิเคชันถูกเริ่มต้นหลังจาก Not Running ลิงก์ลึก การแจ้งเตือนแบบพุช ลิงก์สากล — ทั้งหมดนี้ถูกส่งผ่านพารามิเตอร์เริ่มต้น นักพัฒนาต้องดึงข้อมูลนี้อย่างถูกต้องและนำทางผู้ใช้ไปยังหน้าจอที่เหมาะสม
// การจัดการ deep link หลังการเริ่มต้นแบบเย็น
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// ตรวจสอบว่ามีการแจ้งเตือนมาหรือไม่
if let notification = launchOptions?[.remoteNotification] as? [String: Any] {
handleNotification(notification)
}
// ตรวจสอบ deep link
if let url = launchOptions?[.url] as? URL {
handleDeepLink(url)
}
return true
}
private func handleDeepLink(_ url: URL) {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let screenId = components.queryItems?.first(where: { $0.name == "screen" })?.value
else { return }
openScreen(screenId)
}โค้ดแสดงการจัดการพารามิเตอร์เริ่มต้นในการเริ่มต้นแบบเย็นของ iOS launchOptions มีข้อมูลที่ระบบใช้เริ่มต้นแอปพลิเคชัน การแจ้งเตือน ลิงก์ลึก และลิงก์สากลถูกส่งผ่านพจนานุกรมนี้ นักพัฒนาต้องจัดการสถานการณ์การเริ่มต้นที่เป็นไปได้ทั้งหมดอย่างถูกต้องเพื่อให้แน่ใจว่ามีประสบการณ์ผู้ใช้ที่ราบรื่น
คำถามที่พบบ่อย
ข้อมูลที่บันทึกในที่เก็บถาวร (UserDefaults, Core Data, SharedPreferences, Room) จะถูกเก็บรักษาไว้ ข้อมูลใน RAM — ตัวแปร แคช สถานะ ViewModel ที่ไม่มี SavedStateHandle — จะสูญหายอย่างถาวร ดังนั้น การบันทึกสถานะแอปพลิเคชันทุกครั้งที่เปลี่ยนไปยัง Background จึงมีความสำคัญอย่างยิ่ง
ในระหว่างการเริ่มต้นแบบเย็น application(_:didFinishLaunchingWithOptions:) จะถูกเรียก ในระหว่างการเริ่มต้นแบบร้อน (กลับจาก Suspended) วิธีการนี้จะไม่ถูกเรียก — เฉพาะ applicationWillEnterForeground และ applicationDidBecomeActive เท่านั้นที่ถูกเรียกใช้ หากคุณต้องการดำเนินการเฉพาะในการเริ่มต้นแบบเย็น ให้ตั้งแฟล็กใน didFinishLaunchingWithOptions
ได้ Foreground Service ที่มีการแจ้งเตือนถาวรจะป้องกันไม่ให้ระบบสิ้นสุดกระบวนการ แม้ว่า Activities ทั้งหมดจะถูกทำลายแล้วก็ตาม Background Service (startService โดยไม่มี foreground) สามารถถูกหยุดโดยระบบได้ทุกเมื่อ Service ที่ทำงานอยู่หมายความว่ากระบวนการมีอยู่ และนี่ไม่ใช่ Not Running อีกต่อไป
บนซิมูเลเตอร์ iOS ให้สิ้นสุดแอปผ่าน App Switcher (Cmd+Shift+H สองครั้ง ปัดขึ้น) บนอีมูเลเตอร์ Android ให้ใช้ adb shell am force-stop com.example.app หรือปุ่ม Stop ใน Logcat หลังจากนั้น ให้เริ่มแอปพลิเคชันอีกครั้ง — นี่จะเป็นการเริ่มต้นแบบเย็นที่สะอาดจาก Not Running
Kill-switch คือคำสั่งเซิร์ฟเวอร์สำหรับการสิ้นสุดแอปพลิเคชันฉุกเฉิน ใช้ในแอปพลิเคชันธนาคารและองค์กรสำหรับการบล็อกการเข้าถึงระยะไกล หากแอปพลิเคชันได้รับคำสั่ง kill ในการเริ่มต้นแบบเย็นครั้งถัดไป มันจะบล็อก UI และขอให้ยืนยันตัวตนอีกครั้ง บน iOS kill-switch ถูกใช้งานผ่านการแจ้งเตือนระยะไกลพร้อมแฟล็กการบล็อก
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ