Treo trong phát triển — bản chất, nguyên nhân và phòng tránh

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

Treo (đơ) là trạng thái ứng dụng di động ngừng phản hồi mọi thao tác của người dùng trong thời gian dài. Khác với lag (chậm) và glitch (hành vi sai), treo chặn hoàn toàn UI: chạm không được xử lý, hoạt ảnh dừng, màn hình “đóng băng”. Nguyên nhân là chặn luồng chính bằng thao tác đồng bộ, deadlock trong mã đa luồng hoặc thu gom rác dài bất thường. Theo Tài liệu Apple Main Thread Checker, hơn 40% báo cáo crash iOS liên quan đến chặn luồng chính. Trên Android, tình huống tương tự dẫn đến ANR — hộp thoại hệ thống “Ứng dụng không phản hồi”.

Chính yếu

  • Treo — chặn hoàn toàn UI trong thời gian dài (giây và hàng chục giây), khác với lag và glitch
  • Nguyên nhân chính — chặn luồng chính bằng I/O, deadlock giữa các luồng, vòng lặp vô hạn và rò rỉ bộ nhớ với GC dài
  • Chẩn đoán bao gồm Main Thread Checker trên iOS, nhật ký ANR /data/anr/traces.txt trên Android và phân tích dump luồng
  • Khắc phục — chuyển tất cả thao tác có khả năng dài sang luồng nền, sử dụng Structured Concurrency và tránh synchronized trong luồng UI
  • Phòng tránh — StrictMode, Main Thread Checker trong lược đồ Debug, phân tích tĩnh deadlock và kiểm tra định kỳ đo thời gian phản hồi

Treo trong phát triển di động là gì

Treo (đơ) trong ứng dụng di động là trạng thái ứng dụng ngừng xử lý sự kiện đầu vào và cập nhật giao diện trong vài giây hoặc hơn. Về mặt kỹ thuật, điều này có nghĩa là luồng chính bị chặn và không thể thực hiện vòng lặp chạy tiếp theo.

Khác biệt giữa treo, lag và ANR

Lag là độ trễ lên đến 500 ms mà người dùng nhận thấy chậm nhưng ứng dụng vẫn chạy. Treo kéo dài từ 1 giây đến hàng chục giây. ANR trên Android là trường hợp đặc biệt của treo kéo dài hơn 5 giây và được hệ thống phát hiện. Không phải mọi treo đều dẫn đến ANR, nhưng mọi ANR đều là treo được hệ thống ghi nhận.

Hậu quả của treo

Trên Android, treo hơn 5 giây kích hoạt hộp thoại ANR đề nghị đóng ứng dụng. Trên iOS, hệ thống có watchdog — nếu ứng dụng không phản hồi sự kiện trong 10–20 giây, Watchdog kết thúc tiến trình với mã 0x8badf00d (ate bad food). Người dùng chỉ thấy ứng dụng đột ngột đóng về màn hình chính.

Nguyên nhân treo trên Android và iOS

Bất kỳ thao tác nào mất hơn 100 ms và chạy trên luồng chính đều có khả năng gây treo. Hãy xem các nguồn chặn chính.

I/O đồng bộ trong luồng UI

Đọc tệp lớn, yêu cầu mạng không bất đồng bộ, lưu dữ liệu vào SharedPreferences bằng phương thức đồng bộ apply rồi commit — tất cả thao tác này chặn luồng chính. Trên Android, đọc đồng bộ tệp 10 MB có thể mất 200–500 ms tùy tốc độ bộ nhớ flash. Trên iOS, tải URLSession đồng bộ không có completionHandler chặn UI trong thời gian phản hồi máy chủ.

Deadlock trong mã đa luồng

Khi hai luồng chờ tài nguyên do nhau giữ, xảy ra deadlock. Trong ứng dụng di động, kịch bản điển hình là luồng A khóa Lock1 và chờ Lock2, trong khi luồng B khóa Lock2 và chờ Lock1. Cả hai luồng treo vĩnh viễn. Nếu một trong số đó là luồng chính, ứng dụng treo hoàn toàn.

Vòng lặp vô hạn hoặc đệ quy

