11 cách tối ưu hiệu suất ứng dụng - Phần 1

Để giúp bạn luôn đi trước trong môi trường cạnh tranh này, chúng tôi đã tổng hợp một hướng dẫn toàn diện về tối ưu hóa hiệu suất ứng dụng Android. Là một nhà phát triển ứng dụng, bạn cần hiểu tầm quan trọng của việc tạo ra một ứng dụng mượt mà, phản hồi nhanh và hiệu quả.

Với hàng triệu ứng dụng trên Google Play Store, người dùng đã quen với việc kỳ vọng một mức độ hiệu suất cao, và bất kỳ trải nghiệm kém chất lượng nào cũng có thể dẫn đến đánh giá tiêu cực hoặc tệ hơn, bị gỡ cài đặt.

Trong loạt bài viết này, chúng ta sẽ đi sâu vào các lĩnh vực chính cần tập trung để cải thiện hiệu suất của ứng dụng, bao gồm tối ưu hóa mã, cải tiến giao diện người dùng (UI/UX), tối ưu hóa sử dụng mạng, đa luồng, lưu trữ, sử dụng pin và nhiều hơn nữa.

Chúng ta cũng sẽ thảo luận về tầm quan trọng của việc sử dụng thư viện Android Jetpack, giữ cho kích thước ứng dụng nhỏ và duy trì các thư viện cũng như SDK luôn cập nhật.

Tối ưu hóa mã là điều cần thiết để đảm bảo ứng dụng của bạn chạy mượt mà và hiệu quả. Hãy cùng xem một số cách để đạt được hiệu suất qua việc tối ưu hóa mã.

11 cách tối ưu hiệu suất ứng dụng - Phần 1

Tối ưu hóa mã

Sử dụng Android Profiler để theo dõi và xác định các điểm nghẽn hiệu suất

Android Profiler là một công cụ mạnh mẽ được tích hợp trong Android Studio giúp theo dõi hiệu suất của ứng dụng theo thời gian thực. Nó giám sát việc sử dụng CPU, bộ nhớ và mạng, và xác định các điểm nghẽn có thể làm chậm ứng dụng.

Bằng cách xử lý những điểm nghẽn này, bạn có thể cải thiện đáng kể hiệu suất của ứng dụng. Tuy nhiên, điều này đôi khi có thể tiêu tốn tài nguyên và làm chậm môi trường phát triển của bạn.

Sử dụng Android Profiler để theo dõi và xác định các điểm nghẽn hiệu suất

Giảm phân bổ đối tượng để tránh việc thu gom rác (Garbage Collection)

Giảm thiểu việc phân bổ đối tượng sẽ giảm các sự kiện thu gom rác, dẫn đến ít gián đoạn hiệu suất hơn. Trong Kotlin, bạn có thể sử dụng các hàm mở rộng như apply, also, letwith để tránh tạo ra các đối tượng tạm thời không cần thiết.

Ngoài ra, hãy cân nhắc sử dụng từ khóa object cho các đối tượng singleton và tránh sử dụng các lớp ẩn danh (anonymous inner classes) có thể gây ra rò rỉ bộ nhớ.

Ví dụ:

// Thay vì tạo một đối tượng mới mỗi lần, hãy sử dụng singleton.
object Singleton {
    fun doSomething() { /*...*/ }
}

// Sử dụng các hàm mở rộng để tránh đối tượng tạm thời.
val modifiedList = myList.map { it * 2 }.filter { it > 10 }

Sử dụng cấu trúc dữ liệu và thuật toán phù hợp

Việc lựa chọn cấu trúc dữ liệu và thuật toán đúng có thể ảnh hưởng đáng kể đến hiệu suất của ứng dụng. Ví dụ, nếu bạn cần tra cứu các mục theo khóa của chúng, việc sử dụng HashMap thay vì List có thể tăng tốc ứng dụng:

val itemsMap: HashMap<String, Item> = hashMapOf()
// Thêm mục vào map
itemsMap["itemKey"] = item

// Lấy mục bằng cách sử dụng khóa của chúng
val retrievedItem = itemsMap["itemKey"]

