Giữ cho mức sử dụng bộ nhớ của ứng dụng luôn gọn nhẹ và không bị lỗi phân đoạn bằng Firebase Crashlytics. Phiên này sẽ khám phá bộ công cụ khắc phục sự cố về bộ nhớ toàn diện có trong bảng điều khiển của Firebase và trong Android Studio. Trước tiên, chúng ta sẽ tìm hiểu cách tận dụng thông tin chi tiết về lỗi ANR và báo cáo GWP-ASan để chẩn đoán các lỗi bộ nhớ khó phát hiện, sau đó tìm hiểu sâu về công cụ mới nhất của chúng tôi cho các sự kiện Hết bộ nhớ (OOM) và Giới hạn bộ nhớ (MLK), được thiết kế riêng để giúp bạn điều hướng và thích ứng với các giới hạn bộ nhớ mới được giới thiệu trong Android 17.
ApplicationExitInfo để hiển thị dấu vết đa luồng và xác định các luồng chặn gốc.ProfilingManager của Android 17 để thu thập snapshot của vùng nhớ heap đã được giải mã, có thể kiểm tra trong Perfetto hoặc thông qua Android CLI.Bản chép lời
Polina Govorkova
Chào các bạn!
Chào mừng bạn đến với bài viết Khắc phục sự cố về bộ nhớ bằng Firebase Crashlytics.
Khoan đã, chức danh gì mà nghe oách thế.
Hãy thay đổi một chút.
Giữ cho ứng dụng luôn gọn nhẹ và hiệu quả nhờ Firebase Crashlytics.
Ồ, nghe có vẻ hay hơn đấy.
Tôi là Polina, nhà phát triển phần mềm cấp cao trong nhóm Firebase Crashlytics và là người trình bày hôm nay.
Tại một thời điểm nào đó trong chu kỳ phát triển, bạn sẽ cảm thấy như mình đã giải quyết được tất cả lỗi.
Những lỗi đánh máy khi phân tích cú pháp JSON, những lần quên kiểm tra con trỏ rỗng, tất cả đều biến mất!
Và bạn có thể tự hỏi: "Tôi đã hoàn tất chưa?"
Tôi có tin không vui cho bạn, vì bạn không phải là người duy nhất.
Vì những gì bạn còn lại thực sự là những vấn đề khó khăn.
Và hôm nay, tôi khẳng định rằng hai loại vấn đề khó khăn nhất mà bạn sẽ gặp phải là khả năng thích ứng và bộ nhớ.
Tin vui là chúng tôi luôn sẵn sàng hỗ trợ bạn.
Hôm nay, chúng ta sẽ tìm hiểu các công cụ hiện có và hoàn toàn mới trong Android và Crashlytics để giúp bạn tìm ra nguyên nhân gốc rễ của các vấn đề về khả năng phản hồi biểu hiện qua lỗi Ứng dụng không phản hồi, cũng như các ngoại lệ về hết bộ nhớ, rò rỉ bộ nhớ và hỏng bộ nhớ gốc.
Cùng bắt đầu nào.
Tại sao các vấn đề về khả năng thích ứng và bộ nhớ lại nổi tiếng đến vậy?
Hãy xem xét một lỗi kinh điển: bạn khai báo một chuỗi, đặt chuỗi đó thành rỗng và tại một thời điểm nào đó, bạn cố gắng đọc độ dài của chuỗi đó.
Bạn sẽ nhận được gì?
Một lỗi NullPointerException quen thuộc.
Nhưng thực ra, việc này rất dễ dàng vì dấu vết ngăn xếp ngoại lệ trỏ đến chính xác dòng mã đã làm hỏng ứng dụng của bạn.
Hãy xem dấu vết ngăn xếp ở bên trái.
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.mainChúng ta có thể biết ngay rằng vấn đề nằm ở dòng 42 trong MainActivity.kt.
Và phương pháp này đúng với hầu hết các lỗi nhỏ.
Giờ đây, khi xảy ra vấn đề về khả năng phản hồi hoặc bộ nhớ, chẳng hạn như lỗi Ứng dụng không phản hồi ở bên phải, dấu vết ngăn xếp hầu như không bao giờ chỉ ra thủ phạm thực sự.
"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.mainTrong trường hợp tốt nhất, công cụ này chỉ cho bạn thấy hiệu ứng, tức là một số luồng hoặc quá trình phân bổ vô hại vừa xảy ra tại thời điểm ứng dụng bị treo hoặc hết bộ nhớ.
Trong khi đó, nguyên nhân gốc thực sự đã xảy ra từ trước đó rất lâu, hoặc có thể là ở một nơi hoàn toàn khác.
May mắn là khi gặp những vấn đề này, bạn không cần phải đoán mò vì Firebase Crashlytics có câu trả lời cho chúng ta.
Nhưng trước khi bắt tay vào khắc phục sự cố, hãy giơ tay nếu bạn đã quen thuộc với Crashlytics.
Ồ, tuyệt vời.
Thật vui khi thấy nhiều người giơ tay như vậy.
Đối với những người mới sử dụng, Firebase Crashlytics là một giải pháp báo cáo lỗi theo thời gian thực, cung cấp dữ liệu thực tế cùng với thông tin chi tiết theo bối cảnh và dựa trên AI, mẹo khắc phục sự cố và tín hiệu rõ ràng về các bản phát hành mới, tất cả đều nhằm giúp bạn khắc phục sự cố nhanh hơn.
Crashlytics được các nhà phát triển của chúng tôi yêu thích, là giải pháp báo cáo sự cố được sử dụng nhiều nhất trên thiết bị di động, hỗ trợ hơn 370.000 ứng dụng gốc, bao gồm 57% ứng dụng Android hàng đầu.
Để xem cách Crashlytics giúp chúng ta theo dõi lỗi trong thực tế, hãy xem một ví dụ cụ thể.
Hôm nay, tôi muốn kể cho bạn nghe câu chuyện về ChromaClip.
Đây là một công cụ trích xuất bảng màu video mà tôi đang xây dựng và tôi muốn chia sẻ cách tôi xử lý các lỗi về khả năng thích ứng và bộ nhớ trong quá trình phát triển công cụ này.
Ý tưởng đằng sau ChromaClip rất đơn giản: người dùng chọn một video, có thể là một đoạn video hoàng hôn tuyệt đẹp hoặc một cảnh có màu sắc ấn tượng, sau đó ứng dụng sẽ phân tích các khung hình để tạo ra một bảng màu tuỳ chỉnh. Người dùng có thể sử dụng bảng màu này cho các thiết kế, tác phẩm nghệ thuật kỹ thuật số, bảng tâm trạng hoặc bất cứ thứ gì họ muốn.
Câu chuyện của chúng tôi bắt đầu ngay trước khi phát hành phiên bản 1.0.
Tôi muốn đảm bảo ứng dụng hoạt động ổn định trước khi ra mắt, nên tôi đã chạy các thử nghiệm cuối cùng trên một số thiết bị, chọn video, nhấn vào nút để trích xuất bảng màu, và, thật không may, hộp thoại mà mọi nhà phát triển Android đều không muốn thấy đã xuất hiện.
ChromaClip không phản hồi.
Vì vậy, đây là lỗi Ứng dụng không phản hồi, hay còn gọi là ANR.
Polina Govorkova
Vậy chúng ta nên làm gì trong trường hợp này, ngoài việc hoảng sợ?
Theo bản năng, vì điện thoại kiểm thử được cắm ngay vào máy tính xách tay, nên chúng ta sẽ mở Android Studio và kiểm tra Logcat.
Chúng ta thấy dấu vết ngăn xếp cho luồng chính.
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)Thực ra, điều này cho chúng ta biết điều gì?
Trước tiên, ngay trên cùng, chúng ta nhận được thông báo xác nhận rằng đó là lỗi ANR, đang chạy trong MainActivity.
Còn gì nữa không?
Chúng ta thấy lý do là hết thời gian gửi dữ liệu đầu vào.
Điều đó có nghĩa là người dùng đã nhấn vào màn hình.
Có, chúng tôi đã làm được.
Tuy nhiên, luồng chính bị ràng buộc đến mức không thể xử lý sự kiện đó trong thời gian chờ 5 giây của trình giám sát Android.
Sau đó, chúng ta cũng thấy hệ thống đã kết xuất tệp vào traces.txt và ngay bên dưới, logcat in dấu vết ngăn xếp cho luồng chính.
Lưu ý rằng trạng thái bằng S ở đó.
Luồng chính của chúng ta thậm chí còn không tích cực sử dụng CPU.
S là viết tắt của ngủ (sleeping).
Tại sao luồng chính của chúng ta lại ở trạng thái ngủ trong khi chúng ta đang chờ?
Hãy xem xét kỹ hơn các khung ngăn xếp.
Ngay tại giữa dấu vết, chúng ta thấy luồng chính đang dừng bên trong Unsafe.park để chờ một đối tượng không xác định.
Tại sao chúng ta không biết nó đang chờ đợi điều gì?
Thực ra, đối với một số lỗi ANR, chẳng hạn như khi một luồng bị chặn bởi khoá đồng bộ hoá Java cổ điển, dấu vết ngăn xếp luồng chính sẽ cho chúng ta biết chính xác những gì đang chờ đợi, những gì đang giữ khoá.
Nhưng khi bạn sử dụng các dữ liệu gốc đồng thời ở cấp cao hơn, tất cả những gì logcat cho chúng ta biết là chúng ta đang chờ một đối tượng không xác định.
Vì vậy, chúng ta không biết mình đang chờ đối tượng nào, cụ thể là đang chờ luồng worker trong nền nào hoặc tại sao lại mất hơn 5 giây.
Và vì đang kiểm thử cục bộ, nên chúng ta có thể lấy traces.txt đó ra khỏi thiết bị và cuộn qua hàng chục luồng đang ngủ theo cách thủ công, rồi có thể tìm ra thủ phạm hoặc có thể dùng mã thông báo để hỏi AI.
Trong một số trường hợp, cách này thực sự hiệu quả và trên thực tế, cách này sẽ hiệu quả trong trường hợp này.
Nhưng không chỉ vì tôi thực sự không muốn đọc các tệp báo lỗi luồng thô theo cách thủ công (thành thật mà nói, cách này đã quá lỗi thời), mà tôi còn biết rằng phương pháp này thậm chí không khả thi khi chúng ta đang trong giai đoạn phát hành chính thức.
Chúng ta chỉ không có quyền truy cập trực tiếp vào tệp traces.txt trong điện thoại của người dùng, đúng không?
Vì vậy, thật tốt khi chúng ta đã tích hợp với Firebase Crashlytics.
Vì công cụ này giúp bạn không cần phải tìm kiếm và có thể tìm ra nguyên nhân gốc rễ của lỗi ANR ngay cả trong môi trường thực tế.
Cách thực hiện:
Đó là lúc bạn cần đến API ApplicationExitInfo của Android.
Kể từ Android 11, bất cứ khi nào hệ thống gắn cờ ANR hoặc chấm dứt quy trình ứng dụng, hệ điều hành sẽ ghi lại siêu dữ liệu thoát chi tiết, bao gồm lý do chính xác gây ra sự cố, trạng thái bộ nhớ và dấu vết hệ thống hoàn chỉnh cho mọi luồng đang chạy trong quy trình của bạn tại thời điểm đó.
Và điều tuyệt vời nhất là bạn không cần cài đặt thêm trình bổ trợ hay viết một dòng mã nào.
Chỉ cần bạn sử dụng SDK Crashlytics phiên bản 18.3.5 trở lên, tính năng này sẽ hoạt động ngay lập tức.
Vì vậy, ngay khi ứng dụng của bạn khởi động lại, SDK Crashlytics sẽ tự động truy vấn ApplicationExitInfo, thu thập toàn bộ tệp báo lỗi đa luồng cùng với dữ liệu đo từ xa của thiết bị và gửi đến phần phụ trợ của Crashlytics.
Hãy mở bảng điều khiển Crashlytics và xem thông tin trong đó.
Đây là chế độ xem thông tin chi tiết về vấn đề trong bảng điều khiển Firebase Crashlytics cho lỗi ANR. Chúng ta có thể thấy ngay DefaultDispatcher worker one trong thẻ dấu vết ngăn xếp.
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)Trên thực tế, đây là luồng được Crashlytics đưa lên đầu tiên, là điểm tắc nghẽn cho luồng chính của chúng ta và Crashlytics đánh dấu luồng này bằng một huy hiệu có tên là Root blocking (Chặn gốc).
Như chú giải công cụ trên huy hiệu giải thích, luồng worker này thực sự là luồng chặn luồng đã kích hoạt lỗi ANR.
Ngoài ra, Crashlytics cũng có huy hiệu thông minh cho các thủ phạm khác.
Ví dụ: nếu luồng bị kẹt khi đọc một tệp lớn hoặc thực hiện một truy vấn cơ sở dữ liệu thực sự chậm, thì nếu kích hoạt lỗi ANR, luồng đó sẽ được gắn thẻ là chặn I/O ở cấp độ gốc.
Nếu hai luồng bị kẹt khi chờ nhau, thì Crashlytics sẽ gắn thẻ chúng là bị tắc nghẽn.
Bây giờ, hãy xem xét chính các khung ngăn xếp trong luồng chặn gốc.
Vì vậy, chúng ta có PaletteExtractor processVideo gọi FrameDecoder getUncompressedFrame được đánh dấu ngay tại đó bằng màu xanh dương và hoàn toàn bị kẹt bên trong createBitmap chỉ để cố gắng phân bổ bộ nhớ cho một khung hình.
Vì biết rằng luồng này không chờ một khoá khác và cũng không thực hiện lệnh gọi mạng, nên chúng tôi nghi ngờ bộ nhớ là nguyên nhân chính. Chúng tôi có thể xác nhận ngay bằng cách kiểm tra bộ nhớ trống của thiết bị ngay tại đây trong bảng điều khiển Crashlytics.
Hãy xem hai nơi mà Crashlytics hiển thị thông tin này.
Trong thẻ dữ liệu cho sự kiện cụ thể ngay bên cạnh thẻ dấu vết ngăn xếp mà chúng ta vừa khám phá, và ở trên cùng bên phải cho thấy mức trung bình trên tất cả sự kiện.
Và trong cả hai trường hợp, có vẻ như hầu như không có gì, thậm chí không đến 100 KB.
Vậy hãy cùng suy nghĩ.
Điều xảy ra ở đây là do quy trình này thiếu bộ nhớ, nên Android Runtime đã dành toàn bộ thời gian để chạy tính năng thu gom rác, cố gắng tìm đủ dung lượng cho bitmap tiếp theo và không bao giờ nhận được.
Làm treo luồng worker và khiến giao diện người dùng không hoạt động trong hơn 5 giây.
Sau đây là một mẹo hay dành cho bạn.
Trước khi bạn nhận được, Bất cứ khi nào bạn nhận được một ANR kết hợp với loại RAM trống thấp này, thì đó là dấu hiệu cho thấy ANR thực sự là một vấn đề rò rỉ bộ nhớ hoặc phình to.
Giờ đây, câu hỏi đặt ra là ứng dụng của chúng ta là nạn nhân hay cũng là ứng dụng đang tích trữ bộ nhớ?
Vì vậy, hãy quay lại danh sách vấn đề của dự án và xem liệu có vấn đề nào khác có liên quan hay không.
Polina Govorkova
Và hãy nhìn vào đây.
Ở dưới cùng, chúng ta thấy danh sách các vấn đề và ngay phía trên lỗi ANR có 8 sự kiện, chúng ta có ngoại lệ OutOfMemory.
Vậy nên, có thể chúng tôi mới là thủ phạm.
Và đó thực sự là một manh mối quan trọng.
Vì trên Android, lỗi OutOfMemoryError Java (gọi tắt là OOM) không chỉ có nghĩa là điện thoại sắp hết RAM.
Điều này có nghĩa là quy trình ứng dụng của chúng ta đã vượt quá giới hạn vùng nhớ khối xếp Java trong Android Runtime.
Vì vậy, tại thời điểm này, giả thuyết của tôi là ANR và OOM thực sự là hai mặt của cùng một vấn đề.
Tuỳ thuộc vào thời gian trong một lần chạy kiểm thử, ChromaClip sẽ bị kẹt trong quá trình thu gom rác đủ lâu để kích hoạt lỗi ANR hoặc làm cạn kiệt vùng nhớ khối xếp, dẫn đến lỗi OOM.
Hãy cùng tìm hiểu ngoại lệ OutOfMemory để biết chắc chắn.
Bắt đầu bằng dấu vết ngăn xếp.
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)Chờ một chút.
Bitmap.createBitmap, nghe quen quen.
Chúng ta đã thấy cái đó chưa?
Tuy nhiên, hãy xem lần này nó được gọi từ đâu.
Không phân bổ được 256 byte khi mở hoạt động cài đặt.
Chúng ta lại rơi vào cùng một cái bẫy.
Nếu ứng dụng của bạn gặp sự cố vì không thể tăng cường một biểu tượng cài đặt nhỏ, thì hoạt động cài đặt sẽ không phải là nguyên nhân.
Đó chỉ là phương thức gọi không may yêu cầu một phần bộ nhớ khi không có bộ nhớ.
Vì vậy, để tìm ra thủ phạm thực sự, chúng ta cần thu nhỏ và xem mọi thứ nằm trong bộ nhớ khi xảy ra sự cố.
Nói cách khác, chúng ta cần một tệp báo lỗi Java.
Tại bàn làm việc, nếu biết cách kích hoạt OOM, bạn có thể lấy tệp báo lỗi khối xếp Java theo cách thủ công bằng trình phân tích bộ nhớ của Android Studio.
Nhưng khi lỗi OOM xảy ra ngoài dự kiến, trong quá trình chạy thử nghiệm hoặc trong thực tế, thì phương pháp này sẽ không hiệu quả.
Polina Govorkova
Vì vậy, đây là một điểm sáng khác của Firebase Crashlytics.
Ngoài thông tin về việc thoát ứng dụng, Crashlytics hiện còn tích hợp với ProfilingManager API của Android.
API ProfilingManager trong Android 17 được tăng cường khả năng phân tích bộ nhớ do hệ thống kích hoạt, và đó chính xác là những gì chúng ta cần ở đây.
Điều đó có nghĩa là ngay khi ứng dụng của bạn gặp lỗi OutOfMemory hoặc vi phạm giới hạn bộ nhớ hệ thống, hệ điều hành sẽ chụp nhanh snapshot của vùng nhớ heap trong nền, sau đó trong Crashlytics, bộ sưu tập được kích hoạt sẽ được cung cấp miễn phí ngay khi bạn sử dụng Crashlytics SDK 20.1.0 trở lên.
Sau đó, Crashlytics sẽ tự động nhóm các sự cố OOM và Memory Limiter, chúng ta sẽ nói thêm về các sự cố này sau, trong trang tổng quan.
Và khi bạn chọn thu thập tệp báo lỗi vùng nhớ khối xếp thông qua bảng điều khiển của Firebase hoặc CLI, Crashlytics sẽ bắt đầu tự động thu thập những snapshot của vùng nhớ heap đã chụp, giải mã chúng cho bạn và về cơ bản, bạn sẽ nhận được bản chụp X quang đầy đủ về mọi thứ trong vùng nhớ khối xếp Java của ứng dụng tại thời điểm xảy ra sự cố.
Vì vậy, đây là giao diện của ChromaClip OOM trong bảng điều khiển Crashlytics sau khi vùng nhớ khối xếp được ghi lại.
Ở trên cùng, bạn sẽ thấy nội dung giải thích lý do Java làm cạn kiệt vùng nhớ khối xếp và lý do xảy ra tình trạng này.
Sau đó, bạn cũng sẽ thấy các mẹo khắc phục sự cố hữu ích và các gợi ý về tài nguyên bên ngoài.
Trong thẻ dấu vết ngăn xếp, ngay phía trên hoạt động cài đặt gây hiểu lầm đó, Crashlytics thực sự gắn cờ rằng dấu vết ngăn xếp OOM không mang tính tất định, nhờ đó mọi người không phải lãng phí thời gian xem dấu vết ngăn xếp như chúng ta vừa làm.
Sau đó, Crashlytics sẽ đính kèm tệp Perfetto đã được gỡ rối (tệp báo lỗi) cho phiên đó.
Và từ đây, chúng ta có thể tải tệp xuống.
Chúng ta có thể duyệt xem tất cả tệp báo lỗi trong Google Cloud Storage hoặc mở tệp báo lỗi trong giao diện người dùng Perfetto.
Do đó, khi bạn nhấp vào Analyze (Phân tích) trong giao diện người dùng Perfetto, trình khám phá tệp báo lỗi khối xếp Perfetto sẽ mở ra.
Giờ thì hãy giơ tay nếu bạn đã thấy hình này.
Được rồi, có một vài người giơ tay.
Đối với tôi, chế độ xem này rất đáng sợ, nhưng trong trường hợp này, vấn đề thực ra khá đơn giản.
Hãy xem tập hợp các thanh lớn trải dài gần như toàn bộ vùng nhớ khối xếp Java.
Vì vậy, phần lớn là một mảng các đối tượng cặp, mỗi đối tượng gói một màu và một số thực thể chuỗi.
Ồ!
Tôi nghĩ là tôi biết chuyện gì đang xảy ra.
Mã của chúng ta đang phân bổ một màu cùng với một số dữ liệu hỗ trợ cho mỗi pixel trong mỗi khung hình và lưu giữ hàng triệu pixel trong một danh sách.
Vì vậy, không có gì ngạc nhiên khi hoạt động Cài đặt không thể phân bổ 256 byte đó.
Ít nhất thì cũng giải đáp được một bí ẩn.
Hãy cùng khắc phục vấn đề này.
Đến đây, chúng ta có một vài lựa chọn.
Vì Perfetto đã chỉ ra PaletteExtractor, nên chúng ta có thể mở IDE và viết lại vòng lặp theo cách thủ công hoặc sử dụng tác nhân lập trình AI.
Hoặc chúng ta có thể bỏ qua hoàn toàn bước kiểm tra vùng nhớ khối xếp thủ công và tận dụng 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>Vì vậy, khi cài đặt các kỹ năng Android CLI và Perfetto, bạn có thể liệt kê và tải các tệp báo cáo khối nhớ đã được gỡ rối của Crashlytics xuống, truy vấn các đối tượng được giữ lại nhiều nhất từ các tệp báo cáo khối nhớ này thông qua trace_processor, đồng thời tìm nạp dữ liệu vấn đề phù hợp của Crashlytics như khoá tuỳ chỉnh, nhật ký, breadcrumb, bản phát hành liên quan và nhiều dữ liệu khác, tất cả đều từ thiết bị đầu cuối của bạn.
Tốt hơn nữa, hãy nhìn vào cuối trang trình bày.
Đừng tự chạy các lệnh này.
Một lần nữa, hãy yêu cầu tác nhân lập trình AI mà bạn yêu thích bằng ngôn ngữ tự nhiên để kiểm tra tệp báo lỗi vùng nhớ khối xếp OOM mới nhất bằng lệnh Android và gửi bản sửa lỗi.
# 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)Và Gemini sẽ có đủ bối cảnh để thực hiện toàn bộ quy trình cho bạn.
Vì vậy, việc liệt kê các lỗi OOM, tải dấu vết xuống, chạy phân tích và tạo bản vá mã ngay trong dự án của bạn.
Và hãy xem bản vá này trong tệp 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
}Thay vì phân bổ hàng triệu đối tượng, giờ đây, chúng ta chỉ cần giảm tần số lấy mẫu khung hình và đếm trực tiếp các số nguyên thô.
Vì vậy, hãy áp dụng bản sửa lỗi, đóng các vấn đề, nâng cấp phiên bản và tiếp tục thử nghiệm.
Hy vọng rằng không chỉ lỗi hết bộ nhớ sẽ biến mất, mà vì chúng ta sẽ không còn gặp lỗi trong quá trình thu gom rác nữa, nên hy vọng rằng lỗi ANR khó chịu đó cũng sẽ biến mất.
Vì vậy, chúng tôi đã triển khai bản vá trên các thiết bị thử nghiệm, chọn một đoạn video mới và bắt đầu trích xuất bảng màu.
Và giao diện người dùng lại bị treo.
Lại một lỗi ANR khác.
Thật thú vị, khi thử nghiệm thêm, chúng tôi phát hiện ra rằng một số thiết bị vẫn gặp lỗi ANR, nhưng đối với các thiết bị khác, ứng dụng hiện chỉ gặp sự cố ngẫu nhiên và khá thường xuyên, thường xuyên hơn so với các lỗi OOM đó.
Nhưng chúng ta đã khắc phục lỗi OOM, vậy thì chuyện gì đang xảy ra?
Polina Govorkova
Giờ thì quay lại Crashlytics thôi.
Tin vui trước.
Xem danh sách vấn đề của chúng tôi.
Lỗi hết bộ nhớ đã hoàn toàn biến mất, đúng như chúng ta mong đợi.
Tin xấu là gì?
Giờ đây, khi ChromaClip có thể tồn tại lâu hơn một chút, nó có thời gian để khám phá những điều khác đang ẩn giấu bên dưới.
Vì vậy, lỗi ANR vẫn còn.
Và giờ chúng ta có hai trục trặc mã gốc ở dưới cùng, may mắn là chỉ có một vài sự kiện.
Và ngay ở trên cùng, với 11 sự kiện, chúng ta có lệnh buộc dừng của Bộ giới hạn bộ nhớ, may mắn thay, đã được gắn huy hiệu là có tệp báo lỗi vùng nhớ khối xếp.
Đợi đã.
Thế nào là thao tác tắt do Bộ giới hạn bộ nhớ?
Có ai trong số các bạn đã nghe nói về nó chưa?
Được rồi.
Một vài ván bài hiếm hoi.
Hãy cùng nhau tìm hiểu.
Nếu bạn chưa từng thấy Trình giới hạn bộ nhớ tắt ứng dụng, thì đó là vì tính năng này hoàn toàn mới trên Android 17.
Và lỗi OOM của Java chỉ kích hoạt khi bạn vượt quá giới hạn vùng nhớ khối xếp Java được quản lý, nhưng khi tổng mức sử dụng bộ nhớ của ứng dụng cũng bao gồm RAM gốc, chẳng hạn như vùng đệm đồ hoạ và mức phân bổ C++, thì trước đây không có giới hạn cho mỗi ứng dụng.
Vì vậy, chúng ta sẽ có trình tắt ứng dụng khi bộ nhớ thấp, đây là trình tắt ứng dụng chung.
Nó sẽ can thiệp khi một số ứng dụng ngốn hết RAM hệ thống cho đến khi toàn bộ điện thoại bị nghẽn.
Giờ đây, kể từ Android 17, Trình giới hạn bộ nhớ sẽ giới hạn mức sử dụng bộ nhớ của từng ứng dụng.
Vì vậy, khi quy trình tiêu thụ lượng RAM không tương xứng, hệ điều hành sẽ chấm dứt ứng dụng cụ thể đó.
Crashlytics ghi lại tất cả thông tin chi tiết đó và nhóm các trường hợp chấm dứt lại với nhau trong trang tổng quan.
Vì vậy, khi nhấp vào Memory Limiter kill (Trình giới hạn bộ nhớ buộc dừng) trong Crashlytics, giống như với OOM, bạn sẽ nhận được nội dung giải thích về lý do hệ điều hành can thiệp, nên bạn không cần phải ghi nhớ những gì tôi vừa nói.
Bạn cũng sẽ nhận được các mẹo về cách xử lý vấn đề tiếp theo và đường liên kết trực tiếp đến tài liệu về việc thực thi bộ nhớ Android.
Và cũng giống như lỗi OOM, dấu vết ngăn xếp sẽ đổ lỗi cho một khung hình vô tội khi hệ điều hành tắt ứng dụng và trang tổng quan Crashlytics sẽ làm nổi bật khung hình đó.
Hiện tại, các sự cố dừng do Trình giới hạn bộ nhớ chủ yếu xảy ra trong mã gốc vì môi trường gốc thiếu lớp xử lý việc phân bổ và an toàn bộ nhớ.
Nếu không, lỗi OOM sẽ xảy ra.
Vì vậy, câu hỏi đầu tiên của chúng ta là liệu các đối tượng Kotlin có đang giữ các lượt phân bổ lớn được hỗ trợ gốc hay không?
Hay đó là mã C++ thô đang tự rò rỉ?
Hãy nhấp vào Analyze in Perfetto UI (Phân tích trong giao diện người dùng Perfetto) để tìm hiểu.
Xem thẻ Tổng quan trong trình khám phá tệp báo lỗi khối nhớ Perfetto.
Trong phần thông tin chung, chúng ta thấy RSS ẩn danh cộng với không gian hoán đổi cho thấy tổng bộ nhớ của quy trình đạt 4 GB.
Được rồi.
Tuy nhiên, bên dưới, chúng ta thấy rằng trong phần Bytes Retained by Heap (Số byte được vùng nhớ khối xếp giữ lại), toàn bộ vùng nhớ khối xếp được quản lý, bao gồm cả bộ nhớ gốc liên kết với các đối tượng Kotlin như bitmap, chỉ có 42 MB.
Được rồi.
Vì vậy, trong 10 giây, chúng tôi đã loại trừ tất cả mã Kotlin và Java.
Vì vậy, thủ phạm phải là rò rỉ từ lớp C++ gốc.
Và vì ChromaClip có một lớp C++ mỏng để xử lý khung hình, nên chúng ta không cần phải tìm đâu xa.
Khi hướng tác nhân lập trình hoặc trình phân tích gốc của Android Studio vào NativePaletteEngine.cpp processFrame, lỗi sẽ xuất hiện ngay tại đó.
// 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
}Vì vậy, trên mỗi khung hình video, mã C++ của chúng tôi sẽ phân bổ một vùng đệm tạm thời, nhưng không tạm thời nếu bạn không giải phóng nó, một lỗi rất sơ đẳng.
Giờ là lúc xâu chuỗi các thông tin.
Điều gì đã xảy ra?
Sau khi chúng ta khắc phục lỗi rò rỉ Java, lỗi rò rỉ C++ sẽ có thời gian tích luỹ và chạy không kiểm soát.
Trên Android 17, Trình giới hạn bộ nhớ đã phát hiện và tắt ứng dụng.
Nhưng trên các thiết bị cũ chưa có Trình giới hạn bộ nhớ, lỗi này thực sự biểu hiện dưới dạng lỗi ANR.
Cuối cùng, mọi thứ đều hợp lý.
Hãy thêm delete[] scratch vào đây, tăng phiên bản và phát hành bản cập nhật.
Khi các lỗi ANR và lỗi buộc dừng do Trình giới hạn bộ nhớ đã được giải quyết, chúng ta có thể phát hành phiên bản 1.0.
Thú vị.
Nhờ tốc độ và độ ổn định của ChromaClip, ứng dụng của chúng tôi đã thành công rực rỡ.
Hàng nghìn người đang tải video lên và phối lại video mỗi ngày.
Nhưng với tất cả lưu lượng truy cập, bạn có nhớ những sự cố mã gốc bí ẩn mà chúng ta đã thấy trước đó ở cuối trang tổng quan không?
Chúng cũng đang chạy lung tung.
Đột nhiên, số lượng người dùng tăng lên rất nhiều.
Và công việc của chúng tôi vẫn chưa dừng lại ở đó.
Polina Govorkova
Vì vậy, hãy mở trục trặc mã gốc đó trong Crashlytics và xem dấu vết ngăn xếp.
Không có một dòng mã nào trong ứng dụng của chúng tôi.
RenderThread gặp sự cố sâu bên trong libc.so khi giải phóng bộ nhớ.
Và một mẹo khác cho bạn ở đây, khi một luồng hệ thống gặp sự cố bên trong trình phân bổ bộ nhớ mà không có mã ứng dụng nào của bạn bên trong, thì đó là dấu hiệu điển hình của lỗi hỏng bộ nhớ gốc.
Vì vậy, mã C++ của chúng ta đã làm hỏng hoặc giải phóng vùng đệm trước đó, và luồng kết xuất chỉ bị lỗi sau đó.
Vậy làm cách nào để bắt được thủ phạm thực sự vào đúng thời điểm xảy ra lỗi hỏng?
Android cung cấp cho chúng ta hai công cụ mạnh mẽ và Crashlytics hỗ trợ cả hai công cụ này ngay từ đầu.
Đầu tiên là GWP-ASan, một mạng lưới an toàn gọn nhẹ được xây dựng riêng cho môi trường thực tế.
Công cụ này lấy mẫu một phần nhỏ các lượt phân bổ vùng nhớ khối xếp gốc và bao quanh các lượt phân bổ đó bằng các trang bảo vệ để mức hao tổn CPU và bộ nhớ gần như bằng không.
Trong hàng nghìn phiên hoạt động của người dùng, khi một lỗi chạm vào bộ nhớ đã giải phóng hoặc nằm ngoài giới hạn trên một lượt phân bổ được lấy mẫu, GWP-ASan sẽ phát hiện lỗi đó ngay lập tức.
Tuy nhiên, vì GWP-ASan vốn dĩ mang tính xác suất, nên khi kiểm thử mọi thứ trên thiết bị, bạn không nên chạy điện thoại kiểm thử trong 16 giờ trở lên để lấy mẫu ngẫu nhiên.
Vậy bạn sẽ nhận được gì?
Để gỡ lỗi cục bộ tất định, bạn có thể dùng ARM MTE (Tiện ích gắn thẻ bộ nhớ).
MTE sử dụng con trỏ phần cứng và thẻ bộ nhớ để kiểm tra mọi lượt phân bổ. MTE có trên các thiết bị ARM v9 như Pixel 8 trở lên.
Vì vậy, hãy kết hợp HWASan với GWP-ASan trong phiên bản chính thức để tận dụng tối đa cả hai công cụ.
Và như bạn có thể đoán, khi một trong hai công cụ phát hiện lỗi, Crashlytics sẽ tự động gắn huy hiệu cho vấn đề và cho phép bạn lọc theo GWP-ASan hoặc MTE, đồng thời ghim thẻ thông tin có nguyên nhân chính xác gây ra lỗi hỏng trong trang tổng quan.
Vì vậy, hãy xem điều gì xảy ra trong bảng điều khiển Crashlytics khi GWP-ASan phát hiện sự cố.
Bên cạnh thẻ dấu vết ngăn xếp thông thường, chúng ta có dấu vết ngăn xếp bộ nhớ.
Và ở trên cùng, chúng ta thấy nguyên nhân chính xác gây ra lỗi hỏng, đó là Use After Free (Sử dụng sau khi giải phóng).
Thay vì cho thấy luồng kết xuất gặp sự cố trong libc.so, GWP-ASan hiện cung cấp cho chúng ta toàn bộ thông tin về vùng đệm đó bên trong effect_pipeline.cc của chúng ta.
Vì vậy, trong phần Allocated By (Được phân bổ bởi), chúng ta sẽ thấy nơi tạo vùng đệm.
Và ngay bên dưới Deallocated By (Được giải phóng bởi), chúng ta thấy chính xác thủ phạm đã giải phóng nó khỏi luồng kết xuất.
Và điều đó ẩn ở dòng 208 trên quy trình hiệu ứng của chúng ta.
Vì vậy, chúng ta mở mã gốc trong EffectPipeline và lỗi sẽ xuất hiện.
// 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
}Bất cứ khi nào người dùng chuyển đổi bộ lọc màu trong luồng nền, chúng ta sẽ giải phóng vùng đệm cũ trong khi RenderThread vẫn đang đọc vùng đệm đó.
Được rồi, vậy là chúng ta thêm khoá mutex xung quanh việc hoán đổi vùng đệm, phát hành bản cập nhật và cuối cùng là xem cả báo cáo GWP-ASan và tất cả những sự cố bí ẩn đó biến mất gần như chỉ sau một đêm.
Và chỉ số người dùng không gặp sự cố của chúng tôi đã tăng vọt lên 100%.
Thật tuyệt vời!
Hãy nhớ lại, ở phần đầu của bài nói chuyện, chúng ta đã tự hỏi: "Tôi đã hoàn thành chưa?"
Ngay trước khi gặp phải hầu hết mọi vấn đề về khả năng phản hồi và bộ nhớ.
Giờ đây, các lỗi ANR, lỗi hết bộ nhớ, lỗi tắt do giới hạn bộ nhớ và lỗi hỏng bộ nhớ gốc đều đã được giải quyết.
Và ngay cả khi dấu vết ngăn xếp không chính xác và thủ phạm đang ẩn náu ở đâu đó trong các luồng trong C++, Android và Firebase Crashlytics vẫn cung cấp cho bạn công cụ để theo dõi và tự tin phát hành ứng dụng.
Polina Govorkova
Hãy cùng điểm lại những tính năng bạn có thể sử dụng ngay khi quay lại máy tính xách tay: thông tin chi tiết về luồng ANR và siêu dữ liệu liên quan khác thông qua ApplicationExitInfo; tự động thu thập các điều kiện kích hoạt lỗi hết bộ nhớ và trình giới hạn bộ nhớ thông qua ProfilingManager; cũng như hỗ trợ đầy đủ GWP-ASan và Arm MTE cho lỗi bộ nhớ gốc.
Ngoài ra, chúng tôi cũng ra mắt bản xem trước riêng tư cho tính năng tệp báo lỗi vùng nhớ khối xếp tại chỗ trên Android 17 trở lên.
Nếu bạn muốn trở thành một trong những người đầu tiên dùng thử tính năng tự động thu thập tệp báo lỗi trên Firebase Crashlytics và phân tích các tệp đó bằng Perfetto và Android CLI, vui lòng chụp nhanh ảnh của trang trình bày và đăng ký theo đường liên kết.
Vậy là chúng ta đã đi qua hầu hết nội dung của buổi trò chuyện. Tôi xin cảm ơn bạn đã tham gia và dành thời gian cho buổi hôm nay.
Chúc luồng chính của bạn luôn thông suốt, vùng nhớ khối xếp luôn sạch và bản phát hành tiếp theo sẽ là bản ổn định nhất từ trước đến nay.
Chúc bạn gỡ lỗi thành công.
Một lần nữa, xin cảm ơn bạn rất nhiều.
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
}