Lỗi logic — ví dụ while(true) không có điều kiện thoát hoặc đệ quy không có trường hợp cơ sở — dẫn đến thực thi vô hạn trên luồng chính. Android phát hiện qua ANR sau 5 giây, iOS — qua Stackshot, ghi lại ngăn xếp cuộc gọi lặp vô hạn.

  • Android — Cursor không đóng, yêu cầu đồng bộ qua execute() thay vì enqueue(), FileInputStream.read() trong luồng UI
  • iOS — performSelectorOnMainThread:withObject:waitUntilDone:YES, khởi chạy NSURLConnection sendSynchronousRequest, tải ảnh với dataWithContentsOfURL
  • Đa nền tảng — Flutter compute không có isolate chuyên dụng, React Native NativeModule đồng bộ

Cách chẩn đoán treo

Chẩn đoán treo cần công cụ có thể ghi lại trạng thái tất cả luồng tại thời điểm chặn.

Nhật ký ANR trên Android

Mỗi ANR, hệ thống Android lưu tệp /data/anr/traces.txt chứa dump ngăn xếp của mỗi luồng ứng dụng. Phân tích tệp này là phương pháp chẩn đoán chính: tìm luồng main và xem nó dừng ở phương thức nào. Nếu ngăn xếp kết thúc bằng Thread.sleep, InputStream.read hoặc Lock.lock — đã tìm ra nguyên nhân.

Stackshot trên iOS

Xcode có thể chụp Stackshot — ảnh chụp ngăn xếp tất cả luồng — khi ứng dụng treo (tín hiệu SIGSTOP). Bật “Logging” → “Include Stackshot Logs” trong lược đồ. Khi crash mã 0x8badf00d, trích xuất nhật ký crash từ Devices & Simulators và tìm luồng com.apple.main-thread với ngăn xếp bị kẹt.

Main Thread Checker trong Xcode

Main Thread Checker tự động phát hiện cuộc gọi UIKit từ luồng nền trong khi ứng dụng chạy. Bật nó trong lược đồ (Diagnostics → Main Thread Checker). Mỗi cảnh báo là nguyên nhân tiềm ẩn gây treo, đặc biệt nếu xảy ra trong closure completionHandler của yêu cầu mạng.

Ví dụ phát hiện chặn qua StrictMode trên Android:

kotlin
class App : Application() {
    override fun onCreate() {
        super.onCreate()
        StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
            .detectLeakedSqlLiteObjects()
            .detectLeakedClosableObjects()
            .penaltyLog()
            .build())
        StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
            .detectAll()
            .penaltyDeath()
            .build())
    }
}

Phương pháp loại bỏ chặn UI

Loại bỏ treo bắt đầu bằng việc chuyển tất cả thao tác có khả năng dài sang luồng nền. Hãy xem kỹ thuật cụ thể cho từng nền tảng.

Đồng thời có cấu trúc với coroutine

Kotlin Coroutines với viewModelScope.launch(Dispatchers.IO) đảm bảo thao tác mạng hoặc đọc cơ sở dữ liệu chạy trong luồng nền. Dispatchers.Main chỉ dùng để cập nhật UI. Quan trọng: tất cả hàm suspend phải có cấu trúc — coroutine con bị hủy khi coroutine cha bị hủy, ngăn rò rỉ luồng.

Hàng đợi bất đồng bộ trên iOS

Grand Central Dispatch với DispatchQueue.global(qos: .userInitiated) cho tác vụ nền và DispatchQueue.main.async cho cập nhật UI là mẫu chuẩn. Tránh sync() trên hàng đợi chính — đây là deadlock chắc chắn. Sử dụng async/await (Swift 5.5+) cho mã bất đồng bộ dễ đọc hơn với tự động trở về luồng chính qua MainActor.

Tránh synchronized trong luồng UI

Khối synchronized trong Kotlin và @synchronized trong Swift trên luồng chính rất nguy hiểm: nếu luồng khác đã giành khóa này, luồng chính sẽ treo chờ. Sử dụng kiểu nguyên tử (AtomicInteger, thuộc tính nguyên tử trong Swift) hoặc hàng đợi tuần tự thay vì khóa.

Ví dụ tải dữ liệu bất đồng bộ với coroutine trên Android:

kotlin
class DataViewModel : ViewModel() {
    private val _data = MutableStateFlow<List<Item>>(emptyList())
    val data: StateFlow<List<Item>> = _data.asStateFlow()

    fun loadData() {
        viewModelScope.launch(Dispatchers.IO) {
            val result = fetchFromNetwork()
            _data.emit(result)
        }
    }
}

Phòng tránh treo ở giai đoạn phát triển

Sự kết hợp công cụ, nguyên tắc kiến trúc và quy trình đánh giá mã giúp phòng tránh treo một cách có hệ thống.

StrictMode với penaltyDeath