Tránh rò rỉ bộ nhớ bằng cách quản lý tài nguyên cẩn thận

Rò rỉ bộ nhớ xảy ra khi bạn giữ các tham chiếu đến các đối tượng không còn cần thiết. Điều này có thể dẫn đến việc sử dụng bộ nhớ tăng lên và cuối cùng gây ra sự cố ứng dụng. Bạn có thể sử dụng hàm use để tự động đóng các tài nguyên như luồng và tránh rò rỉ bộ nhớ.

// Sử dụng 'use' để tự động đóng tài nguyên.
FileInputStream("file.txt").use { inputStream ->
    // Xử lý tệp.
}

Cân nhắc sử dụng mã gốc (NDK) cho các tác vụ yêu cầu hiệu suất cao

Đối với các tác vụ tính toán cường độ cao, bạn có thể sử dụng mã gốc thông qua Android NDK. Kotlin/Native cho phép bạn viết mã nền tảng cụ thể có thể được gọi từ mã Kotlin. Tuy nhiên, hãy lưu ý rằng sử dụng mã gốc sẽ làm tăng độ phức tạp và chỉ nên được xem xét khi cần thiết.

// Sử dụng gói 'kotlinx.cinterop' để tương tác với thư viện C.
import kotlinx.cinterop.*
import platform.posix.*

fun readNativeFile() {
    val file = fopen("file.txt", "r") ?: throw Error("Không thể mở tệp")
    try {
        // Đọc tệp bằng cách sử dụng các hàm C gốc.
    } finally {
        fclose(file)
    }
}

UI/UX

Tối ưu hóa bố cục với Layout Inspector và Hierarchy Viewer của Android Studio

Các công cụ này giúp bạn phân tích và tối ưu hóa bố cục của ứng dụng. Bằng cách đơn giản hóa hệ thống phân cấp của các view, bạn có thể giảm thiểu overdraw và cải thiện hiệu suất kết xuất. Tuy nhiên, điều này có thể tốn thời gian.

Tối ưu hóa bố cục với Layout Inspector và Hierarchy Viewer của Android Studio

Giảm thiểu overdraw bằng cách đơn giản hóa hệ thống phân cấp view và sử dụng màu nền

Overdraw xảy ra khi nhiều lớp được vẽ chồng lên nhau, gây ra công việc kết xuất không cần thiết. Bạn có thể giảm thiểu overdraw bằng cách làm phẳng bố cục của mình và chỉ đặt màu nền khi cần thiết.

// Thực hành tốt: Sử dụng một nền duy nhất cho bố cục cha
<LinearLayout
    android:background="@color/background"
    ...>
    <TextView .../>
    <TextView .../>
</LinearLayout>

// Thực hành xấu: Nhiều lớp nền gây ra overdraw
<LinearLayout ...>
    <TextView
        android:background="@color/background"
        .../>
    <TextView
        android:background="@color/background"
        .../>
</LinearLayout>

Sử dụng AndroidX

API RenderScript đã bị ngừng hỗ trợ trong Android 12, và nên chuyển sang thư viện AndroidX để thực hiện các tính toán được tăng tốc phần cứng. Thư viện androidx.core:graphics cung cấp các lớp và phương pháp để thực hiện các tác vụ tính toán cường độ cao như xử lý hình ảnh.

Tối ưu hóa tài nguyên hình ảnh bằng cách sử dụng định dạng và độ phân giải phù hợp

Chọn định dạng hình ảnh phù hợp (ví dụ: WebP, JPEG, hoặc PNG) và độ phân giải cho ứng dụng của bạn để giảm sử dụng bộ nhớ và thời gian tải.

Sử dụng nội dung giữ chỗ hoặc skeleton loading để tăng hiệu suất cảm nhận

Trong khi ứng dụng của bạn lấy dữ liệu hoặc tải tài nguyên, hãy sử dụng nội dung giữ chỗ hoặc skeleton loading để cung cấp phản hồi trực quan cho người dùng, giúp ứng dụng cảm thấy phản hồi nhanh hơn.

