Đơ trong phát triển — đó là gì, nguyên nhân và phương pháp tối ưu

Tác giả: IT Sectr Đã đăng: 2026-07-28 Thời gian đọc: 8 phút

Đơ 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à sự chậm lại không liên tục của ứng dụng, xen kẽ với hiệu suất bình thường
  • Nguyên nhân chính — các khoảng dừng GC thường xuyên, thao tác đồng bộ trên luồng UI, khối lượng dữ liệu lớn trong bộ điều hợp mà không phân trang
  • Chẩn đoán cần CPU Profiler để tìm nút thắt cổ chai và Memory Profiler để phân tích tần suất và thời gian GC
  • Khắc phục bao gồm triển khai phân trang (Paging 3), tối ưu truy vấn SQL qua Room và chuyển các tác vụ nặng sang WorkManager
  • Phòng ngừa — Benchmark Baseline Profiles, biên dịch AOT, giảm thiểu cấp phát trong các đường dẫn mã nóng

“Đơ” có nghĩa là gì trong phát triển di động

Đơ 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.

Mô tả kỹ thuật của hiện tượng

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.

Nhận thức của người dùng

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.

Nguyên nhân gây chậm đột ngột trong ứng dụng

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.

Khoảng dừng GC khi cấp phát đối tượng

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.

Truy vấn SQL đồng bộ trên luồng UI

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 đó.

Giải mã hình ảnh không giảm kích thước

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.

  • Android — nối chuỗi trong vòng lặp, tạo đối tượng trong đường dẫn nóng, Bitmap không có inSampleSize
  • iOS — pool autorelease với nhiều đối tượng, imageWithContentsOfFile không thay đổi tỷ lệ, URLSession đồng bộ
  • Đa nền tảng — phân tích JSON trên luồng UI, tải dữ liệu trên luồng chính trong khi chờ phản hồi máy chủ

Cách chẩn đoán đứng hình trên Android và iOS

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.

Memory Profiler với ghi lại sự kiện GC

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ớ.

Xcode Instruments với Allocation Tracking

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 đó.

API JankStats trên Android

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:

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

Phương pháp loại bỏ hiệu suất chậm

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ể.

Triển khai phân trang qua Paging 3

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.

Tối ưu truy vấn SQL và chỉ mục

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ự.

Chuyển tác vụ sang WorkManager

Đồ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:

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

Phòng ngừa đơ ở giai đoạn phát triển

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 cho biên dịch AOT

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.

Giảm thiểu cấp phát trong các đường dẫn nóng

Đườ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.

Hồ sơ qua Baseline Profiles trong CI

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.

  • Android — Baseline Profiles, Macrobenchmark, JankStats, StrictMode với penaltyDeath
  • iOS — MetricKit, os_signpost, XCTMetric, Main Thread Checker trong lược đồ Debug
  • Cách tiếp cận chung — hồ sơ thường xuyên, đánh giá mã tập trung vào cấp phát trên đường dẫn nóng

Câu hỏi thường gặp

Đơ khác với lag thông thường như thế nào?

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ộ.

Làm thế nào để đo tần suất khoảng dừng GC trên Android?

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.

Các yêu cầu mạng có thể gây ra đơ không?

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.

Kotlin Multiplatform ảnh hưởng đến hiệu suất như thế nào?

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 kích thước heap trên Android có giúp không?

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

  • Đơ là sự chậm lại không liên tục của ứng dụng do các yếu tố hướng sự kiện (khoảng dừng GC, truy vấn đồng bộ, giải mã hình ảnh)
  • Chẩn đoán cần Memory Profiler, JankStats trên Android và Allocation Tracking trong Instruments trên iOS
  • Nguyên nhân chính — các khoảng dừng GC thường xuyên, thiếu phân trang, truy vấn SQL không tối ưu và xử lý đồng bộ trên luồng UI
  • Khắc phục — Paging 3, WorkManager, tối ưu chỉ mục DB, giảm kích thước hình ảnh và giảm thiểu cấp phát
  • Phòng ngừa — Baseline Profiles, Macrobenchmark, StrictMode, đánh giá mã với kiểm tra đường dẫn nóng
  • Công cụ — JankStats, Firebase Performance, MetricKit để giám sát đơ trong sản xuất
  • Khuyến nghị: triển khai các lần chạy Macrobenchmark thường xuyên trong CI với ngưỡng 16 ms ở phân vị thứ 99 của khung hình

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.

Thảo luận dự án

Đọc thêm