Cấu hình StrictMode với penaltyDeath cho chính sách luồng — điều này sẽ gây crash ứng dụng ngay lập tức khi phát hiện cuộc gọi mạng hoặc I/O đĩa trên luồng chính. Nhà phát triển không thể bỏ qua vấn đề. Trong bản dựng sản xuất, sử dụng penaltyLog để thu thập thống kê mà không crash.

Main Thread Checker trong lược đồ Debug

Trên iOS, bật Main Thread Checker trong lược đồ Debug và cấu hình CI chạy kiểm tra với tùy chọn này. Nếu kiểm tra chứa cuộc gọi UIKit từ luồng nền — nó phải thất bại. Đây là cách đáng tin cậy duy nhất để xác định vấn đề trước khi gửi lên TestFlight.

Đánh giá đồng nghiệp với kiểm tra đa luồng

Thêm điểm bắt buộc vào quy trình đánh giá mã: kiểm tra mọi cuộc gọi mạng, thao tác tệp, truy cập cơ sở dữ liệu hoặc tính toán nặng chạy trong luồng nền. Deadlock có thể được phát hiện bằng trình phân tích tĩnh: Infer của Facebook và Thread Safety Checker của Xcode tìm khóa tiềm ẩn trước khi chạy.

  • Android — StrictMode, Infer, Android Lint Multithread, Kotlin Coroutines với viewModelScope
  • iOS — Main Thread Checker, TSAN (Thread Sanitizer), Xcode Analyze, Swift async/await với MainActor
  • Đa nền tảng — Flutter compute isolate, React Native interaction manager với requestAnimationFrame

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

Sự khác biệt giữa treo và ANR là gì?

ANR (Application Not Responding) là thông báo hệ thống Android xuất hiện khi luồng chính treo hơn 5 giây. Treo là khái niệm rộng hơn: bất kỳ chặn UI nào ở bất kỳ thời lượng nào. iOS không có ANR nhưng có Watchdog với thời gian chờ 10–20 giây.

Cách đọc traces.txt trên Android?

Tệp nằm ở /data/anr/traces.txt. Truy cập cần quyền root hoặc adb shell: chạy adb shell cat /data/anr/traces.txt \> traces.txt với quyền root. Trong ngăn xếp, tìm luồng “main” — phương thức được gọi cuối cùng chỉ ra nguyên nhân chặn.

Tại sao ứng dụng treo trên iOS nhưng không crash?

Nếu treo kéo dài dưới 10 giây, Watchdog không kích hoạt và ứng dụng chỉ “đơ” cho đến khi thao tác chặn hoàn thành. Người dùng không thấy crash nhưng cảm thấy khó chịu. Để phát hiện trường hợp này, sử dụng MetricKit với dấu vết thời gian thực thi tùy chỉnh.

Cách kiểm tra ứng dụng về treo?

Sử dụng kiểm tra UI với xác nhận màn hình mở trong < 1 giây. Thêm đo thời gian giữa chạm và xuất hiện màn hình tiếp theo vào CI. Trên Android, sử dụng Espresso với IdlingResource để chờ thao tác bất đồng bộ. Trên iOS, sử dụng XCTest với XCTWaiter để kiểm tra thời gian tải.

SwiftUI có thể gây treo không?

SwiftUI tự nó không gây treo, nhưng tính toán phức tạp trong thuộc tính body thì có. Nếu body mất 500 ms để tính do thao tác nặng, UI bị treo. Giải pháp là chuyển tính toán sang Task.detached và cập nhật @State bất đồng bộ trên tác tử chính.

Tổng kết

  • Treo — chặn hoàn toàn UI trong giây đến hàng chục giây, do chặn luồng chính, deadlock hoặc vòng lặp vô hạn
  • Chẩn đoán — /data/anr/traces.txt trên Android, Stackshot và Main Thread Checker trên iOS
  • Nguyên nhân chính — I/O đồng bộ, deadlock giữa luồng, đệ quy vô hạn, GC dài
  • Khắc phục — coroutine với bộ điều phối đúng, async/await với MainActor, chuyển tất cả thao tác I/O sang luồng nền
  • Phòng tránh — StrictMode với penaltyDeath, Main Thread Checker, phân tích tĩnh deadlock (Infer, TSAN)
  • Trên Android treo > 5 giây = ANR; trên iOS > 10–20 giây = Watchdog crash (0x8badf00d)
  • Khuyến nghị: bật Thread Sanitizer trong lược đồ Debug và cấu hình CI chạy kiểm tra với TSAN để phát hiện data race và deadlock

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