val imageView: ImageView = findViewById(R.id.my_image_view)
Glide.with(this)
    .load(imageUrl)
    .placeholder(R.drawable.placeholder_image)
    .into(imageView)

Sử dụng mạng

Sử dụng bộ nhớ cache và lưu trữ cục bộ để giảm các yêu cầu mạng

Bộ nhớ cache dữ liệu cục bộ có thể giúp giảm các yêu cầu mạng và cải thiện hiệu suất của ứng dụng. Ví dụ, bạn có thể sử dụng SharedPreferences hoặc thư viện Room của Android để lưu trữ tạm dữ liệu.

// Lưu dữ liệu vào SharedPreferences
val sharedPreferences = getSharedPreferences("MyAppCache", Context.MODE_PRIVATE)
val editor = sharedPreferences.edit()
editor.putString("key", "value")
editor.apply()

// Lấy dữ liệu từ SharedPreferences
val cachedValue = sharedPreferences.getString("key", null)

Sử dụng tác vụ nền và dịch vụ cho các hoạt động mạng

Thực hiện các hoạt động mạng trong nền có thể giúp giữ cho ứng dụng của bạn phản hồi tốt. Ví dụ, bạn có thể sử dụng Kotlin coroutines để thực hiện các tác vụ bất đồng bộ:

import kotlinx.coroutines.*

fun fetchData() {
    CoroutineScope(Dispatchers.IO).launch {
        val data = apiCall() // Thực hiện yêu cầu mạng
        withContext(Dispatchers.Main) {
            updateUI(data) // Cập nhật giao diện người dùng với dữ liệu đã lấy được
        }
    }
}

Sử dụng kỹ thuật nén dữ liệu để giảm kích thước payload

Nén dữ liệu trước khi gửi qua mạng có thể giúp giảm kích thước payload và cải thiện hiệu suất ứng dụng.

// Yêu cầu dữ liệu nén bằng OkHttp
val client = OkHttpClient.Builder()
    .addInterceptor {
        val request = it.request().newBuilder()
            .header("Accept-Encoding", "gzip")
            .build()
        it.pro

Giám sát kết nối mạng và điều chỉnh hành vi ứng dụng tương ứng

Việc điều chỉnh hành vi của ứng dụng dựa trên kết nối mạng có thể cải thiện trải nghiệm người dùng. Bạn có thể sử dụng ConnectivityManager để phát hiện các thay đổi về mạng.

Ví dụ:

val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val networkCallback = object : ConnectivityManager.NetworkCallback() {
    override fun onAvailable(network: Network) {
        // Mạng có sẵn; cập nhật hành vi của ứng dụng tương ứng
    }

    override fun onLost(network: Network) {
        // Mạng bị mất; cập nhật hành vi của ứng dụng tương ứng
    }
}
connectivityManager.registerDefaultNetworkCallback(networkCallback)

Sử dụng Firebase Cloud Messaging (FCM) cho thông báo đẩy hiệu quả

Tận dụng FCM để gửi thông báo đẩy với tác động tối thiểu đến pin và mạng.

Ví dụ:

class MyFirebaseMessagingService : FirebaseMessagingService() {
    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        // Xử lý thông báo đẩy nhận được
    }
}

Đa luồng

Sử dụng hiệu quả nhiều luồng có thể giúp bạn thực hiện các tác vụ đồng thời, dẫn đến một ứng dụng mượt mà và phản hồi tốt hơn. Dưới đây là một số kỹ thuật cần thiết:

Sử dụng Kotlin Coroutines cho các tác vụ bất đồng bộ

Kotlin coroutines cho phép bạn viết mã bất đồng bộ theo cách dễ đọc và hiệu quả hơn.

Ví dụ:

import kotlinx.coroutines.*

fun fetchData() {
    CoroutineScope(Dispatchers.IO).launch {
        val data = apiCall() // Thực hiện yêu cầu mạng
        withContext(Dispatchers.Main) {
            updateUI(data) // Cập nhật giao diện với dữ liệu đã lấy
        }
    }
}

Sử dụng ThreadPoolExecutor cho các tác vụ đồng thời

