การค้างในการพัฒนา — สาระสำคัญ สาเหตุ และการป้องกัน

ผู้แต่ง: IT Sectr เผยแพร่เมื่อ: 2026-07-28 เวลาอ่าน: 9 นาที

การค้าง (แฮงก์) คือสถานะที่แอปพลิเคชันมือถือหยุดตอบสนองต่อการกระทำใด ๆ ของผู้ใช้เป็นเวลานาน ต่างจากแล็ก (ความช้า) และกลิทช์ (พฤติกรรมที่ไม่ถูกต้อง) การค้างจะบล็อก UI โดยสมบูรณ์: การสัมผัสไม่ได้รับการประมวลผล แอนิเมชันหยุด หน้าจอ “ค้างแข็ง” สาเหตุคือการบล็อกเธรดหลักด้วยการดำเนินการแบบซิงโครนัส เดดล็อกในโค้ดแบบหลายเธรด หรือการเก็บขยะที่ยาวนานผิดปกติ ตาม เอกสาร Apple Main Thread Checker รายงานการขัดข้องของ iOS มากกว่า 40% เกี่ยวข้องกับการบล็อกเธรดหลัก บน Android สถานการณ์ที่คล้ายกันนำไปสู่ ANR — กล่องโต้ตอบของระบบ “แอปไม่ตอบสนอง”

ประเด็นสำคัญ

  • การค้าง — การบล็อก UI โดยสมบูรณ์เป็นเวลานาน (วินาทีถึงสิบวินาที) แตกต่างจากแล็กและกลิทช์
  • สาเหตุหลัก — การบล็อกเธรดหลักโดย I/O เดดล็อกระหว่างเธรด ลูปไม่สิ้นสุด และหน่วยความจำรั่วกับ GC ที่ยาวนาน
  • การวินิจฉัย รวมถึง Main Thread Checker บน iOS, บันทึก ANR /data/anr/traces.txt บน Android และการวิเคราะห์ดัมพ์เธรด
  • การแก้ไข — การย้ายการดำเนินการที่อาจยาวทั้งหมดไปยังเธรดพื้นหลัง การใช้ Structured Concurrency และการหลีกเลี่ยง synchronized ในเธรด UI
  • การป้องกัน — StrictMode, Main Thread Checker ในสกีมา Debug, การวิเคราะห์แบบคงที่สำหรับเดดล็อก และการทดสอบเป็นระยะเพื่อวัดเวลาตอบสนอง

การค้างในการพัฒนามือถือคืออะไร

การค้าง (แฮงก์) ในแอปพลิเคชันมือถือคือสถานะที่แอปหยุดประมวลผลเหตุการณ์อินพุตและอัปเดตอินเทอร์เฟซเป็นเวลาหลายวินาทีหรือมากกว่า ในทางเทคนิค หมายความว่าเธรดหลักถูกบล็อกและไม่สามารถดำเนินการวนรอบการทำงานถัดไปได้

ความแตกต่างระหว่างการค้าง แล็ก และ ANR

แล็กคือความล่าช้าสูงสุด 500 มิลลิวินาทีซึ่งผู้ใช้สังเกตเห็นความช้าแต่แอปยังคงทำงาน การค้าง กินเวลาตั้งแต่ 1 วินาทีถึงสิบวินาที ANR บน Android เป็นกรณีพิเศษของการค้างที่กินเวลานานกว่า 5 วินาทีและถูกตรวจพบโดยระบบ ไม่ทุกการค้างนำไปสู่ ANR แต่ทุก ANR คือการค้างที่ระบบบันทึกไว้

ผลกระทบของการค้าง

บน Android การค้างนานกว่า 5 วินาทีจะเรียกกล่องโต้ตอบ ANR ที่เสนอให้ปิดแอป บน iOS ระบบมีวอทช์ด็อก — หากแอปไม่ตอบสนองต่อเหตุการณ์เป็นเวลา 10–20 วินาที วอทช์ด็อกจะยุติกระบวนการด้วยรหัส 0x8badf00d (ate bad food) ผู้ใช้เพียงเห็นแอปปิดอย่างกะทันหันไปยังหน้าจอหลัก

