ทำให้หน่วยความจำที่ใช้ของแอปมีประสิทธิภาพและไม่เกิดข้อผิดพลาดในการแบ่งกลุ่มด้วย Firebase Crashlytics เซสชันนี้จะสำรวจชุดเครื่องมือแก้ปัญหาหน่วยความจำที่ครอบคลุมซึ่งมีอยู่ในคอนโซล Firebase และใน Android Studio เราจะเริ่มด้วยการพูดถึงวิธีใช้ประโยชน์จากข้อมูลเชิงลึกเกี่ยวกับ ANR และรายงาน GWP-ASan เพื่อวิเคราะห์ข้อบกพร่องด้านหน่วยความจำที่ตรวจจับได้ยาก จากนั้นจะเจาะลึกเครื่องมือล่าสุดสำหรับเหตุการณ์หน่วยความจำไม่เพียงพอ (OOM) และการปิดแอปเนื่องจากหน่วยความจำไม่เพียงพอ (MLK) ซึ่งออกแบบมาเพื่อช่วยให้คุณรับมือและปรับตัวให้เข้ากับขีดจำกัดหน่วยความจำใหม่ที่เปิดตัวใน Android 17 โดยเฉพาะ
ApplicationExitInfo เพื่อแสดงการติดตามแบบมัลติเธรดและระบุเธรดที่บล็อกรูทProfilingManager ของ Android 17 เพื่อรวบรวมสแนปชอตฮีปที่ถอดรหัสแล้ว ซึ่งตรวจสอบได้ใน Perfetto หรือผ่าน Android CLIข้อความถอดเสียง
Polina Govorkova
สวัสดีทุกคน
ยินดีต้อนรับสู่การแก้ปัญหาเกี่ยวกับหน่วยความจำด้วย Firebase Crashlytics
เดี๋ยวนะ ชื่อตำแหน่งในองค์กรคืออะไร
มาลองเปลี่ยนแนวกันสักหน่อย
การทำให้แอปมีประสิทธิภาพและทำงานได้อย่างราบรื่นด้วย Firebase Crashlytics
โอ้ ฟังดูดีขึ้นนะ
ฉันชื่อ Polina เป็นนักพัฒนาซอฟต์แวร์อาวุโสในทีม Firebase Crashlytics และเป็นผู้บรรยายในวันนี้
เมื่อถึงจุดหนึ่งในวงจรการพัฒนา คุณจะรู้สึกว่าได้แก้ไขข้อบกพร่องทั้งหมดแล้ว
ข้อผิดพลาดในการพิมพ์การแยกวิเคราะห์ JSON, การตรวจสอบตัวชี้ค่า Null ที่ถูกลืม ทั้งหมดนี้จะหายไป
คุณอาจถามตัวเองว่า "ฉันทำเสร็จแล้วหรือยัง"
เรามีข่าวร้ายจะบอกคุณ เพราะคุณไม่ได้เป็นเช่นนั้น
เพราะสิ่งที่จะเหลืออยู่คือปัญหาที่ยากจริงๆ
และผมขอยืนยันในวันนี้ว่าปัญหาที่ยากที่สุด 2 หมวดหมู่ที่คุณจะต้องเผชิญคือความสามารถในการตอบสนองและหน่วยความจำ
ข่าวดีคือเราพร้อมช่วยเหลือคุณ
วันนี้เราจะแนะนำเครื่องมือที่มีอยู่และเครื่องมือใหม่ใน Android และ Crashlytics เพื่อช่วยคุณค้นหาสาเหตุของปัญหาการตอบสนองที่แสดงผ่านข้อผิดพลาดแอปพลิเคชันไม่ตอบสนอง รวมถึงข้อยกเว้นหน่วยความจำไม่เพียงพอ หน่วยความจำรั่วไหล และความเสียหายของหน่วยความจำแบบเนทีฟ
มาดูรายละเอียดกัน
ทำไมปัญหาด้านการตอบสนองและหน่วยความจำจึงเป็นที่รู้จักกันดี
ลองดูข้อบกพร่องแบบคลาสสิก: คุณประกาศสตริง ตั้งค่าเป็น Null และพยายามอ่านความยาวของสตริง ณ จุดใดจุดหนึ่ง
สิ่งที่คุณจะได้รับ
NullPointerException ที่คุ้นเคย
แต่จริงๆ แล้วชีวิตที่นี่ง่ายมาก เพราะสแต็กเทรซของข้อยกเว้นจะชี้ไปยังบรรทัดโค้ดที่ทำให้แอปขัดข้องอย่างชัดเจน
ดูสแต็กเทรซทางด้านซ้าย
java.lang.NullPointerException
at MainActivity.onCreate(MainActivity.kt:42)
at android.app.Activity.performCreate
at android.os.Handler.dispatchMessage
at android.os.Looper.loopOnce
at android.app.ActivityThread.mainเราจะเห็นได้ทันทีว่าปัญหาอยู่ที่บรรทัด 42 ใน MainActivity.kt
และแนวทางนี้ใช้ได้กับข้อบกพร่องเล็กๆ น้อยๆ ส่วนใหญ่
ตอนนี้เมื่อเกิดปัญหาด้านการตอบสนองหรือหน่วยความจำ เช่น แอปพลิเคชันไม่ตอบสนองทางด้านขวา สแต็กเทรซแทบจะไม่ชี้ไปยังสาเหตุที่แท้จริงเลย
"main" prio=5 tid=1 Blocked
| group="main" sCount=1 dsCount=0 flags=1
| state=S schedstat=( 1450239 48192 145 )
at DataManager.getData(DataManager.kt:25)
- waiting to lock <0x0a1b> held by thread 12
at MainActivity.onResume(MainActivity.kt:50)
at android.app.Activity.performResume
at android.os.Handler.dispatchMessage
at android.os.Looper.loopOnce
at android.app.ActivityThread.mainในกรณีที่ดีที่สุด ระบบจะแสดงเฉพาะผลลัพธ์ ซึ่งเป็นเทรดหรือการจัดสรรที่ไม่เป็นอันตรายซึ่งบังเอิญทำงานอยู่ในขณะที่แอปค้างหรือหน่วยความจำไม่เพียงพอ
ทั้งที่สาเหตุของปัญหาที่แท้จริงเกิดขึ้นก่อนหน้านั้นนานแล้ว หรืออาจเกิดขึ้นที่อื่นโดยสิ้นเชิง
โชคดีที่เมื่อเกิดปัญหานี้ขึ้น คุณไม่ต้องคาดเดาไปเอง เพราะ Firebase Crashlytics มีคำตอบให้เรา
แต่ก่อนที่เราจะเริ่มแก้ปัญหา โปรดยกมือขึ้นหากคุณคุ้นเคยกับ Crashlytics
เยี่ยมเลย
ดีใจที่ได้เห็นทุกคนยกมือขึ้น
สำหรับผู้ที่เพิ่งเคยใช้งาน Firebase Crashlytics คือโซลูชัน Error Reporting แบบเรียลไทม์ที่ให้ข้อมูลภาคสนาม พร้อมด้วยข้อมูลเชิงลึกตามบริบทและที่ขับเคลื่อนด้วย AI, เคล็ดลับการแก้ปัญหา และสัญญาณที่ชัดเจนเกี่ยวกับรุ่นใหม่ๆ ทั้งหมดนี้เพื่อช่วยให้คุณแก้ไขปัญหาได้เร็วขึ้น
และ Crashlytics ก็เป็นที่ชื่นชอบของนักพัฒนาซอฟต์แวร์ โดยเป็นโซลูชันการรายงานข้อขัดข้องที่ได้รับความนิยมมากที่สุดบนอุปกรณ์เคลื่อนที่ ซึ่งขับเคลื่อนแอปที่มาพร้อมเครื่องมากกว่า 370,000 แอป รวมถึง 57% ของแอป Android ยอดนิยม
มาดูตัวอย่างที่ชัดเจนเพื่อดูว่า Crashlytics ช่วยเราติดตามข้อบกพร่องในการทำงานได้อย่างไร
วันนี้ฉันอยากเล่าเรื่องราวของ ChromaClip ให้คุณฟัง
ซึ่งเป็นเครื่องมือแยก Color Palette วิดีโอที่ฉันสร้างขึ้น และฉันอยากจะบอกคุณว่าฉันจัดการกับข้อบกพร่องด้านการปรับเปลี่ยนตามอุปกรณ์และหน่วยความจำอย่างไรในระหว่างที่พัฒนาเครื่องมือนี้
ดังนั้น แนวคิดเบื้องหลัง ChromaClip จึงเรียบง่ายมาก นั่นคือผู้ใช้เลือกวิดีโอ เช่น คลิปพระอาทิตย์ตกที่สวยงามหรือฉากที่มีการปรับสีที่ยอดเยี่ยม จากนั้นแอปจะวิเคราะห์เฟรมเพื่อสร้างจานสีที่กำหนดเอง ซึ่งผู้ใช้สามารถนำไปใช้กับการออกแบบ ศิลปะดิจิทัล มู้ดบอร์ด หรืออะไรก็ได้
เรื่องราวของเราเริ่มต้นขึ้นก่อนที่ผมจะเปิดตัวเวอร์ชัน 1.0
ฉันอยากให้แน่ใจว่าฟีเจอร์นี้ทำงานได้อย่างเสถียรก่อนเปิดตัว จึงได้ทำการทดสอบขั้นสุดท้ายในอุปกรณ์ 2 เครื่อง โดยเลือกวิดีโอ แตะปุ่มเพื่อดึง Color Palette ออกมา และโชคดีที่ได้เห็นกล่องโต้ตอบที่นักพัฒนาแอป Android ทุกคนชื่นชอบ
ChromaClip ไม่ตอบสนอง
นี่คือข้อผิดพลาดเกี่ยวกับแอปพลิเคชันไม่ตอบสนอง หรือที่เรียกว่า ANR
Polina Govorkova
แล้วเราจะทำอย่างไรในสถานการณ์นี้ นอกเหนือจากการตื่นตระหนก
ตามสัญชาตญาณแรกของเรา เนื่องจากโทรศัพท์ทดสอบเสียบเข้ากับแล็ปท็อปโดยตรง เราจึงเปิด Android Studio และตรวจสอบ Logcat
เราจะเห็นสแต็กเทรซของเทรดหลัก
14:32:15.105 1205-1250/system_process E/ActivityManager: ANR in com.chromaclip.app (com.chromaclip.app/.MainActivity)
PID: 14205
Reason: Input dispatching timed out
Dumped ANR trace to /data/anr/traces.txt
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 flags=1 obj=0x72a8c3d0 self=0x759fb92000
| sysTid=14205 nice=-10 cgrp=default sched=0/0 handle=0x7623910548
| state=S schedstat=( 1450239120 4819203 1453 ) utm=132 stm=13 core=4 HZ=100
| stack=0x7fed2a0000-0x7fed2a2000 stackSize=8MB
at sun.misc.Unsafe.park(Native method)
- waiting on an unknown object
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source:1)
at com.chromaclip.app.extractor.PaletteExtractor.extractColors(PaletteExtractor.kt:42)ข้อมูลนี้บอกอะไรกับเรา
อันดับแรก ที่ด้านบนสุด เราได้รับการยืนยันว่าเป็น ANR ที่ทำงานใน MainActivity
มีอะไรอีกไหม
เราเห็นสาเหตุแล้ว ซึ่งก็คือการส่งอินพุตหมดเวลา
ซึ่งหมายความว่าผู้ใช้แตะหน้าจอ
ใช่ เราทำ
แต่เทรดหลักไม่ว่างจนไม่สามารถประมวลผลเหตุการณ์นั้นภายในระยะหมดเวลาของ Watchdog 5 วินาทีของ Android
จากนั้นเรายังเห็นว่าระบบดัมพ์ไฟล์ลงใน traces.txt และด้านล่างนั้น logcat จะพิมพ์สแต็กเทรซสำหรับเทรดหลักของเรา
โปรดสังเกตว่าสถานะเท่ากับ S ตรงนั้น
เธรดหลักของเราไม่ได้ใช้ CPU อย่างต่อเนื่องด้วยซ้ำ
S หมายถึงการนอนหลับ (Sleeping)
เหตุใดเทรดหลักจึงหยุดทำงานขณะที่เรารอ
มาดูสแตกเฟรมอย่างละเอียดกัน
ตรงกลางของการติดตามนี้ เราจะเห็นว่าเทรดหลักของเราอยู่ใน Unsafe.park เพื่อรอออบเจ็กต์ที่ไม่รู้จัก
ทำไมเราถึงไม่รู้ว่ากำลังรออะไรอยู่
อันที่จริงแล้ว สำหรับ ANR บางรายการ เช่น เมื่อเทรดถูกบล็อกโดยการล็อกแบบซิงค์ของ Java แบบคลาสสิก สแต็กเทรซของเทรดหลักจะบอกเราว่ากำลังรออะไรอยู่ และอะไรที่กำลังถือล็อกอยู่
แต่เมื่อคุณใช้พื้นฐานการทำงานพร้อมกันในระดับที่สูงขึ้นเบื้องหลัง สิ่งที่ Logcat บอกเราก็คือเรากำลังรอออบเจ็กต์ที่ไม่รู้จัก
ดังนั้น เราจึงไม่ทราบว่าเรากำลังรอออบเจ็กต์ใด เทรดผู้ปฏิบัติงานในเบื้องหลังใดที่เรากำลังรออยู่ หรือเหตุใดจึงใช้เวลานานกว่า 5 วินาที
และเนื่องจากเรากำลังทดสอบในเครื่อง เราจึงสามารถดึง traces.txt นั้นออกจากอุปกรณ์และเลื่อนดูเทรดที่ไม่ได้ทำงานหลายสิบรายการด้วยตนเอง และอาจตามล่าหาผู้กระทำผิดหรืออาจใช้โทเค็นเพื่อถาม AI
ในบางกรณี วิธีนี้อาจได้ผล และในความเป็นจริง วิธีนี้จะได้ผลสำหรับกรณีนี้
แต่ฉันไม่อยากอ่านดัมพ์เทรดดิบด้วยตนเองจริงๆ และบอกตามตรงว่านั่นเป็นวิธีที่ล้าสมัยมาก ฉันรู้ว่าแนวทางนี้ใช้ไม่ได้เลยเมื่อเราอยู่ในขั้นตอนการผลิต
เราแค่ไม่มีสิทธิ์เข้าถึงไฟล์ traces.txt ในโทรศัพท์ของผู้ใช้โดยตรงใช่ไหม
โชคดีที่เราผสานรวมกับ Firebase Crashlytics
เนื่องจากช่วยลดความจำเป็นในการค้นหาและเข้าถึงสาเหตุของ ANR ได้แม้ในเวอร์ชันที่ใช้งานจริง
วิธีการ
นี่คือจุดที่ ApplicationExitInfo API ของ Android เข้ามามีบทบาท
ตั้งแต่ Android 11 เป็นต้นไป เมื่อใดก็ตามที่ระบบแจ้งว่ามี ANR หรือสิ้นสุดกระบวนการของแอป ระบบปฏิบัติการจะบันทึกข้อมูลเมตาการออกที่สมบูรณ์ ซึ่งรวมถึงสาเหตุที่แน่ชัดของข้อขัดข้อง สถานะหน่วยความจำ และการติดตามของระบบที่สมบูรณ์สำหรับทุกชุดข้อความที่ทำงานอยู่ในกระบวนการของคุณในขณะนั้น
และที่ดีที่สุดคือคุณไม่จำเป็นต้องติดตั้งปลั๊กอินเพิ่มเติมหรือเขียนโค้ดแม้แต่บรรทัดเดียว
ตราบใดที่คุณใช้ Crashlytics SDK 18.3.5 ขึ้นไป ฟีเจอร์นี้จะใช้งานได้ทันที
ดังนั้น ทันทีที่แอปรีสตาร์ท Crashlytics SDK จะค้นหา ApplicationExitInfo โดยอัตโนมัติ บันทึกดัมพ์แบบมัลติเธรดทั้งหมดพร้อมกับข้อมูลการตรวจวัดระยะไกลของอุปกรณ์ และส่งไปยังแบ็กเอนด์ของ Crashlytics
มาเปิดคอนโซล Crashlytics และดูว่ามีข้อมูลอะไรบ้าง
นี่คือมุมมองรายละเอียดปัญหาในคอนโซล Firebase Crashlytics สำหรับ ANR และเราจะเห็น DefaultDispatcher worker 1 รายการทันทีในแท็บสแต็กเทรซ
DefaultDispatcher-worker-1 (Root blocking)
dalvik.system.VMRuntime.allocateNode (Native method)
android.graphics.Bitmap.createBitmap (Bitmap.java:1124)
android.graphics.Bitmap.createBitmap (Bitmap.java:1091)
com.chromaclip.app.extractor.FrameDecoder.getUncompressedFrame (FrameDecoder.kt:112)
com.chromaclip.app.extractor.PaletteExtractor$processVideo$2.invokeSuspend (PaletteExtractor.kt:65)
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
kotlinx.coroutines.DispatchedTask.run (DispatchedTask.kt:106)
java.util.concurrent.ThreadPoolExecutor.runWorker (ThreadPoolExecutor.java:1167)
java.util.concurrent.ThreadPoolExecutor$Worker.run (ThreadPoolExecutor.java:641)
java.lang.Thread.run (Thread.java:923)อันที่จริงแล้ว เทรดนี้เป็นเทรดที่ Crashlytics แสดงไว้ที่ด้านบนสุด ซึ่งเป็นจุดคอขวดสำหรับเทรดหลักของเรา และ Crashlytics จะทำเครื่องหมายด้วยป้ายที่ชื่อว่า "การบล็อกรูท"
ตามที่เคล็ดลับเครื่องมือบนป้ายอธิบายไว้ เทรดผู้ปฏิบัติงานนี้เป็นเทรดที่บล็อกเทรดที่ทริกเกอร์ ANR จริงๆ
นอกจากนี้ Crashlytics ยังมีป้ายอัจฉริยะสำหรับสาเหตุอื่นๆ ด้วย
ดังนั้น หากเทรดค้างขณะอ่านไฟล์ขนาดใหญ่หรือทำการค้นหาฐานข้อมูลที่ช้ามาก แล้วทำให้เกิด ANR เทรดจะได้รับการติดแท็กเป็นการบล็อกรูท I/O
และหากมีเทรด 2 เทรดที่ติดขัดเนื่องจากรอซึ่งกันและกัน Crashlytics จะติดแท็กเทรดเหล่านั้นว่าติดล็อก
คราวนี้เรามาดูสแตกเฟรมภายในเทรดการบล็อกรูทกัน
เรามี PaletteExtractor processVideo ที่เรียกใช้ FrameDecoder getUncompressedFrame ซึ่งไฮไลต์เป็นสีน้ำเงิน และติดอยู่ภายใน createBitmap โดยพยายามจัดสรรหน่วยความจำสำหรับเฟรมเดียว
เนื่องจากเรารู้ว่าไม่ได้รอการล็อกอื่น และเรารู้ว่าไม่ได้อยู่ในการเรียกใช้เครือข่าย ผู้ต้องสงสัยหลักของเราจึงต้องเป็นหน่วยความจำ และเราสามารถยืนยันได้ทันทีโดยตรวจสอบหน่วยความจำว่างของอุปกรณ์ในคอนโซล Crashlytics ด้วยเช่นกัน
ดู 2 ตำแหน่งที่ Crashlytics แสดงข้อมูลนี้
ในแท็บข้อมูลของเหตุการณ์ที่เฉพาะเจาะจงซึ่งอยู่ถัดจากแท็บสแต็กเทรซที่เราเพิ่งสำรวจไป และที่ด้านขวาบนซึ่งแสดงค่าเฉลี่ยของเหตุการณ์ทั้งหมด
และในทั้ง 2 กรณีนี้ ดูเหมือนว่าแทบจะไม่มีอะไรเลย ไม่ถึง 100 KB ด้วยซ้ำ
ลองคิดดูนะ
สิ่งที่เกิดขึ้นที่นี่คือเนื่องจากกระบวนการไม่มีหน่วยความจำ Android Runtime จึงใช้เวลาทั้งหมดไปกับการรัน Garbage Collection อย่างบ้าคลั่งเพื่อพยายามหาพื้นที่ว่างให้เพียงพอสำหรับบิตแมปถัดไป แต่ก็ไม่เคยได้เลย
การหยุดการทำงานของเทรดผู้ปฏิบัติงานและทำให้ UI ไม่ทำงานเป็นเวลานานกว่า 5 วินาที
เราจึงมีเคล็ดลับง่ายๆ มาฝาก
ก่อนที่คุณจะได้รับ เมื่อใดก็ตามที่คุณได้รับ ANR ที่จับคู่กับ RAM ที่มีพื้นที่ว่างเหลือน้อยแบบนี้ นั่นเป็นหลักฐานที่ชัดเจนว่า ANR เป็นปัญหาหน่วยความจำรั่วไหลหรือปัญหา Bloat
คราวนี้คำถามคือแอปของเราเป็นเหยื่อหรือเป็นตัวที่กักตุนหน่วยความจำไว้เอง
ลองย้อนกลับไปดูรายการปัญหาของโปรเจ็กต์และดูว่ามีสิ่งอื่นใดที่เกี่ยวข้องอีกหรือไม่
Polina Govorkova
และดูตรงนั้นสิ
ที่ด้านล่าง เราจะเห็นรายการปัญหา และเหนือ ANR ที่มี 8 เหตุการณ์ เราจะเห็นข้อยกเว้น OutOfMemory
ดังนั้น เราอาจเป็นผู้ร้ายตัวจริงก็ได้
และนั่นก็เป็นเบาะแสสำคัญ
เนื่องจากใน Android ข้อผิดพลาด OutOfMemoryError ของ Java หรือเรียกสั้นๆ ว่า OOM ไม่ได้หมายความว่าโทรศัพท์มี RAM เหลือน้อยเท่านั้น
ซึ่งหมายความว่ากระบวนการของแอปของเราเองใช้หน่วยความจำเกินขีดจำกัดฮีป Java ของ Android Runtime
ดังนั้น ณ จุดนี้ ทฤษฎีการทำงานของฉันคือ ANR และ OOM เป็น 2 ด้านของเหรียญเดียวกัน
ChromaClip อาจติดขัดในการรวบรวมขยะนานพอที่จะทำให้เกิด ANR หรือใช้ฮีปจนหมด ซึ่งส่งผลให้เกิด OOM ทั้งนี้ขึ้นอยู่กับช่วงเวลาระหว่างการทดสอบ
มาตรวจสอบข้อยกเว้น OutOfMemory เพื่อให้แน่ใจกัน
โดยเริ่มจากสแต็กเทรซ
java.lang.OutOfMemoryError: Failed to allocate a 256 byte allocation
at android.graphics.Bitmap.createBitmap
at android.view.LayoutInflater.inflate(LayoutInflater.java:532)
at com.chromaclip.app.SettingsActivity.onCreate(SettingsActivity.kt:21)โปรดรอสักครู่
Bitmap.createBitmap ฟังดูคุ้นๆ นะ
เราเคยเห็นอันนั้นมาก่อนไหม
แต่ครั้งนี้ให้ดูว่ามีการเรียกใช้จากที่ใด
จัดสรร 256 ไบต์ไม่สำเร็จเมื่อเปิดกิจกรรมการตั้งค่า
เรากำลังตกหลุมพรางเดิมอีกครั้ง
หากแอปขัดข้องเนื่องจากไม่สามารถขยายไอคอนการตั้งค่าขนาดเล็กได้ กิจกรรมการตั้งค่าก็ไม่ใช่ตัวร้าย
เป็นเพียงผู้เรียกที่โชคร้ายซึ่งขอหน่วยความจำเพียงเล็กน้อยเมื่อไม่มีหน่วยความจำเหลืออยู่
ดังนั้น หากต้องการค้นหาตัวการที่แท้จริง เราต้องซูมออกและดูทุกอย่างที่อยู่ในหน่วยความจำเมื่อเกิดข้อขัดข้อง
กล่าวคือ เราต้องการฮีปดัมป์ของ Java
ที่โต๊ะทำงาน หากคุณทราบวิธีทริกเกอร์ OOM คุณสามารถดึงฮีปดัมป์ของ Java ด้วยตนเองได้โดยใช้เครื่องมือสร้างโปรไฟล์หน่วยความจำของ Android Studio
แต่เมื่อ OOM เกิดขึ้นโดยไม่คาดคิดในการทดสอบหรือในสภาพแวดล้อมจริง วิธีนี้จะใช้ไม่ได้ผลอีกครั้ง
Polina Govorkova
นี่จึงเป็นอีกหนึ่งช่วงเวลาที่ Firebase Crashlytics ได้เฉิดฉาย
นอกเหนือจากข้อมูลการออกจากแอปพลิเคชันแล้ว ตอนนี้ Crashlytics ยังผสานรวมกับ ProfilingManager API ของ Android ด้วย
ProfilingManager API ใน Android 17 ได้รับการเพิ่มประสิทธิภาพด้วยการทำโปรไฟล์หน่วยความจำที่ระบบทริกเกอร์ ซึ่งเป็นสิ่งที่เราต้องการในกรณีนี้
ซึ่งหมายความว่าเมื่อแอปพบข้อผิดพลาด OutOfMemory หรือละเมิดขีดจำกัดหน่วยความจำของระบบ ระบบปฏิบัติการจะจับภาพสแนปชอตฮีปในเบื้องหลัง จากนั้นใน Crashlytics คอลเล็กชันที่ทริกเกอร์จะพร้อมใช้งานทันทีโดยไม่มีค่าใช้จ่าย ตราบใดที่คุณใช้ Crashlytics SDK 20.1.0 ขึ้นไป
จากนั้น Crashlytics จะจัดกลุ่มการหยุดทำงานเนื่องจาก OOM และตัวจำกัดหน่วยความจำโดยอัตโนมัติ เราจะพูดถึงเรื่องนี้เพิ่มเติมในภายหลังในแดชบอร์ด
และเมื่อคุณเลือกใช้การรวบรวมฮีปดัมพ์ผ่านคอนโซล Firebase หรือ CLI แล้ว Crashlytics จะเริ่มรวบรวมสแนปชอตฮีปที่บันทึกไว้โดยอัตโนมัติ และยกเลิกการสร้างความสับสนให้คุณ โดยพื้นฐานแล้ว คุณจะได้รับข้อมูลเอกซเรย์แบบเต็มของทุกอย่างที่อยู่ในฮีป Java ของแอป ณ ขณะที่เกิดข้อขัดข้อง
นี่คือลักษณะของ OOM ของ ChromaClip ในคอนโซล Crashlytics เมื่อบันทึกฮีปแล้ว
ที่ด้านบน คุณจะเห็นคำอธิบายสาเหตุที่ Java ใช้ฮีปจนหมดและสาเหตุที่เกิดเหตุการณ์ดังกล่าว
จากนั้น คุณจะเห็นเคล็ดลับการแก้ปัญหาที่เป็นประโยชน์และตัวชี้ไปยังแหล่งข้อมูลภายนอกด้วย
ในแท็บสแต็กเทรซด้านล่าง เหนือกิจกรรมการตั้งค่าที่ทำให้เข้าใจผิดนั้น Crashlytics จะแจ้งว่าสแต็กเทรซ OOM ไม่ใช่เชิงกำหนด เพื่อให้ผู้ใช้ไม่ต้องเสียเวลาดูสแต็กเทรซเหมือนที่เราเพิ่งทำไป
จากนั้น Crashlytics จะแนบไฟล์ Perfetto ที่ถอดรหัสซอร์สโค้ดที่สร้างความสับสนแล้ว ซึ่งก็คือฮีปดัมป์สำหรับเซสชันนั้น
และเราจะดาวน์โหลดไฟล์ได้จากที่นี่
เราสามารถเรียกดูดัมพ์ทั้งหมดใน Google Cloud Storage หรือจะเปิดใน Perfetto UI ก็ได้
ดังนั้น การคลิก "วิเคราะห์" ใน Perfetto UI จะเปิดเครื่องมือสำรวจฮีปดัมป์ของ Perfetto
คราวนี้ ใครเคยเห็นอันนี้แล้วบ้าง ยกมือขึ้น
โอเค มีคนยกมือ 2-3 คน
สำหรับฉันแล้ว มุมมองนี้ดูน่ากลัวมาก แต่ในกรณีนี้ ปัญหากลับค่อนข้างง่าย
ดูชุดแถบขนาดใหญ่ที่ครอบคลุมเกือบตลอดทั้งฮีป Java
ดังนั้น ส่วนใหญ่แล้วจะเป็นอาร์เรย์ของออบเจ็กต์คู่ ซึ่งแต่ละออบเจ็กต์จะรวมสีและอินสแตนซ์สตริงบางรายการไว้
โห
ฉันคิดว่าฉันรู้ว่าเกิดอะไรขึ้น
โค้ดของเราจะจัดสรรสีและข้อมูลสนับสนุนบางอย่างสำหรับทุกพิกเซลในทุกเฟรม และเก็บข้อมูลเหล่านั้นไว้หลายล้านรายการในลิสต์
จึงไม่น่าแปลกใจที่กิจกรรมการตั้งค่าไม่สามารถจัดสรร 256 ไบต์เหล่านั้นได้
อย่างน้อยก็ไขปริศนาได้ข้อหนึ่งแล้ว
มาแก้ปัญหากัน
และในตอนนี้ เรามี 2 ตัวเลือก
ดังนั้น เนื่องจาก Perfetto ได้ชี้ให้เราเห็น PaletteExtractor แล้ว เราจึงสามารถเปิด IDE และเขียนลูปใหม่ด้วยตนเอง หรือใช้ Agent การเขียนโค้ด AI ก็ได้
หรือเราจะข้ามการตรวจสอบฮีปด้วยตนเองไปเลย แล้วใช้ประโยชน์จาก Android CLI แทนก็ได้
# 1. Install Android CLI & Perfetto skills
$ android skills add android-cli perfetto
# 2. List recent heap dumps from Crashlytics
$ android signals heaps list --kind oom --timeframe=90d
# 3. Download a heap dump as a Perfetto trace
$ android signals heaps download <heap-dump-id> \
--output ./heaps
# 4. Query top retained objects in the heap dump
$ trace_processor query ./heaps/oom.perfetto-trace ...
# 5. Fetch matching Crashlytics events
$ android signals heaps describe <heap-dump-id>ดังนั้น การติดตั้งทักษะ Android CLI และ Perfetto จะช่วยให้คุณแสดงรายการและดาวน์โหลดฮีปดัมป์ของ Crashlytics ที่ผ่านการถอดรหัสซอร์สโค้ดแล้ว ค้นหาออบเจ็กต์ที่คงอยู่สูงสุดจากฮีปดัมป์เหล่านี้ผ่าน trace_processor และดึงข้อมูลปัญหา Crashlytics ที่ตรงกัน เช่น คีย์ที่กำหนดเอง บันทึก เบรดครัมบ์ การเปิดตัวที่เกี่ยวข้อง และอื่นๆ ทั้งหมดนี้ทำได้จากเทอร์มินัล
หรือจะดูที่ด้านล่างของสไลด์ก็ได้
อย่าเรียกใช้คำสั่งนี้ด้วยตนเอง
อีกครั้ง ให้ถาม AI Coding Agent ที่คุณชื่นชอบด้วยภาษาธรรมชาติเพื่อตรวจสอบฮีปดัมพ์ OOM ล่าสุดโดยใช้คำสั่ง Android และส่งการแก้ไข
# Or ask your favorite LLM in natural language
> Using `android` command, inspect the latest heap dump
for my OOM issue and propose a fix
● Ran android signals heaps list · download · describe
● Analyzed ./heaps/oom.perfetto-trace (perfetto)
● Updated PaletteExtractor.kt (+6 -4 lines)และจะมีบริบทเพียงพอที่จะดำเนินการทั้งขั้นตอนให้คุณ
ซึ่งก็คือการแสดงรายการ OOM, ดาวน์โหลดการติดตาม, เรียกใช้การวิเคราะห์ และสร้างแพตช์โค้ดในโปรเจ็กต์ของคุณโดยตรง
และดูแพตช์นี้ในไฟล์ PaletteExtractor
// PaletteExtractor.kt
fun extractPalette(framePixels: IntArray): SparseIntArray {
- val pixelColors = mutableListOf<Pair<Color, String>>() // 8.3M objects per 4K frame!
- for (px in framePixels) {
- pixelColors.add(Color.valueOf(px) to "#%06X".format(px))
- }
+ val colorHistogram = SparseIntArray() // Downsampled & packed ARGB ints!
+ for (i in framePixels.indices step 16) {
+ val argb = framePixels[i]
+ colorHistogram.put(argb, colorHistogram.get(argb) + 1)
+ }
+ return colorHistogram
}ตอนนี้เราจะลดจำนวนตัวอย่างเฟรมและนับจำนวนเต็มดิบโดยตรง แทนที่จะจัดสรรออบเจ็กต์หลายล้านรายการ
ดังนั้น เรามาใช้การแก้ไข ปิดปัญหา อัปเดตเวอร์ชัน และทดสอบต่อไปกัน
หวังว่าข้อผิดพลาดหน่วยความจำไม่เพียงพอจะหายไป และเนื่องจากเราจะไม่ต้องเสียเวลาไปกับการเก็บขยะอีกต่อไป หวังว่า ANR ที่น่ารำคาญนั้นจะหายไปด้วย
เราจึงติดตั้งใช้งานบิลด์แพตช์ในอุปกรณ์ทดสอบ เลือกคลิปใหม่ และเริ่มดึงจานสี
และ UI ก็ค้างอีกครั้ง
ANR อื่นๆ
สิ่งที่น่าสนใจคือเมื่อทดสอบเพิ่มเติม เราพบว่าอุปกรณ์บางเครื่องยังคงมี ANR อยู่ แต่อุปกรณ์อื่นๆ แอปกลับขัดข้องแบบสุ่ม และค่อนข้างบ่อยด้วย บ่อยกว่า OOM เหล่านั้นด้วยซ้ำ
แต่เราแก้ไข OOM แล้ว แล้วเกิดอะไรขึ้น
Polina Govorkova
เอาล่ะ กลับไปที่ Crashlytics กัน
ข่าวดีก่อน
ดูรายการปัญหาของเรา
ข้อยกเว้นหน่วยความจำไม่เพียงพอหายไปอย่างสมบูรณ์ตามที่เราคาดหวังไว้
ข่าวร้ายคือ
ตอนนี้ ChromaClip มีชีวิตรอดนานขึ้นอีกนิด จึงมีเวลาค้นพบสิ่งที่ซ่อนอยู่ข้างใต้
ดังนั้น ANR จึงไม่หายไป
และตอนนี้เรามีการขัดข้องของระบบ 2 รายการที่ด้านล่าง ซึ่งโชคดีที่มีเหตุการณ์เพียงไม่กี่รายการ
และที่ด้านบนสุดซึ่งมี 11 เหตุการณ์ เรามี Memory Limiter Kill ซึ่งโชคดีที่ได้รับการติดป้ายว่ามีฮีปดัมป์แล้ว
เดี๋ยวนะ
การหยุดทำงานโดยตัวจำกัดหน่วยความจำคืออะไร
มีใครเคยได้ยินเรื่องนี้บ้างไหม ช่วยยกมือขึ้นหน่อย
โอเค
ไพ่ที่หายากบางชุด
มาเรียนรู้ไปด้วยกัน
หากคุณไม่เคยเห็นการปิดแอปโดย Memory Limiter มาก่อน นั่นเป็นเพราะฟีเจอร์นี้เป็นฟีเจอร์ใหม่ใน Android 17
และ Java OOM จะทริกเกอร์ก็ต่อเมื่อคุณใช้หน่วยความจำเกินขีดจำกัดฮีป Java ที่มีการจัดการ แต่เมื่อหน่วยความจำที่ใช้ทั้งหมดของแอปรวม RAM แบบเนทีฟด้วย เช่น บัฟเฟอร์กราฟิกและการจัดสรร C++ ก่อนหน้านี้จะไม่มีเพดานต่อแอป
ดังนั้นเราจึงมี Low Memory Killer ซึ่งเป็นแบบส่วนกลาง
โดยจะเข้ามาช่วยเมื่อแอปบางแอปใช้ RAM ของระบบจนหมดจนทำให้โทรศัพท์ทั้งเครื่องทำงานช้าลง
ตอนนี้ตั้งแต่ Android 17 เป็นต้นไป ตัวจำกัดหน่วยความจำจะจำกัดหน่วยความจำที่แอปแต่ละแอปใช้
ดังนั้น เมื่อกระบวนการใช้ RAM มากเกินไป ระบบปฏิบัติการจะสิ้นสุดการทำงานของแอปนั้นๆ
และ Crashlytics จะบันทึกรายละเอียดทั้งหมดเหล่านั้นและจัดกลุ่มการสิ้นสุดไว้ด้วยกันในแดชบอร์ด
ดังนั้น การคลิกเข้าไปที่การปิดแอปโดยตัวจำกัดหน่วยความจำใน Crashlytics จะทำให้คุณได้รับคำอธิบายว่าทำไมระบบปฏิบัติการจึงเข้ามาแทรกแซง เช่นเดียวกับ OOM คุณจึงไม่จำเป็นต้องจำสิ่งที่ฉันเพิ่งพูดไป
นอกจากนี้ คุณยังจะได้รับเคล็ดลับเกี่ยวกับวิธีจัดการกับปัญหาในขั้นตอนถัดไป และลิงก์ไปยังเอกสารการบังคับใช้หน่วยความจำของ Android โดยตรง
และเช่นเดียวกับ OOM สแต็กเทรซจะระบุว่าเฟรมที่ไม่มีความผิดเป็นสาเหตุเมื่อระบบปฏิบัติการหยุดทำงาน และแดชบอร์ด Crashlytics จะไฮไลต์เฟรมดังกล่าว
ปัจจุบัน การหยุดทำงานของ Memory Limiter มักเกิดขึ้นในโค้ดแบบเนทีฟ เนื่องจากสภาพแวดล้อมแบบเนทีฟไม่มีเลเยอร์ที่จัดการความปลอดภัยและการจัดสรรหน่วยความจำ
ไม่เช่นนั้นจะเกิด OOM
ดังนั้นคำถามแรกของเราคือ ออบเจ็กต์ Kotlin ของเราเก็บการจัดสรรที่ใช้หน่วยความจำเนทีฟจำนวนมากไว้หรือไม่
หรือเป็นโค้ด C++ ดิบที่รั่วไหลออกมาเอง
มาคลิก "วิเคราะห์ใน Perfetto UI" เพื่อดูสาเหตุกัน
ดูแท็บภาพรวมใน Perfetto ฮีปดัมป์ Explorer
เราเห็นว่าในส่วนข้อมูลทั่วไป RSS แบบไม่ระบุตัวตนบวกกับ Swap แสดงให้เห็นว่าหน่วยความจำของกระบวนการทั้งหมดถึง 4 GB
ตกลง
แต่ด้านล่างนั้น เราจะเห็นว่าในส่วนไบต์ที่เก็บไว้โดยฮีป ฮีปที่มีการจัดการทั้งหมดของเรา ซึ่งรวมถึงหน่วยความจำแบบเนทีฟที่เชื่อมโยงกับออบเจ็กต์ Kotlin เช่น บิตแมป มีขนาดเพียง 42 MB
ตกลง
ดังนั้นใน 10 วินาที เราจึงตัดโค้ด Kotlin และ Java ทั้งหมดออก
ดังนั้น สาเหตุจึงต้องมาจากการรั่วไหลจากเลเยอร์ C++ แบบเนทีฟ
และเนื่องจาก ChromaClip มีเลเยอร์ C++ แบบบางสำหรับการประมวลผลเฟรม เราจึงไม่ต้องมองหาที่ไหนไกล
เมื่อชี้ Agent การเขียนโค้ดหรือเครื่องมือวิเคราะห์โปรไฟล์แบบเนทีฟของ Android Studio ไปที่ NativePaletteEngine.cpp processFrame ก็จะพบบั๊กทันที
// NativePaletteEngine.cpp
void NativePaletteEngine::processFrame(const uint8_t* rgba, size_t size) {
uint8_t* scratch = new uint8_t[size]; // Temporary frame buffer
transformColorSpace(rgba, scratch, size);
// BUG: Allocated every frame, but never freed!
+ delete[] scratch; // Free native buffer when done
}ดังนั้นในทุกเฟรมวิดีโอ โค้ด C++ ของเราจะจัดสรรบัฟเฟอร์ชั่วคราว ซึ่งจะไม่ชั่วคราวหากคุณไม่ปล่อยบัฟเฟอร์นั้น ถือเป็นข้อผิดพลาดที่มือใหม่มักทำกัน
ได้เวลาเชื่อมโยงข้อมูลแล้ว
เกิดอะไรขึ้น
เมื่อเราแก้ไขการรั่วไหลของ Java แล้ว การรั่วไหลของ C++ จะมีเวลาสะสมและทำงานอย่างไม่เป็นระเบียบ
ใน Android 17 ตัวจำกัดหน่วยความจำจะตรวจจับและปิดแอป
แต่ในอุปกรณ์รุ่นเก่าที่ยังไม่มีตัวจำกัดหน่วยความจำ ปัญหานี้จะแสดงเป็น ANR
ในที่สุดทุกอย่างก็สมเหตุสมผล
มาเพิ่ม delete[] scratch ตรงนี้ อัปเดตเวอร์ชัน และเผยแพร่การอัปเดตกัน
เมื่อการปิดแอปและ ANR ของ Memory Limiter หมดไปแล้ว เราก็พร้อมที่จะเปิดตัวเวอร์ชัน 1.0
น่าตื่นเต้น
และด้วยความเร็วและความเสถียรของ ChromaClip ในตอนนี้ แอปของเราจึงได้รับความนิยมอย่างมาก
ผู้คนหลายพันคนอัปโหลดและรีมิกซ์วิดีโอทุกวัน
แต่เมื่อมีการเข้าชมทั้งหมด โปรดจำการขัดข้องของระบบที่ลึกลับซึ่งเราเห็นก่อนหน้านี้ที่ด้านล่างของแดชบอร์ด
พวกเขาก็วิ่งกันวุ่นเลย
จู่ๆ ก็มีจำนวนเพิ่มขึ้นอย่างมาก
และงานของเรายังไม่จบเพียงเท่านี้
Polina Govorkova
ดังนั้นเมื่อเปิดการขัดข้องของระบบใน Crashlytics ให้ดูที่สแต็กเทรซ
ไม่มีโค้ดแม้แต่บรรทัดเดียวจากแอปของเราเอง
RenderThread ขัดข้องลึกภายใน libc.so ขณะเพิ่มหน่วยความจำ
และเคล็ดลับอีกอย่างหนึ่งคือ เมื่อเทรดระบบขัดข้องภายในตัวจัดสรรหน่วยความจำโดยไม่มีโค้ดแอปของคุณอยู่ภายใน นั่นเป็นสัญญาณที่ชัดเจนว่าหน่วยความจำเนทีฟเสียหาย
ดังนั้น โค้ด C++ ของเราจึงทำให้บัฟเฟอร์เสียหายหรือปล่อยบัฟเฟอร์ก่อนหน้านี้ และเทรดการแสดงผลก็สะดุดกับบัฟเฟอร์ในภายหลัง
แล้วเราจะจับผู้กระทำผิดตัวจริงในขณะที่เกิดการทุจริตได้อย่างไร
Android มีเครื่องมืออันทรงพลัง 2 รายการ และ Crashlytics ก็รองรับทั้ง 2 รายการนี้ตั้งแต่แกะกล่อง
อย่างแรกคือ GWP-ASan ซึ่งเป็นตาข่ายนิรภัยน้ำหนักเบาที่สร้างขึ้นมาเพื่อการใช้งานจริงโดยเฉพาะ
โดยจะสุ่มตัวอย่างการจัดสรรฮีปแบบเนทีฟเพียงเล็กน้อยและล้อมรอบด้วยหน้าป้องกัน เพื่อให้โอเวอร์เฮดของ CPU และหน่วยความจำเกือบเป็นศูนย์
ในเซสชันของผู้ใช้หลายพันเซสชันในสภาพแวดล้อมจริง เมื่อข้อบกพร่องแตะหน่วยความจำที่ว่างหรืออยู่นอกขอบเขตในการจัดสรรที่สุ่มตัวอย่าง GWP-ASan จะตรวจจับข้อบกพร่องนั้นทันที
อย่างไรก็ตาม เนื่องจากโดยธรรมชาติแล้ว GWP-ASan เป็นแบบความน่าจะเป็น เมื่อคุณทดสอบสิ่งต่างๆ ในเครื่อง คุณจึงไม่ต้องการใช้โทรศัพท์ทดสอบเป็นเวลา 16 ชั่วโมงขึ้นไปเพื่อให้ได้ตัวอย่างแบบสุ่ม
แล้วคุณจะได้อะไรบ้าง
สำหรับการแก้ไขข้อบกพร่องในเครื่องแบบเชิงกำหนด คุณมี ARM MTE หรือ Memory Tagging Extension
MTE ใช้ตัวชี้ฮาร์ดแวร์และแท็กหน่วยความจำเพื่อตรวจสอบการจัดสรรทุกครั้ง และพร้อมใช้งานในอุปกรณ์ ARM v9 เช่น Pixel 8 ขึ้นไป
ดังนั้น ให้ใช้ร่วมกับ GWP-ASan ในเวอร์ชันที่ใช้งานจริง แล้วคุณจะได้รับประโยชน์จากทั้ง 2 อย่าง
และตามที่คุณอาจคาดไว้ เมื่อเครื่องมือใดเครื่องมือหนึ่งตรวจพบบั๊ก Crashlytics จะติดป้ายกำกับปัญหาโดยอัตโนมัติและให้คุณกรองตาม GWP-ASan หรือ MTE และปักหมุดการ์ดข้อมูลที่มีสาเหตุที่แน่ชัดของความเสียหายในแดชบอร์ด
มาดูสิ่งที่เกิดขึ้นในคอนโซล Crashlytics เมื่อ GWP-ASan ตรวจพบข้อขัดข้อง
ถัดจากแท็บสแต็กเทรซปกติ เรามีสแต็กเทรซหน่วยความจำ
และที่ด้านบน เราจะเห็นสาเหตุที่แน่นอนของความเสียหาย ซึ่งก็คือ Use After Free
และแทนที่จะแสดงข้อขัดข้องของเทรดการแสดงผลใน libc.so ตอนนี้ GWP-ASan จะแสดงประวัติทั้งหมดของบัฟเฟอร์นั้นภายใน effect_pipeline.cc ของเราเอง
ดังนั้นในส่วน "จัดสรรโดย" เราจะเห็นว่าบัฟเฟอร์สร้างขึ้นที่ใด
และใต้ Deallocated By เราจะเห็นตัวการที่ทำให้หน่วยความจำว่างจากใต้เทรดการแสดงผล
ซึ่งซ่อนอยู่ในบรรทัดที่ 208 ในไปป์ไลน์เอฟเฟกต์ของเรา
เราจึงเปิดโค้ดแบบเนทีฟใน EffectPipeline และเห็นข้อบกพร่องที่กำลังยิ้มเยาะเราอยู่
// effect_pipeline.cc
void EffectPipeline::onFilterSwitched(FilterPresetId preset) {
// BUG: Freed on worker thread while RenderThread is still reading it!
+ std::lock_guard<std::mutex> lock(lut_mutex_);
releaseLutBuffer(); // Frees old color-grading LUT
loadFilterPreset(preset); // Allocates new color-grading LUT
}เมื่อใดก็ตามที่ผู้ใช้เปลี่ยนฟิลเตอร์สีในชุดข้อความเบื้องหลัง เราจะเพิ่มพื้นที่ว่างในบัฟเฟอร์เก่าขณะที่ RenderThread ยังคงอ่านอยู่
เราจึงเพิ่ม Mutex Lock รอบๆ บัฟเฟอร์สวอป ส่งอัปเดต และสุดท้ายก็ดูรายงาน GWP-ASan และข้อขัดข้องลึกลับทั้งหมดหายไปเกือบชั่วข้ามคืน
และเมตริกผู้ใช้ที่ไม่พบข้อขัดข้องก็พุ่งสูงขึ้นเป็น 100%
ไชโย
จำได้ไหมว่าตอนเริ่มพูดคุย เราถามตัวเองว่า "ฉันทำเสร็จแล้วหรือยัง"
ก่อนที่จะถูกโจมตีโดยบอสที่ชื่อว่า "การตอบสนอง" และ "หน่วยความจำ" ในหนังสือ
ตอนนี้เราได้แก้ไขปัญหา ANR, ข้อผิดพลาดหน่วยความจำไม่เพียงพอ, การปิดแอปเนื่องจากหน่วยความจำไม่เพียงพอ และข้อขัดข้องที่เกิดจากความเสียหายของหน่วยความจำแบบเนทีฟทั้งหมดแล้ว
และแม้ว่าสแต็กเทรซจะโกหกและผู้ร้ายซ่อนตัวอยู่ที่ไหนสักแห่งในชุดข้อความใน C++, Android และ Firebase Crashlytics ก็มีเครื่องมือให้คุณติดตามและจัดส่งได้อย่างมั่นใจ
Polina Govorkova
มาสรุปสิ่งที่คุณใช้ได้ทันทีเมื่อกลับไปที่แล็ปท็อปกัน ข้อมูลเชิงลึกเกี่ยวกับเทรด ANR และข้อมูลเมตาอื่นๆ ที่เกี่ยวข้องผ่าน ApplicationExitInfo, การรวบรวมทริกเกอร์การหยุดทำงานของ OOM และตัวจำกัดหน่วยความจำโดยอัตโนมัติผ่าน ProfilingManager รวมถึงการรองรับ GWP-ASan และ Arm MTE อย่างเต็มรูปแบบสำหรับความเสียหายของหน่วยความจำแบบเนทีฟ
และเราจะเปิดตัวฟีเจอร์ฮีปดัมป์ในภาคสนามในเวอร์ชันตัวอย่างแบบส่วนตัวบน Android 17 ขึ้นไป
หากต้องการเป็นคนกลุ่มแรกๆ ที่ได้ลองใช้การเก็บรวบรวมฮีปดัมพ์อัตโนมัติใน Firebase Crashlytics และวิเคราะห์ด้วย Perfetto และ Android CLI โปรดถ่ายภาพสไลด์นี้ไว้และลงชื่อสมัครใช้ที่ลิงก์ดังกล่าว
ตอนนี้เราได้พูดคุยกันไปเกือบหมดแล้ว เราขอขอบคุณที่สละเวลามาเข้าร่วมเซสชันในวันนี้
ขอให้เทรดหลักของคุณไม่ถูกบล็อก, ฮีปของคุณสะอาด และการเปิดตัวครั้งถัดไปของคุณเสถียรที่สุดเท่าที่เคยมีมา
ขอให้สนุกกับการแก้ไขข้อบกพร่อง
ขอขอบคุณอีกครั้ง
java.lang.NullPointerException
at MainActivity.onCreate(MainActivity.kt:42)
at android.app.Activity.performCreate
at android.os.Handler.dispatchMessage
at android.os.Looper.loopOnce
at android.app.ActivityThread.main"main" prio=5 tid=1 Blocked
| group="main" sCount=1 dsCount=0 flags=1
| state=S schedstat=( 1450239 48192 145 )
at DataManager.getData(DataManager.kt:25)
- waiting to lock <0x0a1b> held by thread 12
at MainActivity.onResume(MainActivity.kt:50)
at android.app.Activity.performResume
at android.os.Handler.dispatchMessage
at android.os.Looper.loopOnce
at android.app.ActivityThread.main14:32:15.105 1205-1250/system_process E/ActivityManager: ANR in com.chromaclip.app (com.chromaclip.app/.MainActivity)
PID: 14205
Reason: Input dispatching timed out
Dumped ANR trace to /data/anr/traces.txt
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 flags=1 obj=0x72a8c3d0 self=0x759fb92000
| sysTid=14205 nice=-10 cgrp=default sched=0/0 handle=0x7623910548
| state=S schedstat=( 1450239120 4819203 1453 ) utm=132 stm=13 core=4 HZ=100
| stack=0x7fed2a0000-0x7fed2a2000 stackSize=8MB
at sun.misc.Unsafe.park(Native method)
- waiting on an unknown object
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:190)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source:1)
at com.chromaclip.app.extractor.PaletteExtractor.extractColors(PaletteExtractor.kt:42)DefaultDispatcher-worker-1 (Root blocking)
dalvik.system.VMRuntime.allocateNode (Native method)
android.graphics.Bitmap.createBitmap (Bitmap.java:1124)
android.graphics.Bitmap.createBitmap (Bitmap.java:1091)
com.chromaclip.app.extractor.FrameDecoder.getUncompressedFrame (FrameDecoder.kt:112)
com.chromaclip.app.extractor.PaletteExtractor$processVideo$2.invokeSuspend (PaletteExtractor.kt:65)
kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)
kotlinx.coroutines.DispatchedTask.run (DispatchedTask.kt:106)
java.util.concurrent.ThreadPoolExecutor.runWorker (ThreadPoolExecutor.java:1167)
java.util.concurrent.ThreadPoolExecutor$Worker.run (ThreadPoolExecutor.java:641)
java.lang.Thread.run (Thread.java:923)java.lang.OutOfMemoryError: Failed to allocate a 256 byte allocation
at android.graphics.Bitmap.createBitmap
at android.view.LayoutInflater.inflate(LayoutInflater.java:532)
at com.chromaclip.app.SettingsActivity.onCreate(SettingsActivity.kt:21)# 1. Install Android CLI & Perfetto skills
$ android skills add android-cli perfetto
# 2. List recent heap dumps from Crashlytics
$ android signals heaps list --kind oom --timeframe=90d
# 3. Download a heap dump as a Perfetto trace
$ android signals heaps download <heap-dump-id> \
--output ./heaps
# 4. Query top retained objects in the heap dump
$ trace_processor query ./heaps/oom.perfetto-trace ...
# 5. Fetch matching Crashlytics events
$ android signals heaps describe <heap-dump-id># Or ask your favorite LLM in natural language
> Using `android` command, inspect the latest heap dump
for my OOM issue and propose a fix
● Ran android signals heaps list · download · describe
● Analyzed ./heaps/oom.perfetto-trace (perfetto)
● Updated PaletteExtractor.kt (+6 -4 lines)// PaletteExtractor.kt
fun extractPalette(framePixels: IntArray): SparseIntArray {
- val pixelColors = mutableListOf<Pair<Color, String>>() // 8.3M objects per 4K frame!
- for (px in framePixels) {
- pixelColors.add(Color.valueOf(px) to "#%06X".format(px))
- }
+ val colorHistogram = SparseIntArray() // Downsampled & packed ARGB ints!
+ for (i in framePixels.indices step 16) {
+ val argb = framePixels[i]
+ colorHistogram.put(argb, colorHistogram.get(argb) + 1)
+ }
+ return colorHistogram
}// NativePaletteEngine.cpp
void NativePaletteEngine::processFrame(const uint8_t* rgba, size_t size) {
uint8_t* scratch = new uint8_t[size]; // Temporary frame buffer
transformColorSpace(rgba, scratch, size);
// BUG: Allocated every frame, but never freed!
+ delete[] scratch; // Free native buffer when done
}// effect_pipeline.cc
void EffectPipeline::onFilterSwitched(FilterPresetId preset) {
// BUG: Freed on worker thread while RenderThread is still reading it!
+ std::lock_guard<std::mutex> lock(lut_mutex_);
releaseLutBuffer(); // Frees old color-grading LUT
loadFilterPreset(preset); // Allocates new color-grading LUT
}