ThreadPoolExecutor có thể giúp bạn quản lý một nhóm các luồng làm việc để thực hiện các tác vụ đồng thời. Nó cải thiện hiệu suất bằng cách song song hóa các tác vụ độc lập.

Ví dụ:

import java.util.concurrent.Executors

val threadPoolExecutor = Executors.newFixedThreadPool(4)

fun performTask(task: Runnable) {
    threadPoolExecutor.execute(task)
}

Sử dụng WorkManager cho các tác vụ có thể hoãn và được đảm bảo

WorkManager là một phần của Android Jetpack và cung cấp một cách đơn giản để lập lịch các tác vụ nền. Tuy nhiên, nó không phù hợp cho các tác vụ cần được thực hiện ngay lập tức.

Ví dụ:

import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters

class MyWorker(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) {
    override fun doWork(): Result {
        // Thực hiện tác vụ nền
        return Result.success()
    }
}

val myWorkRequest = OneTimeWorkRequest.Builder(MyWorker::class.java).build()
WorkManager.getInstance(this).enqueue(myWorkRequest)

Tối ưu hóa lưu trữ

Việc lưu trữ dữ liệu hiệu quả là một phần quan trọng trong việc tối ưu hóa hiệu suất ứng dụng Android. Dưới đây là một số cách để tối ưu hóa lưu trữ trong ứng dụng của bạn.

Sử dụng các tùy chọn lưu trữ phù hợp

1. Shared Preferences: Sử dụng Shared Preferences để lưu trữ một lượng nhỏ dữ liệu dưới dạng cặp key-value. Đây là lựa chọn hoàn hảo cho việc lưu trữ các thiết lập cấu hình đơn giản hoặc sở thích của người dùng.

Ví dụ:

val sharedPreferences = getSharedPreferences("MyAppPrefs", Context.MODE_PRIVATE)
val editor = sharedPreferences.edit()
editor.putString("username", "JohnDoe")
editor.apply()
  • SQLite: SQLite là một cơ sở dữ liệu quan hệ nhẹ, cho phép bạn lưu trữ dữ liệu có cấu trúc. Nó hữu ích cho các ứng dụng cần quản lý một lượng dữ liệu vừa phải với các mối quan hệ phức tạp.

Ví dụ:

val dbHelper = object : SQLiteOpenHelper(context, "my_database.db", null, 1) {
    override fun onCreate(db: SQLiteDatabase) {
        db.execSQL("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
    }

    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {}
}

val db = dbHelper.writableDatabase
val contentValues = ContentValues().apply {
    put("id", 1)
    put("name", "John Doe")
}
db.insert("users", null, contentValues)
  • Room: Room là một lớp trừu tượng cao hơn trên SQLite, cung cấp một cách an toàn và mạnh mẽ hơn để quản lý lưu trữ dữ liệu có cấu trúc. Room đơn giản hóa các hoạt động cơ sở dữ liệu và tích hợp với các thành phần khác của Android như LiveData.

Ví dụ:

val db = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "my-db").build()
val userDao = db.userDao()

Tối ưu hóa truy vấn và giao dịch cơ sở dữ liệu

Việc tối ưu hóa các truy vấn và giao dịch cơ sở dữ liệu là rất quan trọng để giảm thời gian mà ứng dụng của bạn dành để truy cập cơ sở dữ liệu, từ đó cải thiện hiệu suất.

Một số mẹo để tối ưu hóa các hoạt động cơ sở dữ liệu bao gồm:

  • Sử dụng chỉ mục trên các cột thường xuyên được truy vấn để tăng tốc độ thực hiện truy vấn.
  • Giới hạn số lượng hàng trả về bởi một truy vấn bằng cách sử dụng từ khóa LIMIT.
  • Viết giao dịch để nhóm nhiều thao tác chèn, cập nhật hoặc xóa lại với nhau, giảm bớt chi phí của các thao tác riêng lẻ.
  • Sử dụng truy vấn có tham số để ngăn chặn các cuộc tấn công SQL injection và cải thiện hiệu suất.

Ví dụ sử dụng LIMIT và truy vấn có tham số với Room:

