การค้าง (แฮงก์) คือสถานะที่แอปพลิเคชันมือถือหยุดตอบสนองต่อการกระทำใด ๆ ของผู้ใช้เป็นเวลานาน ต่างจากแล็ก (ความช้า) และกลิทช์ (พฤติกรรมที่ไม่ถูกต้อง) การค้างจะบล็อก UI โดยสมบูรณ์: การสัมผัสไม่ได้รับการประมวลผล แอนิเมชันหยุด หน้าจอ “ค้างแข็ง” สาเหตุคือการบล็อกเธรดหลักด้วยการดำเนินการแบบซิงโครนัส เดดล็อกในโค้ดแบบหลายเธรด หรือการเก็บขยะที่ยาวนานผิดปกติ ตาม เอกสาร Apple Main Thread Checker รายงานการขัดข้องของ iOS มากกว่า 40% เกี่ยวข้องกับการบล็อกเธรดหลัก บน Android สถานการณ์ที่คล้ายกันนำไปสู่ ANR — กล่องโต้ตอบของระบบ “แอปไม่ตอบสนอง”
ประเด็นสำคัญ
การค้าง (แฮงก์) ในแอปพลิเคชันมือถือคือสถานะที่แอปหยุดประมวลผลเหตุการณ์อินพุตและอัปเดตอินเทอร์เฟซเป็นเวลาหลายวินาทีหรือมากกว่า ในทางเทคนิค หมายความว่าเธรดหลักถูกบล็อกและไม่สามารถดำเนินการวนรอบการทำงานถัดไปได้
แล็กคือความล่าช้าสูงสุด 500 มิลลิวินาทีซึ่งผู้ใช้สังเกตเห็นความช้าแต่แอปยังคงทำงาน การค้าง กินเวลาตั้งแต่ 1 วินาทีถึงสิบวินาที ANR บน Android เป็นกรณีพิเศษของการค้างที่กินเวลานานกว่า 5 วินาทีและถูกตรวจพบโดยระบบ ไม่ทุกการค้างนำไปสู่ ANR แต่ทุก ANR คือการค้างที่ระบบบันทึกไว้
บน Android การค้างนานกว่า 5 วินาทีจะเรียกกล่องโต้ตอบ ANR ที่เสนอให้ปิดแอป บน iOS ระบบมีวอทช์ด็อก — หากแอปไม่ตอบสนองต่อเหตุการณ์เป็นเวลา 10–20 วินาที วอทช์ด็อกจะยุติกระบวนการด้วยรหัส 0x8badf00d (ate bad food) ผู้ใช้เพียงเห็นแอปปิดอย่างกะทันหันไปยังหน้าจอหลัก
การดำเนินการใด ๆ ที่ใช้เวลานานกว่า 100 มิลลิวินาทีและทำงานบนเธรดหลักอาจทำให้เกิดการค้างได้ มาดูแหล่งที่มาหลักของการบล็อกกัน
การอ่านไฟล์ขนาดใหญ่ คำขอเครือข่ายโดยไม่ไม่ประสานเวลา การบันทึกข้อมูลใน SharedPreferences ด้วยเมธอด apply แบบซิงโครนัสตามด้วย commit — การดำเนินการทั้งหมดนี้บล็อก เธรดหลัก บน Android การอ่านไฟล์ 10 MB แบบซิงโครนัสอาจใช้เวลา 200–500 มิลลิวินาทีขึ้นอยู่กับความเร็วของหน่วยความจำแฟลช บน iOS การโหลด URLSession แบบซิงโครนัสโดยไม่มี completionHandler จะบล็อก UI ตลอดเวลาตอบสนองของเซิร์ฟเวอร์
เมื่อสองเธรดรอทรัพยากรที่ยึดไว้ซึ่งกันและกัน จะเกิด เดดล็อก ในแอปพลิเคชันมือถือ สถานการณ์ทั่วไปคือเธรด A ล็อก Lock1 และรอ Lock2 ในขณะที่เธรด B ล็อก Lock2 และรอ Lock1 ทั้งสองเธรดค้างตลอดไป หากหนึ่งในนั้นคือเธรดหลัก แอปพลิเคชันจะค้างโดยสมบูรณ์
ข้อผิดพลาดทางตรรกะ — ตัวอย่างเช่น while(true) โดยไม่มีเงื่อนไขออกหรือรีเคอร์ชันโดยไม่มีกรณีพื้นฐาน — นำไปสู่การทำงานไม่สิ้นสุดบนเธรดหลัก Android ตรวจพบผ่าน ANR หลังจาก 5 วินาที iOS — ผ่าน Stackshot ซึ่งบันทึกสแต็กการเรียกที่ซ้ำไม่สิ้นสุด
การวินิจฉัยการค้างต้องใช้เครื่องมือที่สามารถบันทึกสถานะของทุกเธรดในขณะที่เกิดการบล็อก
ในแต่ละ ANR ระบบ Android จะบันทึกไฟล์ /data/anr/traces.txt ที่มีดัมพ์สแต็กของแต่ละเธรดของแอป การวิเคราะห์ไฟล์นี้เป็นวิธีการวินิจฉัยหลัก: ค้นหาเธรด main และดูว่ามันหยุดที่เมธอดใด หากสแต็กสิ้นสุดที่ Thread.sleep, InputStream.read หรือ Lock.lock — พบสาเหตุแล้ว
Xcode สามารถถ่าย Stackshot — ภาพรวมสแนปช็อตของสแต็กทั้งหมด — เมื่อแอปค้าง (สัญญาณ SIGSTOP) เปิดใช้งาน “Logging” → “Include Stackshot Logs” ในสกีมา เมื่อเกิดการขัดข้องด้วยรหัส 0x8badf00d ให้แยกบันทึกการขัดข้องจาก Devices & Simulators และค้นหาเธรด com.apple.main-thread ที่มีสแต็กติดค้าง
Main Thread Checker ตรวจสอบการเรียก UIKit จากเธรดพื้นหลังโดยอัตโนมัติขณะที่แอปทำงาน เปิดใช้งานในสกีมา (Diagnostics → Main Thread Checker) คำเตือนแต่ละรายการเป็นสาเหตุที่เป็นไปได้ของการค้าง โดยเฉพาะอย่างยิ่งหากเกิดขึ้นใน closure completionHandler ของคำขอเครือข่าย
ตัวอย่างการตรวจจับการบล็อกผ่าน StrictMode บน Android:
class App : Application() {
override fun onCreate() {
super.onCreate()
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.build())
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyDeath()
.build())
}
}
การกำจัดการค้างเริ่มต้นด้วยการย้ายการดำเนินการที่อาจยาวทั้งหมดไปยังเธรดพื้นหลัง มาดูเทคนิคเฉพาะสำหรับแต่ละแพลตฟอร์มกัน
Kotlin Coroutines กับ viewModelScope.launch(Dispatchers.IO) รับประกันว่าการดำเนินการเครือข่ายหรือการอ่านฐานข้อมูลจะทำงานบนเธรดพื้นหลัง Dispatchers.Main ใช้สำหรับการอัปเดต UI เท่านั้น สำคัญ: ฟังก์ชัน suspend ทั้งหมดต้องมีโครงสร้าง — คอรูทีนลูกจะถูกยกเลิกเมื่อยกเลิกคอรูทีนแม่ ป้องกันการรั่วไหลของเธรด
Grand Central Dispatch กับ DispatchQueue.global(qos: .userInitiated) สำหรับงานพื้นหลังและ DispatchQueue.main.async สำหรับการอัปเดต UI เป็นรูปแบบมาตรฐาน หลีกเลี่ยง sync() บนคิวหลัก — นี่คือเดดล็อกที่แน่นอน ใช้ async/await (Swift 5.5+) สำหรับโค้ดแบบไม่ประสานเวลาที่อ่านง่ายกว่าพร้อมการกลับไปยังเธรดหลักโดยอัตโนมัติผ่าน MainActor
บล็อก synchronized ใน Kotlin และ @synchronized ใน Swift บนเธรดหลักเป็นอันตราย: หากเธรดอื่นได้ล็อกนี้ไปแล้ว เธรดหลักจะค้างรอ ใช้ประเภท อะตอมิก (AtomicInteger, คุณสมบัติอะตอมิกใน Swift) หรือคิวแบบลำดับแทนการล็อก
ตัวอย่างการโหลดข้อมูลแบบไม่ประสานเวลากับคอรูทีนบน Android:
class DataViewModel : ViewModel() {
private val _data = MutableStateFlow<List<Item>>(emptyList())
val data: StateFlow<List<Item>> = _data.asStateFlow()
fun loadData() {
viewModelScope.launch(Dispatchers.IO) {
val result = fetchFromNetwork()
_data.emit(result)
}
}
}
การผสมผสานของเครื่องมือ หลักการทางสถาปัตยกรรม และกระบวนการตรวจสอบโค้ดช่วยป้องกันการค้างอย่างเป็นระบบ
กำหนดค่า StrictMode ด้วย penaltyDeath สำหรับนโยบายเธรด — สิ่งนี้จะทำให้แอปขัดข้องทันทีเมื่อตรวจพบการเรียกเครือข่ายหรือ I/O ดิสก์บนเธรดหลัก นักพัฒนาไม่สามารถเพิกเฉยต่อปัญหาได้ ในบิลด์โปรดักชัน ให้ใช้ penaltyLog เพื่อเก็บสถิติโดยไม่ทำให้แอปขัดข้อง
บน iOS เปิดใช้งาน Main Thread Checker ในสกีมา Debug และกำหนดค่า CI ให้รันการทดสอบด้วยตัวเลือกนี้ หากการทดสอบมีการเรียก UIKit จากเธรดพื้นหลัง — ควรล้มเหลว นี่เป็นวิธีเดียวที่เชื่อถือได้ในการระบุปัญหาก่อนส่งไปยัง TestFlight
เพิ่มจุดบังคับในกระบวนการตรวจสอบโค้ด: ตรวจสอบว่าการเรียกเครือข่าย การดำเนินการไฟล์ การเข้าถึงฐานข้อมูล หรือการคำนวณหนักใด ๆ ทำงานบนเธรดพื้นหลัง เดดล็อก สามารถตรวจพบได้ด้วยเครื่องมือวิเคราะห์แบบคงที่: Infer ของ Facebook และ Thread Safety Checker ของ Xcode ค้นหาล็อกที่อาจเกิดขึ้นก่อนรันไทม์
คำถามที่พบบ่อย
ANR (Application Not Responding) คือการแจ้งเตือนระบบ Android ที่ปรากฏเมื่อเธรดหลักค้างนานกว่า 5 วินาที การค้างเป็นแนวคิดที่กว้างกว่า: การบล็อก UI ใด ๆ ไม่ว่าระยะเวลาใด iOS ไม่มี ANR แต่มีวอทช์ด็อกที่มีการหมดเวลา 10–20 วินาที
ไฟล์อยู่ที่ /data/anr/traces.txt การเข้าถึงต้องใช้สิทธิ์ root หรือ adb shell: รัน adb shell cat /data/anr/traces.txt \> traces.txt ด้วยสิทธิ์ root ในสแต็ก ค้นหาเธรด “main” — เมธอดที่ถูกเรียกสุดท้ายบ่งชี้สาเหตุของการบล็อก
หากการค้างกินเวลาน้อยกว่า 10 วินาที วอทช์ด็อกจะไม่ทำงาน และแอปจะ “ค้าง” จนกว่าการดำเนินการที่บล็อกจะเสร็จสมบูรณ์ ผู้ใช้ไม่เห็นการขัดข้องแต่รู้สึกหงุดหงิด ในการตรวจจับกรณีดังกล่าว ให้ใช้ MetricKit กับร่องรอยเวลาดำเนินการที่กำหนดเอง
ใช้ การทดสอบ UI โดยตรวจสอบว่าหน้าจอเปิดในเวลา < 1 วินาที เพิ่มการวัดเวลาระหว่างการแตะและการปรากฏของหน้าจอถัดไปใน CI บน Android ให้ใช้ Espresso กับ IdlingResource เพื่อรอการดำเนินการแบบไม่ประสานเวลา บน iOS ให้ใช้ XCTest กับ XCTWaiter เพื่อตรวจสอบเวลาโหลด
SwiftUI ตัวมันเองไม่ทำให้เกิดการค้าง แต่การคำนวณที่ซับซ้อนในพร็อพเพอร์ตี้ body ทำให้เกิด หาก body ใช้เวลา 500 มิลลิวินาทีในการคำนวณเนื่องจากการดำเนินการหนัก UI จะค้าง วิธีแก้คือย้ายการคำนวณไปยัง Task.detached และอัปเดต @State แบบไม่ประสานเวลาบนแอกเตอร์หลัก
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