Ứng dụng chậm là lý do chính khiến người dùng xóa chương trình. Mỗi phần nghìn giây trễ khi khởi động hoặc cuộn danh sách làm giảm tỷ lệ giữ chân người dùng hàng chục phần trăm. Hiệu suất (performance) không chỉ là tốc độ, mà còn là sự ổn định: không có ANR, crash và rò rỉ bộ nhớ. Bài viết này bao gồm tất cả các khía cạnh của hiệu suất: từ quản lý bộ nhớ (GC, ARC) đến profiling bằng công cụ. Tìm hiểu thêm trong hướng dẫn chính thức về Android Performance.
Những điểm chính
Hiệu suất ứng dụng liên quan trực tiếp đến jank — độ trễ đáng kể giữa hành động của người dùng và phản hồi của giao diện. Nguyên nhân chính: chặn luồng chính (các thao tác nặng trên luồng UI), vẽ lại bố cục thường xuyên (overdraw), rò rỉ bộ nhớ (GC thường xuyên), thuật toán không tối ưu (O(n²) trên dữ liệu lớn). Tốc độ khung hình (FPS) — số khung hình trên giây. Để có trải nghiệm thoải mái, cần 60 FPS ổn định (Android) hoặc 120 FPS (iPhone Pro, iPad Pro). VSync đồng bộ hóa kết xuất với tần số làm mới màn hình.
Jank xảy ra khi kết xuất một khung hình vượt quá 16,6 ms (đối với 60 FPS) hoặc 8,3 ms (đối với 120 FPS). Profiling GPU (Profile GPU Rendering trên Android, Core Animation trên iOS) cho thấy giai đoạn kết xuất nào tốn nhiều thời gian nhất. Các giai đoạn chính: Layout (sắp xếp các phần tử), Draw (vẽ), Display (chuyển đến bộ đệm khung hình). Vấn đề phổ biến nhất là lạm phát bố cục trong XML, đặc biệt khi sử dụng ConstraintLayout lồng nhau phức tạp.
Time-to-Interactive (TTI) — thời gian ứng dụng sẵn sàng hoàn toàn để tương tác. TTI bao gồm Cold Start, tải dữ liệu và khởi tạo thư viện. Google khuyến nghị TTI dưới 5 giây, Apple — dưới 2 giây cho các màn hình chính. Lazy Loading — kỹ thuật tải nội dung và thư viện chậm, rất quan trọng để cải thiện TTI. Tại IT Sectr, chúng tôi sử dụng khởi tạo chậm theo mặc định trong tất cả các dự án.
ANR và Crash là kẻ thù chính của hiệu suất ứng dụng di động. ANR (Application Not Responding) — hộp thoại xuất hiện trên Android nếu luồng chính bị chặn hơn 5 giây. Nguyên nhân: yêu cầu mạng đồng bộ trên luồng UI, làm việc với cơ sở dữ liệu mà không có coroutine, giải mã bitmap lớn mà không giảm mẫu, deadlock trên luồng chính. Ngăn xếp cuộc gọi ANR được lưu trong /data/anr/traces.txt và cho phép xác định chính xác vị trí bị chặn.
Crash — kết thúc ứng dụng bất ngờ. Trên Android — Exception (Java/Kotlin) hoặc Signal (mã gốc). Trên iOS — NSException hoặc tín hiệu (EXC_BAD_ACCESS — truy cập bộ nhớ đã giải phóng). Công cụ báo cáo sự cố: Firebase Crashlytics, Sentry, BugSnag. Chúng thu thập stacktrace, dữ liệu thiết bị và các bước tái tạo. Stack Overflow — tràn ngăn xếp cuộc gọi do đệ quy vô hạn. OutOfMemoryError — khi heap đầy.
StrictMode — công cụ Android để phát hiện vi phạm an toàn luồng. Nó cho phép đặt các quy tắc: ThreadPolicy (cấm đĩa/mạng trên luồng chính), VmPolicy (phát hiện rò rỉ Activity, SQLite, CloseGuard). StrictMode chỉ nên được bật trong bản gỡ lỗi — trong bản phát hành, nó không nên hoạt động. Trên iOS, tương đương là Main Thread Checker (Xcode), tự động phát hiện các cuộc gọi UIKit không trên luồng chính.
Rò rỉ bộ nhớ (Memory Leak) — tình huống một đối tượng vẫn còn trong bộ nhớ mặc dù ứng dụng không còn sử dụng nó. Điều này trực tiếp làm giảm hiệu suất ứng dụng. Trên Android, GC (Garbage Collection) không thể thu thập một đối tượng nếu có tham chiếu mạnh đến nó. Nguyên nhân điển hình: tham chiếu tĩnh đến Activity, callback/observer chưa được hủy, lớp bên trong có tham chiếu ngầm đến lớp bên ngoài, Handler với các thông báo chưa được dọn dẹp. LeakCanary — thư viện phát hiện rò rỉ tự động.
ARC (Automatic Reference Counting) — mô hình quản lý bộ nhớ trong iOS. Mỗi đối tượng có một bộ đếm tham chiếu (retain count). Khi bộ đếm về 0, bộ nhớ được giải phóng. Retain Cycle xảy ra khi hai đối tượng giữ tham chiếu mạnh đến nhau (A → B và B → A). ARC sẽ không bao giờ đặt bộ đếm về 0. Giải pháp: tham chiếu yếu (weak) hoặc vô chủ (unowned). Weak tự động được đặt thành nil khi đối tượng được giải phóng. Unowned không được đặt thành nil nhưng đảm bảo đối tượng còn sống.
GC (Garbage Collection) hoạt động trên Android (Java/Kotlin). GC tạm dừng thực thi định kỳ (tạm dừng Stop-the-World) để tìm và giải phóng các đối tượng không thể truy cập. Kích hoạt GC: khi heap đầy đến một tỷ lệ nhất định. ARC hoạt động trên iOS (Swift/Objective-C) và không có tạm dừng — bộ đếm được cập nhật nguyên tử mỗi khi gán. ARC dễ dự đoán hơn nhưng có thể tích lũy các thao tác retain/release quá mức khi tần suất gán cao.
Tham chiếu yếu (Weak Reference) và tham chiếu mạnh (Strong Reference) — loại tham chiếu quyết định liệu GC/ARC có thể giải phóng đối tượng hay không. Strong Reference — đối tượng sẽ không bị thu thập khi tham chiếu này tồn tại. Weak Reference — GC/ARC có thể thu thập đối tượng; tham chiếu yếu trở thành nil (trong Swift/Java WeakReference). Unowned Reference (Swift) — không được đặt thành nil khi giải phóng; truy cập nó sau khi đối tượng chết gây ra crash. Trên Android, java.lang.ref.WeakReference được sử dụng cho tham chiếu yếu.
Ví dụ phát hiện rò rỉ trên Android bằng LeakCanary:
// Утечка: анонимный класс держит ссылку на Activity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
// Используем `this@MainActivity`, сохраняя ссылку на Activity
Log.d("TAG", "Handler received message")
}
}
handler.sendEmptyMessageDelayed(0, 60000)
}
}
// Исправление: статический Handler + WeakReference
class SafeHandler(activity: MainActivity) : Handler() {
private val weakActivity =
WeakReference(activity)
override fun handleMessage(msg: Message) {
weakActivity.get() ?: return
Log.d("TAG", "Handler received message")
}
}
Profiling là quá trình đo lường hiệu suất ứng dụng: CPU, bộ nhớ, mạng, tiêu thụ năng lượng. Không có profiling, tối ưu hóa mù quáng là vô ích — bạn sẽ không biết phần mã nào thực sự chậm.
| Công cụ | Nền tảng | Đo lường | Khi nào sử dụng |
|---|---|---|---|
| Instruments (Time Profiler) | iOS | CPU, gọi hàm, thời gian thực thi | Tối ưu hóa thuật toán, tìm điểm nghẽn |
| Instruments (Allocations) | iOS | Bộ nhớ, số lượng đối tượng, retain counts | Tìm rò rỉ và tiêu thụ bộ nhớ quá mức |
| Instruments (Leaks) | iOS | Retain cycles, rò rỉ bộ nhớ | Kiểm tra thường xuyên trước khi phát hành |
| Android Profiler (CPU) | Android | Sử dụng CPU, hoạt động luồng, traces | Tìm chặn luồng chính |
| Android Profiler (Memory) | Android | Heap dump, theo dõi cấp phát | Tìm rò rỉ, phân tích đối tượng |
| Android Profiler (Network) | Android | Lưu lượng, tốc độ, thời gian yêu cầu | Tối ưu hóa cuộc gọi mạng |
| LeakCanary | Android | Phát hiện tự động rò rỉ bộ nhớ | Ở tất cả các giai đoạn phát triển |
| StrictMode | Android | Đĩa/mạng trên luồng chính, rò rỉ | Bản gỡ lỗi |
| Traceview / Systrace | Android | Theo dõi phương thức, sự kiện hệ thống | Phân tích độ trễ sâu |
Instruments (Xcode) — công cụ mạnh nhất cho iOS. Time Profiler hiển thị hàm nào tiêu thụ nhiều CPU nhất. Allocations theo dõi việc tạo và giải phóng đối tượng. Leaks tự động tìm retain cycles. Các bước profiling: (1) khởi chạy Instruments; (2) chọn mẫu (Time Profiler cho CPU); (3) chạy kịch bản có vấn đề; (4) phân tích ngăn xếp cuộc gọi — cột rộng nhất là hàm "nóng" nhất.
Android Profiler được tích hợp trong Android Studio (View → Tool Windows → Profiler). CPU Profiler hiển thị tải của mỗi luồng. Memory Profiler — heap dump và theo dõi cấp phát. Network Profiler — tất cả yêu cầu HTTP với thời gian. Energy Profiler — tiêu thụ năng lượng: WakeLock, Location, Network. Để theo dõi chi tiết, sử dụng Systrace (Android 10+) hoặc Perfetto — theo dõi hệ thống với độ chính xác micro giây.
Khởi động ứng dụng là một trong những chỉ số hiệu suất chính. Nó được chia thành ba loại: Cold Start — ứng dụng khởi động từ đầu: quy trình được tạo, Application.onCreate (Android) / AppDelegate.applicationDidFinishLaunching (iOS), tải lớp, khởi tạo thư viện. Warm Start — quy trình tồn tại nhưng Activity/ViewController bị hủy (ví dụ: khi xoay màn hình hoặc trở về từ bộ nhớ). Hot Start — Activity/ViewController trong bộ nhớ, ứng dụng chỉ được hiển thị (chuyển từ ứng dụng khác).
Cold Start là chỉ số quan trọng nhất. Trên Android bao gồm: (1) launch Activity — tải XML, khởi tạo View; (2) khung hình đầu tiên — thời gian đến lần kết xuất đầu tiên. Google khuyến nghị: launch Activity < 200 ms, khung hình đầu tiên < 500 ms, TTI < 5 giây. Tối ưu hóa Cold Start: giảm Application.onCreate (coroutine để khởi tạo chậm), sử dụng SplashScreen API (Android 12+), hoãn khởi tạo thư viện (WorkManager, DI), xóa ContentProviders không cần thiết.
Trên iOS, Cold Start bao gồm: tải tệp nhị phân Mach-O, dyld (trình liên kết động), khởi tạo runtime Objective-C, delegate ứng dụng, bộ điều khiển đầu tiên. Chrome Custom Tabs (Android) và Universal Links (iOS) — công nghệ mở nội dung bên ngoài nhanh chóng trong ứng dụng mà không cần Cold Start đầy đủ. Nên kiểm tra Cold Start trên các thiết bị thực tế tầm trung.
Kích thước ứng dụng là yếu tố hiệu suất cho việc cài đặt và cập nhật. Nó ảnh hưởng đến tỷ lệ chuyển đổi: mỗi 10 MB làm giảm tỷ lệ chuyển đổi 1%. Google Play khuyến nghị kích thước APK dưới 150 MB; App Store — dưới 200 MB (mạng di động — 100 MB). Các phương pháp tối ưu hóa chính: nén hình ảnh (WebP thay vì PNG tiết kiệm 25-35%), vector hóa (VectorDrawable trên Android, SF Symbols trên iOS), xóa mã không sử dụng (R8/ProGuard), xóa tài nguyên không sử dụng (lint → unused resources).
App Bundle (Android) — định dạng xuất bản mà Google Play tạo APK tối ưu hóa cho mỗi thiết bị. App Bundle giảm kích thước tải xuống 20-40%. Dynamic Delivery — các mô-đun được tải theo yêu cầu (on-demand feature modules). Trên iOS, tương đương là On-Demand Resources (ODR): tài nguyên được tải sau lần khởi chạy đầu tiên (cấp độ trò chơi, video).
Lazy Loading — kỹ thuật mà các mô-đun và thư viện không được tải khi khởi động mà được tải khi cần. Split APK (Android) và App Slicing (iOS) — chia ứng dụng thành các khe kiến trúc: arm64-v8a, x86_64. Tối ưu hóa kích thước ứng dụng — một quá trình liên tục: phân tích thành phần APK (Analyze APK trong Android Studio), xóa biểu tượng trùng lặp, sử dụng SVG thay vì nhiều mật độ PNG. Tại IT Sectr, chúng tôi bao gồm kiểm tra kích thước bản dựng trong CI/CD cho mỗi MR.
Câu hỏi thường gặp
ANR (Application Not Responding) — hộp thoại xuất hiện trên Android nếu luồng chính bị chặn hơn 5 giây. Để tránh ANR, hãy chuyển tất cả các thao tác nặng (mạng, cơ sở dữ liệu, xử lý tệp) sang luồng nền. Tương đương trên iOS là frozen UI, khi ứng dụng ngừng phản hồi khi chạm.
Rò rỉ bộ nhớ — khi một đối tượng không thể được giải phóng vì vẫn còn tham chiếu đến nó. Retain Cycle — tình huống trong iOS/Objective-C khi hai đối tượng tham chiếu lẫn nhau (A → B → A) và ARC không thể giải phóng cái nào. Giải pháp: tham chiếu weak/unowned và dọn dẹp callback kịp thời.
Cho iOS: Instruments (Time Profiler, Allocations, Leaks). Cho Android: Android Profiler (CPU, Memory, Network), LeakCanary (rò rỉ bộ nhớ), StrictMode (vi phạm luồng). Nên kết hợp profiling trong quá trình phát triển và tích hợp.
Cold Start — ứng dụng khởi động từ đầu: quy trình được tạo, lớp được tải, Application.onCreate chạy. Warm Start — quy trình tồn tại nhưng Activity/ViewController được tạo lại. Hot Start — Activity/ViewController đã có trong bộ nhớ, chỉ được hiển thị. Cold Start là chậm nhất (1-5 giây) và rất quan trọng cho trải nghiệm người dùng.
Các phương pháp chính: xóa tài nguyên và mã không sử dụng (sử dụng R8/ProGuard), vector hóa hình ảnh (VectorDrawable, SF Symbols), nén PNG/WebP (Android), sử dụng App Bundle thay vì APK, xóa thư viện không cần thiết, sử dụng Lazy Loading cho các mô-đun. Tối ưu hóa kích thước có thể giảm APK 40-60%.
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.