สาเหตุของการค้างบน Android และ iOS

การดำเนินการใด ๆ ที่ใช้เวลานานกว่า 100 มิลลิวินาทีและทำงานบนเธรดหลักอาจทำให้เกิดการค้างได้ มาดูแหล่งที่มาหลักของการบล็อกกัน

I/O แบบซิงโครนัสในเธรด UI

การอ่านไฟล์ขนาดใหญ่ คำขอเครือข่ายโดยไม่ไม่ประสานเวลา การบันทึกข้อมูลใน 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 ซึ่งบันทึกสแต็กการเรียกที่ซ้ำไม่สิ้นสุด

  • Android — Cursor ที่ไม่ได้ปิด คำขอแบบซิงโครนัสผ่าน execute() แทน enqueue(), FileInputStream.read() ในเธรด UI
  • iOS — performSelectorOnMainThread:withObject:waitUntilDone:YES, การเริ่ม NSURLConnection sendSynchronousRequest, การโหลดภาพด้วย dataWithContentsOfURL
  • ข้ามแพลตฟอร์ม — Flutter compute โดยไม่มี isolate เฉพาะ, React Native NativeModule แบบซิงโครนัส

วิธีวินิจฉัยการค้าง

การวินิจฉัยการค้างต้องใช้เครื่องมือที่สามารถบันทึกสถานะของทุกเธรดในขณะที่เกิดการบล็อก

บันทึก ANR บน Android

ในแต่ละ ANR ระบบ Android จะบันทึกไฟล์ /data/anr/traces.txt ที่มีดัมพ์สแต็กของแต่ละเธรดของแอป การวิเคราะห์ไฟล์นี้เป็นวิธีการวินิจฉัยหลัก: ค้นหาเธรด main และดูว่ามันหยุดที่เมธอดใด หากสแต็กสิ้นสุดที่ Thread.sleep, InputStream.read หรือ Lock.lock — พบสาเหตุแล้ว

Stackshot บน iOS

Xcode สามารถถ่าย Stackshot — ภาพรวมสแนปช็อตของสแต็กทั้งหมด — เมื่อแอปค้าง (สัญญาณ SIGSTOP) เปิดใช้งาน “Logging” → “Include Stackshot Logs” ในสกีมา เมื่อเกิดการขัดข้องด้วยรหัส 0x8badf00d ให้แยกบันทึกการขัดข้องจาก Devices & Simulators และค้นหาเธรด com.apple.main-thread ที่มีสแต็กติดค้าง

Main Thread Checker ใน Xcode

Main Thread Checker ตรวจสอบการเรียก UIKit จากเธรดพื้นหลังโดยอัตโนมัติขณะที่แอปทำงาน เปิดใช้งานในสกีมา (Diagnostics → Main Thread Checker) คำเตือนแต่ละรายการเป็นสาเหตุที่เป็นไปได้ของการค้าง โดยเฉพาะอย่างยิ่งหากเกิดขึ้นใน closure completionHandler ของคำขอเครือข่าย

ตัวอย่างการตรวจจับการบล็อกผ่าน StrictMode บน Android:

kotlin
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())
    }
}

วิธีการกำจัดการบล็อก UI

การกำจัดการค้างเริ่มต้นด้วยการย้ายการดำเนินการที่อาจยาวทั้งหมดไปยังเธรดพื้นหลัง มาดูเทคนิคเฉพาะสำหรับแต่ละแพลตฟอร์มกัน

การทำงานพร้อมกันแบบมีโครงสร้างกับคอรูทีน

