Đơ là mô tả của người dùng về tình huống khi ứng dụng di động hoạt động chậm và không ổn định: lúc phản hồi bình thường, lúc đột ngột đứng hẳn trong vài giây. Trong bối cảnh kỹ thuật, “đơ” có nghĩa là sự kết hợp của lag và micro-freeze do các khoảng dừng GC thường xuyên, luồng chính bị chặn bởi các thao tác đồng bộ và cấu trúc dữ liệu không tối ưu. Theo Android Performance Benchmarking Guide, giảm thời gian phản hồi từ 300 ms xuống 100 ms làm tăng tỷ lệ giữ chân người dùng lên 25%. Chẩn đoán tình trạng đơ đòi hỏi sự kết hợp của hồ sơ CPU và Bộ nhớ với phân tích tần suất thu gom rác.
Những điểm chính
Đơ là thuật ngữ không chính thức mà người dùng sử dụng để mô tả hiệu suất ứng dụng chậm một cách chủ quan. Khác với lag, biểu hiện như một độ trễ không đổi, đơ bao gồm các lần đứng không đều đặn: ứng dụng có thể hoạt động hoàn hảo trong vài giây rồi đột nhiên “suy nghĩ” trong 1–3 giây.
Từ quan điểm hồ sơ, đơ thể hiện dưới dạng một chuỗi các khung hình bỏ lỡ (jank) với độ trễ đỉnh vượt quá 100 ms. Trên biểu đồ FPS, điều này trông giống như những cú giảm mạnh: 60 → 20 → 55 → 10 khung hình trên giây. Khác với lag với FPS thấp đồng đều, đơ có sự biến động rõ rệt.
Khi ứng dụng đơ, người dùng không hiểu được logic của sự chậm lại: màn hình có thể cuộn mượt rồi đột nhiên dừng lại trong một giây. Điều này gây khó chịu và làm giảm lòng tin vào ứng dụng. Theo Google, 53% người dùng rời bỏ trang web hoặc ứng dụng nếu tải mất hơn 3 giây.
Bản chất không liên tục của đơ cho thấy vấn đề xuất phát từ các yếu tố hướng sự kiện chứ không phải quá tải liên tục. Hãy xem xét các kịch bản điển hình.
Trên Android, trong môi trường ART, thu gom rác dừng tất cả các luồng ứng dụng. Nếu mã tạo nhiều đối tượng tạm thời — ví dụ, tạo String mới qua phép nối mỗi lần gọi onBindViewHolder — GC chạy thường xuyên hơn. Một khoảng dừng có thể kéo dài 5–50 ms tùy theo kích thước heap và thế hệ đối tượng. Người dùng cảm nhận điều này như sự “suy nghĩ” đột ngột.
Room trên Android và Core Data trên iOS hỗ trợ truy vấn bất đồng bộ, nhưng các nhà phát triển thường gọi getValue() hoặc thực thi truy vấn qua runBlocking cho đơn giản. Một SELECT nặng với các join trên bảng 10.000 dòng có thể mất 200–500 ms, chặn hoàn toàn UI trong thời gian đó.
Tải ảnh máy ảnh (12 MP, 4000x3000 px) mà không thay đổi tỷ lệ mất đến 200 ms cho việc giải mã thành Bitmap. Nếu hình ảnh được tải bất đồng bộ nhưng không có nhóm luồng giới hạn, việc chạy 5–6 lần giải mã cùng lúc có thể làm quá tải CPU, gây ra các đợt chậm di chuyển.
Chẩn đoán các đợt chậm không liên tục khó hơn chẩn đoán lag liên tục vì vấn đề có thể không tái tạo mỗi lần chạy. Cần thu thập thống kê trong một thời gian dài.
Android Studio Memory Profiler không chỉ hiển thị mức sử dụng bộ nhớ mà còn các sự kiện GC: tần suất, loại (Concurrent, Full), thời gian. Nếu GC xảy ra nhiều hơn một lần mỗi 5 giây ở trạng thái rảnh — đó là dấu hiệu của việc cấp phát quá mức. Chụp heap dump tại thời điểm đơ cho thấy đối tượng nào đang chiếm dụng bộ nhớ.
Trên iOS, sử dụng mẫu Allocations trong Instruments để theo dõi việc tạo và giải phóng đối tượng. Bật Generations — chúng cho phép chụp ảnh nhanh heap giữa các hành động và xem đối tượng nào vẫn trong bộ nhớ. Các đối tượng liên tục không được giải phóng là nguồn tích tụ bộ nhớ và các khoảng dừng sau đó.
JankStats là một thư viện Android thu thập các số liệu về khung hình bỏ lỡ theo thời gian thực. Nó gắn mỗi jank với kịch bản hiện tại (ví dụ, “cuộn danh sách”, “mở màn hình”), cho phép hiểu hành động cụ thể nào gây ra đơ.
Ví dụ tích hợp JankStats để theo dõi đứng hình trên 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", "Duration=${frameData.durationMs}ms")
}
}
}
}
Loại bỏ đơ đòi hỏi công việc có mục tiêu vào từng nguyên nhân. Không có giải pháp phổ quát — cần phân tích các hồ sơ hiệu suất cụ thể.
Nếu danh sách chứa 1000+ mục và tất cả đều được tải cùng lúc — đó là sự đơ chắc chắn. Paging 3 trên Android và NSFetchedResultsController trên iOS tải dữ liệu theo từng phần khi người dùng cuộn. Người dùng chỉ thấy 10–20 mục đầu tiên; phần còn lại được tải ở nền.
Room cho phép hồ sơ truy vấn qua Công cụ Kiểm tra trong Android Studio: thời gian thực thi, số dòng trả về và kế hoạch truy vấn được hiển thị. Thêm chỉ mục trên các cột WHERE và ORDER BY có thể giảm thời gian truy vấn từ 300 ms xuống 5 ms. Trên iOS, Core Data Profiler trong Instruments thực hiện kiểm tra tương tự.
Đồng bộ nền, tải tệp, xử lý dữ liệu — tất cả nên được thực thi qua WorkManager (Android) hoặc Background Tasks (iOS). Nếu đồng bộ chạy trên luồng UI, ứng dụng sẽ đơ trong quá trình thực thi. WorkManager đảm bảo thực thi trên luồng nền với nhận thức về trạng thái pin và mạng.
Ví dụ đồng bộ nền qua WorkManager trên Android:
class SyncWorker(context: Context, params: WorkerParameters)
: CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
Log.d("Sync", "Syncing data in background thread")
syncData()
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
}
Có thể ngăn ngừa đơ ngay ở giai đoạn viết mã bằng cách tuân theo các nguyên tắc quản lý bộ nhớ và luồng hiệu quả.
Baseline Profiles là danh sách các lớp và phương thức mà Android biên dịch trước (AOT) thay vì JIT. Nếu không có hồ sơ, mỗi màn hình mới sẽ được biên dịch khi mở lần đầu, gây ra độ trễ 100–500 ms. Chuẩn bị Baseline Profile cho các màn hình chính và bật tạo trong Gradle qua plugin baseline-profile-gradle-plugin.
Đường dẫn nóng là mã được thực thi trên mỗi khung hình: onBindViewHolder, draw, layoutSubviews. Tránh tạo đối tượng trong các phương thức này: sử dụng pool đối tượng, StringBuilder thay vì nối chuỗi, lưu vùng đệm các chuỗi đã định dạng và trình định dạng. Mỗi lần cấp phát thừa sẽ đưa GC tiếp theo đến gần hơn.
Thêm Macrobenchmark vào đường ống CI của bạn với kịch bản cuộn danh sách và mở màn hình. Đặt ngưỡng: phân vị thứ 99 của thời gian khung hình không được vượt quá 16 ms. Nếu vượt quá ngưỡng — bản build sẽ bị từ chối cho đến khi tối ưu.
Câu hỏi thường gặp
Lag là một độ trễ không đổi (ví dụ, 200 ms cho mỗi lần chạm). Đơ không liên tục: ứng dụng hoạt động bình thường, đột nhiên chậm lại trong 1–3 giây, sau đó trở lại bình thường. Nguyên nhân là các yếu tố hướng sự kiện như khoảng dừng GC hoặc truy vấn cơ sở dữ liệu đồng bộ.
Sử dụng Memory Profiler trong Android Studio: tab Memory hiển thị các sự kiện GC với thời gian. Để giám sát sản xuất, tích hợp Firebase Performance Monitoring với các trace tùy chỉnh. Trên iOS, bật Malloc Debug và đánh dấu các thế hệ cấp phát trong Instruments.
Gián tiếp — có. Nếu phản hồi máy chủ bị chậm và UI đợi nó một cách đồng bộ, ứng dụng sẽ đứng hình. Nếu yêu cầu bất đồng bộ nhưng xử lý phản hồi được thực hiện trên luồng UI — điều đó cũng sẽ gây ra đơ. Giải pháp là xử lý bất đồng bộ với coroutine và chỉ báo tiến trình.
Khi sử dụng không đúng cách, KMP có thể tạo ra các đối tượng bao bọc quá mức cho khả năng tương tác. Trên iOS, điều này làm tăng tần suất cấp phát và do đó, các khoảng dừng ARC. Sử dụng @ObjCName, tối ưu expect/actual và tránh các lời gọi thường xuyên đến mã dùng chung từ các đường dẫn nóng của UI.
Tăng heap qua android:largeHeap=”true” làm trì hoãn GC nhưng không loại bỏ được nguyên nhân cấp phát. Khi GC cuối cùng chạy, khoảng dừng sẽ lâu hơn vì cần duyệt nhiều đối tượng hơn. Giải pháp là giảm số lượng cấp phát, không phải mở rộng heap.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm