Mất kết nối — một trong những hiện tượng phổ biến và gây khó chịu nhất trong ứng dụng di động. Người dùng mất quyền truy cập dữ liệu, thao tác bị gián đoạn, ứng dụng bị đơ hoặc treo. Theo Google Android Developer Blog, 70% người dùng xóa ứng dụng nếu nó bị treo hoặc đơ hai lần. Hãy cùng tìm hiểu nguyên nhân mất kết nối và cách xây dựng hệ thống truyền thông chịu lỗi.
Những điểm chính
Mất kết nối — thuật ngữ người dùng mô tả tình huống ứng dụng mất kết nối với máy chủ, ngừng phản hồi thao tác hoặc kết thúc với lỗi. Về mặt kỹ thuật, đây có thể là: lỗi mạng (hết thời gian chờ, lỗi DNS), ANR (đơ luồng UI), treo (ngoại lệ không được xử lý) hoặc điều kiện cạnh tranh (race condition).
Từ góc nhìn của người dùng, tất cả các kịch bản này đều giống nhau: ứng dụng ngừng hoạt động. Sự khác biệt đối với nhà phát triển nằm ở cách tiếp cận chẩn đoán và sửa chữa. Lỗi mạng được giải quyết bằng cơ chế thử lại, ANR bằng cách đưa thao tác ra khỏi luồng UI, treo bằng cách xử lý ngoại lệ.
Theo Crittercism (nay là Apteligent), trung bình một ứng dụng di động mất 1–2% người dùng sau mỗi lần treo. Đối với ứng dụng có 1 triệu người dùng, điều này có nghĩa là 10–20 nghìn lượt cài đặt bị mất cho một lỗi duy nhất. Điều này đặc biệt quan trọng đối với ứng dụng trong lĩnh vực tài chính và y tế.
Mạng không ổn định — thiết bị di động liên tục chuyển đổi giữa Wi-Fi và mạng di động, đi vào khu vực không có phủ sóng (tàu điện ngầm, thang máy, tầng hầm). Mỗi lần chuyển đổi gây mất kết nối tạm thời mà ứng dụng phải xử lý đúng cách.
Hết thời gian chờ — nếu máy chủ không phản hồi trong thời gian chờ đã đặt (thường 10–30 giây), máy khách ném SocketTimeoutException. Thời gian chờ dài mà không có phản hồi bị người dùng coi là đơ. Nên đặt thời gian chờ không quá 15 giây.
val client = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build()
Điều kiện cạnh tranh (race condition) — xảy ra khi nhiều luồng đọc và ghi cùng một dữ liệu đồng thời mà không đồng bộ hóa. Ví dụ, tải dữ liệu từ bộ nhớ đệm trong luồng UI song song với cập nhật bộ nhớ đệm từ mạng có thể dẫn đến hiển thị dữ liệu cũ hoặc không chính xác.
Offline-first — một mẫu kiến trúc nơi bộ nhớ cục bộ (Room, CoreData) là nguồn dữ liệu gốc duy nhất. Mạng được sử dụng để đồng bộ dữ liệu nền. Người dùng luôn thấy dữ liệu cập nhật từ bộ nhớ đệm cục bộ, ngay cả khi không có kết nối mạng.
Mẫu Repository — một điểm truy cập duy nhất cho dữ liệu quyết định lấy dữ liệu từ mạng hay từ bộ nhớ đệm. Repository trừu tượng hóa nguồn dữ liệu khỏi ViewModel và UI. Khi có lỗi mạng, repository tự động chuyển sang nguồn cục bộ.
class UserRepository(
private val api: UserApi,
private val dao: UserDao
) {
suspend fun getUsers(): Result<List<User>> {
return try {
val remote = api.fetchUsers()
dao.insertAll(remote)
Result.success(remote)
} catch (e: IOException) {
val cached = dao.getAll()
if (cached.isNotEmpty()) {
Result.success(cached) // return cache on network error
} else {
Result.failure(e)
}
}
}
}
Circuit Breaker — một mẫu bảo vệ máy chủ khỏi làn sóng yêu cầu khi không khả dụng. Sau N lỗi liên tiếp, bộ ngắt mạch mở ra và tất cả yêu cầu ngay lập tức trả về lỗi mà không thử kết nối. Sau một thời gian chờ nhất định, bộ ngắt mạch chuyển sang trạng thái nửa mở cho một yêu cầu thử nghiệm.
Backoff theo cấp số nhân (Exponential backoff) — cơ chế thử lại tiêu chuẩn. Sau lần thất bại đầu tiên, chờ 1 giây; sau lần thứ hai, 2 giây; sau đó 4, 8, 16. Giới hạn số lần thử lại tối đa (thường 3–5) để không làm quá tải máy chủ và pin.
Phản hồi người dùng — khi có lỗi mạng, hiển thị thông báo rõ ràng: “Không có kết nối”, “Máy chủ tạm thời không khả dụng”, “Kiểm tra kết nối Internet”. Sử dụng Snackbar hoặc Inline State View. Không bao giờ hiển thị lỗi kỹ thuật (HTTP 500, SocketException) cho người dùng.
ConnectivityManager — API Android để giám sát mạng. Cho phép ứng dụng phản ứng với thay đổi: hiển thị trình giữ chỗ khi mất kết nối, tự động làm mới dữ liệu khi khôi phục. Trên iOS, sử dụng NWPathMonitor từ framework Network.
class NetworkMonitor(private val context: Context) {
private val manager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
fun isOnline(): Boolean {
val network = manager.activeNetwork ?: return false
val caps = manager.getNetworkCapabilities(network) ?: return false
return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}
}
Crashlytics (Firebase) — công cụ báo cáo treo tiêu chuẩn cho ứng dụng di động. Nó thu thập stacktrace của tất cả ngoại lệ không được xử lý, phiên bản HĐH, mẫu thiết bị và thời gian treo. Nó cho phép nhóm lỗi và chỉ định người chịu trách nhiệm sửa lỗi.
Sentry — giải pháp thay thế Crashlytics với hỗ trợ giám sát hiệu suất. Nó cho phép theo dõi các giao dịch cụ thể (ví dụ: “ủy quyền người dùng”) và xem bước nào xảy ra lỗi. Theo dõi hiệu suất giúp phân biệt thời gian chờ mạng với lỗi trong logic ứng dụng.
Timber — thư viện ghi nhật ký cho Android với tính năng tự động thêm thẻ theo lớp. Trong bản dựng gỡ lỗi, ghi lại tất cả yêu cầu và phản hồi mạng. Trong bản dựng phát hành, chỉ ghi lại lỗi và cảnh báo qua Crashlytics.setCustomLog.
| Công cụ | Loại | Khi nào sử dụng |
|---|---|---|
| Crashlytics | Báo cáo treo | Luôn trong bản phát hành — tự động thu thập treo |
| Sentry | Treo + Hiệu suất | Khi cần phân tích kịch bản người dùng cụ thể |
| Timber | Ghi nhật ký | Gỡ lỗi: ghi đầy đủ; Phát hành: chỉ lỗi |
| HTTP Toolkit | Gỡ lỗi mạng | Chặn và phân tích lưu lượng HTTP cục bộ |
Theo Firebase Summit 2023, các ứng dụng triển khai Crashlytics + Performance Monitoring giảm thời gian phát hiện và sửa lỗi nghiêm trọng trung bình từ 3 ngày xuống 4 giờ. Nên thiết lập cảnh báo cho mỗi lần treo với tần suất trên 0,1% người dùng hoạt động.
Câu hỏi thường gặp
Nếu treo không bị bắt trong Crashlytics, hãy kiểm tra treo gốc (SIGSEGV, SIGABRT) — chúng không được xử lý bởi trình xử lý ngoại lệ Java/Kotlin. Trên Android, đây có thể là rò rỉ bộ nhớ gốc từ JNI; trên iOS, EXC_BAD_ACCESS. Sử dụng Breakpad (Android) hoặc PLCrashReporter (iOS) để thu thập stacktrace treo gốc.
Sử dụng Network Link Conditioner (tích hợp trong iOS; cho Android, sử dụng Facebook Network Connection Class hoặc Developer Options > Network > Select network type). Đặt độ trễ 500–3000 ms và mất gói 5–30%. Bạn cũng có thể sử dụng Charles Proxy hoặc mitmproxy để mô phỏng độ trễ mạng và ngắt kết nối.
ANR xảy ra nếu luồng UI bị chặn quá 5 giây. Yêu cầu mạng nên được thực thi trong luồng nền: coroutine (viewModelScope.launch(Dispatchers.IO)), RxJava (subscribeOn(Schedulers.io)), hoặc WorkManager để đồng bộ hóa. Luôn đặt thời gian chờ trên máy khách HTTP — thiếu thời gian chờ có thể dẫn đến chặn vĩnh viễn.
Điều kiện cạnh tranh — tình huống mà kết quả của thao tác phụ thuộc vào thứ tự thực thi luồng. Ví dụ, người dùng nhấn nhanh nút “Gửi” hai lần và yêu cầu được gửi hai lần. Giải pháp: sử dụng Mutex, bộ thực thi đơn luồng hoặc máy trạng thái (vô hiệu hóa nút sau lần nhấp đầu tiên). Trong Kotlin, sử dụng Mutex từ coroutine hoặc chú thích @Synchronized.
Áp dụng Chaos Engineering cho ứng dụng di động: ngắt mạng trong khi thao tác, mô phỏng độ trễ cao, chuyển đổi giữa Wi-Fi và mạng di động, giết tiến trình qua hệ thống. Công cụ: Facebook Network Connection Class, Charles Proxy, iOS Network Link Conditioner. Trong CI/CD, thêm kiểm thử UI với các điều kiện mạng khác nhau qua AndroidTest Orchestrator.
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