กระตุก — เป็นคำที่ผู้ใช้ใช้อธิบายสถานการณ์ที่แอปมือถือทำงานช้าและไม่สมำเสมอ: บางครั้งตอบสนองปกติ บางครั้งค้างกะทันหันเป็นเวลาหลายวินาที ในบริบททางเทคนิค “กระตุก” หมายถึงการรวมกันของการแล็กและการค้างขนาดเล็กที่เกิดจาก GC พาวสบ่อยครั้ง การบล็อกเธรดหลักด้วยการทำงานแบบซิงโครนัส และโครงสร้างข้อมูลที่ไม่มีประสิทธิภาพ ตามข้อมูล Android Performance Benchmarking Guide, การลดเวลาในการตอบสนองจาก 300 มิลลิวินาทีเหลือ 100 มิลลิวินาทีช่วยเพิ่มการรักษาผู้ใช้ไว้ได้ 25%. การวิจิจย์อาการกระตุกต้องใช้ทั้ง CPU และ Memory Profiling ร่วมกับการวิเคราะห์ความถี่ของการเก็บขยะ.
ประเด็นสำคัญ
กระตุก — คำแสงที่ผู้ใช้ใช้เพื่ออธิบายการทำงานของแอปที่ช้าอย่างเป็นกระยังไม่ตรงแสง แตกต่างจากแล็ก ซึ่งแสดงตัวเป็นความล่าช้าที่คงที่ อาการกระตุกคือการค้างสลับหายไม่สม่ำเสมอ: แอปอาจทำงานได้อย่างสมบูรณ์สักเผ่าวินาที แล้วก็หยุดคิดเป็นเวลา 1–3 วินาที
จากมุมมองของการโปรไฟล์ อาการกระตุก แสดงตัวเป็นชุดของเฟรมที่หายไป (jank) โดยมีความล่าช้าสูงสุดมากกว่า 100 มิลลิวินาที บนกราฟ FPS จะเห็นเป็นการตกลงอย่างกะทันหัน: 60 → 20 → 55 → 10 เฟรมต่อวินาที แตกต่างจากแล็กที่มี FPS ต่ำอย่างสม่ำเสมอ อาการกระตุกมีความแปรผันแปลงอย่างชัดเจน
เมื่อแอป กระตุก ผู้ใช้จะไม่เข้าใจตรรกะของความช้า: หน้าจออาจเลื่อนได้ลื่น แล้วก็หยุดลงกะทันหันเป็นเวลาหนึ่งวินาที สิ่งนี้ทำให้เกิดความหงุดหงิดและลดความวางใจในแอป ตามข้อมูลจาก Google 53% ของผู้ใช้ออกจากเว็บไซต์หรือแอปหากการโหลดใช้เวลามากกว่า 3 วินาที
ลักษณะที่ไม่สม่ำเสมอของอาการกระตุกชี้ว่าปัญหาเกิดจากปัจจัยที่เกิดขึ้นตามเหตุการณ์ ไม่ใช่การโอเหลี่ยงแบบคงที่ มาดูโครงการทั่วไปกันเลย
บน Android ในสภาพแวดล้อม ART การเก็บขยะจะหยุดเธรดทั้งหมดของแอปพลิเคชัน หากในโค้ดมีการสร้างออบเจกต์ชั่วคราวจำนวนมาก เช่น การสร้าง String ใหม่ผ่านการต่อคำทุกครั้งที่เรียก onBindViewHolder — GC จะทำงานบ่อยขึ้น ระยะเวลาพาวสอาจยูดนาน 5–50 มิลลิวินาทีขึ้นอยู่กับขนาดของกองและรุ่นของออบเจกต์ ผู้ใช้จะรู้สึกได้ว่าแอปหยุดคิดกะทันหัน
Room บน Android และ Core Data บน iOS รองรับคำสั่งแบบอสยิงโครนัส แต่ผู้พัฒนามักเรียก getValue() หรือดำเนินการคำสั่งผ่าน runBlocking เพื่อความสะดวก คำสั่ง SELECT ที่มี JOIN บนตารางที่มี 10,000 แถวอาจใช้เวลา 200–500 มิลลิวินาที และบล็อก UI โดยสิ้นเชิงในระยะเวลานี้
การโหลดภาพจากกล้อง (12 เมกาพิกเซล, 4000x3000 px) โดยไม่มีการย่อขนาดใช้เวลาถึง 200 มิลลิวินาที ในการแยกเป็น Bitmap หากภาพถูกโหลดแบบอสยิงโครนัส แต่ไม่มีพุลเธรดที่มีข้อจำกัด การเริ่มต้นการแยก 5–6 ภาพพร้อมกันอาจโอเหลี่ยง CPU ทำให้เกิดการกระตุกแบบเคลื่อนย้าย
การวิจิจย์การช้าลงแบบไม่สม่ำเสมอนั้นยากกว่าการวิจิจย์แล็กที่เกิดขึ้นตรงไปตรงมา, เพราะปัญหาอาจไม่เกิดขึ้นในทุกการเริ่มต้นแอป. ต้องเก็บสถิติในระยะเวลานาน
Android Studio Memory Profiler แสดงไม่เพียงการใช้หน่วยความจำ แต่ยังแสดงอีเวนต์ GC: ความถี่, ประเภท (Concurrent, Full), ระยะเวลา. หาก GC เกิดบ่อยกว่า 1 ครั้งต่อ 5 วินาทีในสภาพปกติ นั่นคือสัญญาณของการจัดสรรคมากเกินไป. การบันทึก heap dump ขณะที่เกิดอาการกระตุกจะช่วยให้เห็นว่าออบเจกต์ใดที่ใช้หน่วยความจำ
บน iOS ใช้เทมเพลต Allocations ใน Instruments เพื่อติดตามการสร้างและปลอยออบเจกต์. ใช้ Generations เพื่อถ่ายภาพหน่วยความจำระหว่างการกระทำ. Persistent objects ที่ไม่ถูกปลอยเป็นแหล่งของการสะสมหน่วยความจำและพาวสที่ตามมา
JankStats — ไลบรารี Android ที่รวมเมตริกเฟรมที่หายไปแบบเรียลไทม์. เชื่อม jank แต่ละตัวกับสถานการณ์ปัจจุบัน (เช่น เลื่อนรายการ, เปิดหน้าจอ).
ตัวอย่างการผนวก JankStats เพื่อติดตามการค้างบน Android:
class MainActivity : AppCompatActivity() {
private lateinit var jankStats: JankStats
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
jankStats = JankStats.create(this.window.decorView) { frameData ->
if (frameData.isJank()) {
Log.w("Jank", "ระยะเวลา=${frameData.durationMs}ms")
}
}
}
}
การแก้ไขต้องเป้าไปที่สาเหตุแต่ละข้อ. ไม่มีวิธีตาตาม — ต้องวิเคราะห์โปรไฟล์เฉพาะจะ.
หากรายการมี 1000+ รายการและโหลดทั้งหมดทันที — นั่นคือการกระตุกที่แน่นอน. Paging 3 บน Android และ NSFetchedResultsController บน iOS โหลดข้อมูลเป็นส่วนๆ ตามที่เลื่อน. ผู้ใช้เห็นเฉพาะ 10–20 รายการแรก, ส่วนที่เหลือโหลดในพื้นหลัง
Room โปรไฟล์คำสั่งผ่าน Inspection Tool ใน Android Studio: เวลาทำงาน, จำนวนแถวที่คืน, แผนคำสั่ง. เพิ่มดัชนีใน WHERE และ ORDER BY ลดเวลาจาก 300 มิลลิวินาทีเหลือ 5 มิลลิวินาที. บน iOS ใช้ Core Data Profiler ใน Instruments
การซิงค์พื้นหลัง, โหลดไฟล์, ประมวลผลข้อมูล — ทั้งหมดนี้ควรทำผ่าน WorkManager (Android) หรือ Background Tasks (iOS). หากเริ่มใน UI เธรด แอปจะกระตุกเมื่อทำงาน. WorkManager รับประกันการทำงานในเธรดพื้นหลังโดยคำนึงถึงแบตเตอรี่และเครือข่าย
ตัวอย่างการซิงค์พื้นหลังผ่าน WorkManager บน Android:
class SyncWorker(context: Context, params: WorkerParameters)
: CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
Log.d("Sync", "กำลังซิงค์ข้อมูลในเธรดพื้นหลัง")
syncData()
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
}
ป้องกันได้ตั้งแต่ขั้นเขียนโค้ด โดยปฏิบัติตามหลักการทำงานกับหน่วยความจำและเธรด
Baseline Profiles — รายการคลาสและเมธอดที่ Android คอมไพล์ล่วงหน้า (AOT). ไม่มีโปรไฟล์ หน้าใหม่จะคอมไพล์เมื่อเปิดครั้งแรก ทำให้เกิดความล่าช้า 100–500 มิลลิวินาที. เตรียม Baseline Profile สำหรับหน้าสำคัญ และเปิดใช้งานใน Gradle.
Hot path — โค้ดที่ทำงานทุกเฟรม: onBindViewHolder, draw, layoutSubviews. หลีกสร้างออบเจกต์ในเมธอดเหล่านี้: ใช้พุลออบเจกต์, StringBuilder, แคชสตริงและตัวจัดรูปแบบ. ทุกการจัดสรรคพิเศษจะทำให้ GC ใกล้ขึ้น
เพิ่ม Macrobenchmark ใน CI ด้วยสถานการณ์เลื่อนรายการและเปิดหน้าจอ. ตั้งเกณฑ์: 99th เปอร์เซินไทลของเวลาต่อเฟรมต้องไม่เกิน 16 มิลลิวินาที. หากเกิน บิลดจะถูกปฏเสธ
คำถามที่พบบ่อย
แล็กคือความล่าช้าคงที่ (200 มิลลิวินาทีต่อการกด). อาการกระตุก — ไม่สม่ำเสมอ: ทำงานปกติ แล้วช้า 1–3 วินาที แล้วปกติอีก. สาเหตุคือ GC พาวสหรือคำขอ SQL ซิงโครนัสต่อฐานข้อมูล
ใช้ Memory Profiler ใน Android Studio: แท็บ Memory แสดงอีเวนต์ GC พร้อมระยะเวลา. สำหรับโปรดักชัน ใช้ Firebase Performance Monitoring. บน iOS เปิด Malloc Debug และทำเครื่องหมายรุ่นการจัดสรรคใน Instruments
โดยอ้อม — ใช่. หากคำตอบจากเซิิฟเวอร์มาช้า และ UI รอ ซิงโครนัส — แอปจะค้าง. หากคำขอเป็นแบบอสยิงโครนัส แต่ประมวลผลใน UI เธรด ก็จะกระตุก. แก้ด้วยการประมวลแบบอสยิงโครนัสด้วยโครุตินและตัวบ่งบอกความคืบหน้า
เมื่อใช้ KMP ไม่ถูกต้อง อาจสร้างได้มากเกินไป ออบเจกต์ห่อหุ้มสำหรับการทำงานร่วมกัน. บน iOS นี้เพิ่มความถี่ของการจัดสรรคและทำให้เกิดการพาวส ARC. ใช้ @ObjCName, ปรับปรุง expect/actual และหลีกเรียกบ่อยเกินไป shared โค้ด จาก hot path.
การเพิ่มขนาด heap ผ่าน android:largeHeap="true" เลื่อน GC, แต่ไม่ได้กำจัดที่สาเหตุของการจัดสรรค. เมื่อ GC ทำงานในที่สุด, ระยะเวลาพาวส จะนานขึ้น เพราะ ต้องเดินอบเจกต์มากขึ้น. แนะนำ — ลดจำนวนการจัดสรรค, ไม่ใช่ขยายกอง.
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