@Dao
interface UserDao {
    @Query("SELECT * FROM user WHERE age > :minAge LIMIT 10")
    fun getUsersAboveAge(minAge: Int): List<User>
}

Thực hiện các hoạt động lưu trữ tốn kém một cách bất đồng bộ

Chạy các hoạt động lưu trữ một cách bất đồng bộ giúp ngăn chặn việc chặn luồng chính, đảm bảo trải nghiệm người dùng mượt mà. Bạn có thể sử dụng Kotlin Coroutines, AsyncTask hoặc các kỹ thuật đa luồng khác để đạt được điều này.

Sử dụng thư viện Android Jetpack

Android Jetpack là một bộ sưu tập các thư viện nhằm đơn giản hóa và tinh giản phát triển UI. Chúng được thiết kế để cải thiện hiệu suất ứng dụng, khả năng duy trì và giảm thiểu mã lệnh rườm rà.

Các thành phần kiến trúc: ViewModel, LiveData và Data Binding

1. ViewModel: Là một lớp giúp quản lý và lưu trữ dữ liệu liên quan đến UI một cách nhạy bén với vòng đời. Nó cho phép dữ liệu tồn tại qua các thay đổi cấu hình và giữ dữ liệu UI tách biệt với Activity hoặc Fragment.

2. LiveData: Là một lớp lưu trữ dữ liệu có thể quan sát, nhạy bén với vòng đời. Nó đảm bảo rằng các cập nhật chỉ được gửi đến các observer đang hoạt động, chẳng hạn như Activity hoặc Fragment đang trong trạng thái khởi động hoặc tiếp tục.

Ví dụ với LiveData (kết hợp với ViewModel):

class MainActivity : AppCompatActivity() {
    private lateinit var userViewModel: UserViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        userViewModel = ViewModelProvider(this).get(UserViewModel::class.java)
        userViewModel.userName.observe(this, { name ->
            // Cập nhật UI khi giá trị userName thay đổi
            textView.text = name
        })
    }
}

3. Data Binding: Là một thư viện cho phép bạn liên kết các thành phần UI trực tiếp với các nguồn dữ liệu trong các bố cục XML của bạn, giảm bớt nhu cầu mã lệnh rườm rà trong các Activity hoặc Fragment của bạn.

WorkManager cho các tác vụ nền hiệu quả

WorkManager là một thư viện quản lý và lập lịch các tác vụ bất đồng bộ có thể hoãn lại, phải chạy ngay cả khi ứng dụng thoát hoặc thiết bị khởi động lại.

Nó xử lý các vấn đề tương thích và cung cấp một API đơn giản cho việc thực hiện công việc nền. Tuy nhiên, điều này có thể tạo ra một số chi phí trong một số trường hợp.

Android KTX cho các mở rộng Kotlin đơn giản hóa mã

KTX là một tập hợp các mở rộng Kotlin nhằm giúp phát triển Android bằng Kotlin ngắn gọn và mang tính biểu cảm hơn. Nó cung cấp các hàm và thuộc tính mở rộng cho các tác vụ thông thường, đơn giản hóa mã lệnh của bạn.

ConstraintLayout cho thiết kế UI hiệu quả hơn

ConstraintLayout là một trình quản lý bố cục linh hoạt cho phép bạn tạo ra các UI phức tạp với cấu trúc cây nhìn phẳng, cải thiện hiệu suất.

Nó cũng cung cấp một trình chỉnh sửa thiết kế mạnh mẽ trong Android Studio, giúp dễ dàng tạo và sửa đổi các bố cục một cách trực quan.

Kết luận

Bằng cách thực hiện những chiến lược này, bạn đang trên đường xây dựng một ứng dụng Android hiệu suất cao mà người dùng sẽ yêu thích.

Trong phần 2 sắp tới của loạt bài này, chúng ta sẽ khám phá các kỹ thuật tối ưu hóa còn lại, chẳng hạn như tối ưu hóa việc sử dụng pin, đảm bảo tính tương thích trên nhiều thiết bị và giữ cho kích thước ứng dụng nhỏ gọn, cùng nhiều vấn đề khác.