Kotlin Coroutines กับ viewModelScope.launch(Dispatchers.IO) รับประกันว่าการดำเนินการเครือข่ายหรือการอ่านฐานข้อมูลจะทำงานบนเธรดพื้นหลัง Dispatchers.Main ใช้สำหรับการอัปเดต UI เท่านั้น สำคัญ: ฟังก์ชัน suspend ทั้งหมดต้องมีโครงสร้าง — คอรูทีนลูกจะถูกยกเลิกเมื่อยกเลิกคอรูทีนแม่ ป้องกันการรั่วไหลของเธรด

คิวแบบไม่ประสานเวลาบน iOS

Grand Central Dispatch กับ DispatchQueue.global(qos: .userInitiated) สำหรับงานพื้นหลังและ DispatchQueue.main.async สำหรับการอัปเดต UI เป็นรูปแบบมาตรฐาน หลีกเลี่ยง sync() บนคิวหลัก — นี่คือเดดล็อกที่แน่นอน ใช้ async/await (Swift 5.5+) สำหรับโค้ดแบบไม่ประสานเวลาที่อ่านง่ายกว่าพร้อมการกลับไปยังเธรดหลักโดยอัตโนมัติผ่าน MainActor

การหลีกเลี่ยง synchronized ในเธรด UI

บล็อก synchronized ใน Kotlin และ @synchronized ใน Swift บนเธรดหลักเป็นอันตราย: หากเธรดอื่นได้ล็อกนี้ไปแล้ว เธรดหลักจะค้างรอ ใช้ประเภท อะตอมิก (AtomicInteger, คุณสมบัติอะตอมิกใน Swift) หรือคิวแบบลำดับแทนการล็อก

ตัวอย่างการโหลดข้อมูลแบบไม่ประสานเวลากับคอรูทีนบน Android:

kotlin
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

กำหนดค่า StrictMode ด้วย penaltyDeath สำหรับนโยบายเธรด — สิ่งนี้จะทำให้แอปขัดข้องทันทีเมื่อตรวจพบการเรียกเครือข่ายหรือ I/O ดิสก์บนเธรดหลัก นักพัฒนาไม่สามารถเพิกเฉยต่อปัญหาได้ ในบิลด์โปรดักชัน ให้ใช้ penaltyLog เพื่อเก็บสถิติโดยไม่ทำให้แอปขัดข้อง

Main Thread Checker ในสกีมา Debug

บน iOS เปิดใช้งาน Main Thread Checker ในสกีมา Debug และกำหนดค่า CI ให้รันการทดสอบด้วยตัวเลือกนี้ หากการทดสอบมีการเรียก UIKit จากเธรดพื้นหลัง — ควรล้มเหลว นี่เป็นวิธีเดียวที่เชื่อถือได้ในการระบุปัญหาก่อนส่งไปยัง TestFlight

การตรวจสอบโดยเพื่อนร่วมงานกับการตรวจสอบหลายเธรด

เพิ่มจุดบังคับในกระบวนการตรวจสอบโค้ด: ตรวจสอบว่าการเรียกเครือข่าย การดำเนินการไฟล์ การเข้าถึงฐานข้อมูล หรือการคำนวณหนักใด ๆ ทำงานบนเธรดพื้นหลัง เดดล็อก สามารถตรวจพบได้ด้วยเครื่องมือวิเคราะห์แบบคงที่: Infer ของ Facebook และ Thread Safety Checker ของ Xcode ค้นหาล็อกที่อาจเกิดขึ้นก่อนรันไทม์

  • Android — StrictMode, Infer, Android Lint Multithread, Kotlin Coroutines กับ viewModelScope
  • iOS — Main Thread Checker, TSAN (Thread Sanitizer), Xcode Analyze, Swift async/await กับ MainActor
  • ข้ามแพลตฟอร์ม — Flutter compute isolate, React Native interaction manager กับ requestAnimationFrame

คำถามที่พบบ่อย

ความแตกต่างระหว่างการค้างและ ANR คืออะไร?

