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 (đơ) 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.
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.
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.
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.
Đọ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ủ.
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.
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.
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.
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.
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 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:
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())
}
}
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.
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.
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.
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:
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)
}
}
}
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.
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.
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.
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.
Câu hỏi thường gặp
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.
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.
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.
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 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
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