ANR (Application Not Responding) คือการแจ้งเตือนระบบ Android ที่ปรากฏเมื่อเธรดหลักค้างนานกว่า 5 วินาที การค้างเป็นแนวคิดที่กว้างกว่า: การบล็อก UI ใด ๆ ไม่ว่าระยะเวลาใด iOS ไม่มี ANR แต่มีวอทช์ด็อกที่มีการหมดเวลา 10–20 วินาที

วิธีอ่าน traces.txt บน Android?

ไฟล์อยู่ที่ /data/anr/traces.txt การเข้าถึงต้องใช้สิทธิ์ root หรือ adb shell: รัน adb shell cat /data/anr/traces.txt \> traces.txt ด้วยสิทธิ์ root ในสแต็ก ค้นหาเธรด “main” — เมธอดที่ถูกเรียกสุดท้ายบ่งชี้สาเหตุของการบล็อก

ทำไมแอปค้างบน iOS แต่ไม่ขัดข้อง?

หากการค้างกินเวลาน้อยกว่า 10 วินาที วอทช์ด็อกจะไม่ทำงาน และแอปจะ “ค้าง” จนกว่าการดำเนินการที่บล็อกจะเสร็จสมบูรณ์ ผู้ใช้ไม่เห็นการขัดข้องแต่รู้สึกหงุดหงิด ในการตรวจจับกรณีดังกล่าว ให้ใช้ MetricKit กับร่องรอยเวลาดำเนินการที่กำหนดเอง

วิธีทดสอบแอปสำหรับการค้าง?

ใช้ การทดสอบ UI โดยตรวจสอบว่าหน้าจอเปิดในเวลา < 1 วินาที เพิ่มการวัดเวลาระหว่างการแตะและการปรากฏของหน้าจอถัดไปใน CI บน Android ให้ใช้ Espresso กับ IdlingResource เพื่อรอการดำเนินการแบบไม่ประสานเวลา บน iOS ให้ใช้ XCTest กับ XCTWaiter เพื่อตรวจสอบเวลาโหลด

SwiftUI สามารถทำให้เกิดการค้างได้หรือไม่?

SwiftUI ตัวมันเองไม่ทำให้เกิดการค้าง แต่การคำนวณที่ซับซ้อนในพร็อพเพอร์ตี้ body ทำให้เกิด หาก body ใช้เวลา 500 มิลลิวินาทีในการคำนวณเนื่องจากการดำเนินการหนัก UI จะค้าง วิธีแก้คือย้ายการคำนวณไปยัง Task.detached และอัปเดต @State แบบไม่ประสานเวลาบนแอกเตอร์หลัก

สรุป

  • การค้าง — การบล็อก UI โดยสมบูรณ์เป็นวินาทีถึงสิบวินาที เกิดจากการบล็อกเธรดหลัก เดดล็อก หรือลูปไม่สิ้นสุด
  • การวินิจฉัย — /data/anr/traces.txt บน Android, Stackshot และ Main Thread Checker บน iOS
  • สาเหตุหลัก — I/O แบบซิงโครนัส เดดล็อกระหว่างเธรด รีเคอร์ชันไม่สิ้นสุด GC ยาว
  • การแก้ไข — คอรูทีนกับดิสแพตเชอร์ที่ถูกต้อง, async/await กับ MainActor, การย้ายการดำเนินการ I/O ทั้งหมดไปยังเธรดพื้นหลัง
  • การป้องกัน — StrictMode กับ penaltyDeath, Main Thread Checker, การวิเคราะห์แบบคงที่สำหรับเดดล็อก (Infer, TSAN)
  • บน Android การค้าง > 5 วินาที = ANR; บน iOS > 10–20 วินาที = วอทช์ด็อกขัดข้อง (0x8badf00d)
  • คำแนะนำ: เปิดใช้งาน Thread Sanitizer ในสกีมา Debug และกำหนดค่า CI ให้รันการทดสอบด้วย TSAN เพื่อตรวจจับ data race และเดดล็อก

เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร

IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ

ปรึกษาโครงการ

อ่านเพิ่มเติม