Quản lý bộ nhớ của ứng dụng

Trang này giải thích cách chủ động giảm mức sử dụng bộ nhớ trong ứng dụng của bạn. Để biết thông tin về cách hệ điều hành Android quản lý bộ nhớ, hãy xem bài viết Tổng quan về việc quản lý bộ nhớ.

Bộ nhớ truy xuất ngẫu nhiên (RAM) là một tài nguyên quan trọng trong mọi môi trường phát triển phần mềm, thậm chí còn quan trọng hơn nữa trên hệ điều hành dành cho thiết bị di động do nơi này thường có bộ nhớ thực bị hạn chế. Mặc dù cả môi trường máy ảo Android Runtime (ART) và Dalvik đều thực hiện quy trình thu gom rác thông thường, nhưng điều đó không có nghĩa là bạn có thể bỏ qua thời điểm và vị trí mà ứng dụng phân bổ cũng như giải phóng bộ nhớ. Bạn vẫn cần tránh gây tình trạng rò rỉ bộ nhớ, thường là do việc giữ lại thông tin tham chiếu đến đối tượng trong các biến thành phần tĩnh và giải phóng bất kỳ đối tượng Reference nào vào các thời điểm thích hợp như được xác định bằng các phương thức gọi lại trong vòng đời.

Giám sát mức sử dụng bộ nhớ và bộ nhớ hiện có

Bạn cần tìm thấy các vấn đề về mức sử dụng bộ nhớ của ứng dụng rồi mới có thể khắc phục. Trình phân tích bộ nhớ trong Android Studio giúp bạn tìm và chẩn đoán các vấn đề về bộ nhớ theo những cách sau:

  • Xem cách ứng dụng của bạn phân bổ bộ nhớ theo thời gian. Trình phân tích bộ nhớ cho thấy biểu đồ theo thời gian thực về mức sử dụng bộ nhớ của ứng dụng, số lượng đối tượng Java được phân bổ và thời điểm thu gom rác.
  • Hãy bắt đầu các sự kiện thu thập rác và chụp nhanh vùng nhớ khối xếp Java trong khi ứng dụng của bạn chạy.
  • Ghi lại quy trình phân bổ bộ nhớ của ứng dụng, kiểm tra tất cả đối tượng được phân bổ, xem dấu vết ngăn xếp của từng lượt phân bổ rồi chuyển đến mã tương ứng trong trình chỉnh sửa Android Studio.

Giải phóng bộ nhớ để phản hồi sự kiện

Android có thể thu hồi bộ nhớ của ứng dụng hoặc dừng ứng dụng hoàn toàn nếu cần để giải phóng bộ nhớ cho các tác vụ quan trọng, như giải thích trong phần Tổng quan về việc quản lý bộ nhớ. Để giúp cân bằng hơn mức bộ nhớ hệ thống và tránh việc hệ thống phải dừng các tiến trình ứng dụng của bạn, bạn có thể triển khai giao diện ComponentCallbacks2 trong các lớp Activity. Phương thức gọi lại onTrimMemory() được cung cấp giúp ứng dụng của bạn theo dõi các sự kiện liên quan đến bộ nhớ khi ứng dụng đang chạy ở chế độ nền trước hoặc trong nền. Sau đó, phương thức này cho phép ứng dụng giải phóng đối tượng theo vòng đời của ứng dụng hoặc sự kiện của hệ thống cho biết rằng hệ thống cần lấy lại bộ nhớ.

Bạn có thể triển khai lệnh gọi lại onTrimMemory() để phản hồi nhiều sự kiện liên quan đến bộ nhớ, như trong ví dụ sau:

Kotlin

import android.content.ComponentCallbacks2
// Other import statements.

class MainActivity : AppCompatActivity(), ComponentCallbacks2 {

    // Other activity code.

    /**
     * Release memory when the UI becomes hidden or when system resources become low.
     * @param level the memory-related event that is raised.
     */
    override fun onTrimMemory(level: Int) {

        // Determine which lifecycle or system event is raised.
        when (level) {

            ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
                /*
                   Release any UI objects that currently hold memory.

                   The user interface moves to the background.
                */
            }

            ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
                /*
                   Release any memory your app doesn't need to run.

                   The device is running low on memory while the app is running.
                   The event raised indicates the severity of the memory-related event.
                   If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system
                   begins stopping background processes.
                */
            }

            ComponentCallbacks2.TRIM_MEMORY_BACKGROUND,
            ComponentCallbacks2.TRIM_MEMORY_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> {
                /*
                   Release as much memory as the process can.

                   The app is on the LRU list and the system is running low on memory.
                   The event raised indicates where the app sits within the LRU list.
                   If the event is TRIM_MEMORY_COMPLETE, the process is one of the
                   first to be terminated.
                */
            }

            else -> {
                /*
                  Release any non-critical data structures.

                  The app receives an unrecognized memory level value
                  from the system. Treat this as a generic low-memory message.
                */
            }
        }
    }
}

Java

import android.content.ComponentCallbacks2;
// Other import statements.

public class MainActivity extends AppCompatActivity
    implements ComponentCallbacks2 {

    // Other activity code.

    /**
     * Release memory when the UI becomes hidden or when system resources become low.
     * @param level the memory-related event that is raised.
     */
    public void onTrimMemory(int level) {

        // Determine which lifecycle or system event is raised.
        switch (level) {

            case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:

                /*
                   Release any UI objects that currently hold memory.

                   The user interface moves to the background.
                */

                break;

            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:

                /*
                   Release any memory your app doesn't need to run.

                   The device is running low on memory while the app is running.
                   The event raised indicates the severity of the memory-related event.
                   If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system
                   begins stopping background processes.
                */

                break;

            case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
            case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
            case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:

                /*
                   Release as much memory as the process can.

                   The app is on the LRU list and the system is running low on memory.
                   The event raised indicates where the app sits within the LRU list.
                   If the event is TRIM_MEMORY_COMPLETE, the process is one of the
                   first to be terminated.
                */

                break;

            default:
                /*
                  Release any non-critical data structures.

                  The app receives an unrecognized memory level value
                  from the system. Treat this as a generic low-memory message.
                */
                break;
        }
    }
}

Kiểm tra dung lượng bộ nhớ bạn cần

Để cho phép nhiều tiến trình chạy, Android đặt giới hạn cố định cho dung lượng vùng nhớ khối xếp được phân bổ cho mỗi ứng dụng. Giới hạn dung lượng vùng nhớ khối xếp chính xác sẽ khác nhau giữa các thiết bị tuỳ theo tổng dung lượng RAM có sẵn. Nếu ứng dụng của bạn đạt đến hạn mức vùng nhớ khối xếp và đang cố gắng phân bổ thêm bộ nhớ, hệ thống sẽ gửi ra một OutOfMemoryError.

Để tránh tình trạng hết bộ nhớ, bạn có thể truy vấn hệ thống để xác định dung lượng vùng nhớ khối xếp hiện có trên thiết bị hiện tại. Bạn có thể truy vấn hệ thống cho minh hoạ này bằng cách gọi getMemoryInfo(). Thao tác này sẽ trả về một đối tượng ActivityManager.MemoryInfo cung cấp thông tin về trạng thái bộ nhớ hiện tại của thiết bị, bao gồm bộ nhớ còn trống, tổng bộ nhớ và ngưỡng bộ nhớ – mức dung lượng mà tại đó hệ thống bắt đầu dừng các tiến trình. Đối tượng ActivityManager.MemoryInfo cũng cho thấy lowMemory (một giá trị boolean đơn giản cho biết liệu thiết bị sắp hết bộ nhớ hay chưa).

Đoạn mã mẫu sau cho biết cách dùng phương thức getMemoryInfo() trong ứng dụng của bạn.

Kotlin

fun doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check whether the device is in a low memory state.
    if (!getAvailableMemory().lowMemory) {
        // Do memory intensive work.
    }
}

// Get a MemoryInfo object for the device's current memory status.
private fun getAvailableMemory(): ActivityManager.MemoryInfo {
    val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
    return ActivityManager.MemoryInfo().also { memoryInfo ->
        activityManager.getMemoryInfo(memoryInfo)
    }
}

Java

public void doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check whether the device is in a low memory state.
    ActivityManager.MemoryInfo memoryInfo = getAvailableMemory();

    if (!memoryInfo.lowMemory) {
        // Do memory intensive work.
    }
}

// Get a MemoryInfo object for the device's current memory status.
private ActivityManager.MemoryInfo getAvailableMemory() {
    ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
    ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
    activityManager.getMemoryInfo(memoryInfo);
    return memoryInfo;
}

Tạo cấu trúc mã tiết kiệm bộ nhớ hơn

Một số tính năng của Android, lớp Java và cấu trúc mã sẽ dùng nhiều bộ nhớ hơn những tính năng khác. Bạn có thể giảm thiểu mức bộ nhớ mà ứng dụng dùng bằng cách chọn các phương án thay thế hiệu quả hơn trong mã nguồn của mình.

Dùng dịch vụ một cách thận trọng

Bạn không nên để dịch vụ tiếp tục hoạt động khi không cần thiết. Việc để các dịch vụ không cần thiết chạy là một trong những lỗi quản lý bộ nhớ nghiêm trọng nhất mà ứng dụng Android có thể mắc phải. Nếu ứng dụng của bạn cần một dịch vụ để hoạt động ở chế độ nền, đừng tiếp tục chạy ứng dụng đó trừ trường hợp ứng dụng đó cần thực hiện tác vụ. Hãy dừng dịch vụ khi hoàn thành tác vụ. Nếu không, bạn có thể gây ra sự cố rò rỉ bộ nhớ.

Khi bạn bắt đầu một dịch vụ, hệ thống ưu tiên giữ cho dịch vụ đó chạy. Hành vi này khiến các tiến trình dịch vụ trở nên rất tốn kém vì RAM mà dịch vụ sử dụng không dùng được cho các tiến trình khác. Điều này làm giảm số lượng các tiến trình lưu vào bộ nhớ đệm mà hệ thống có thể giữ trong bộ nhớ đệm LRU, khiến việc chuyển đổi ứng dụng trở nên kém hiệu quả hơn. Điều này thậm chí có thể dẫn đến tình trạng đơ máy trong hệ thống khi bộ nhớ bị quá tải và hệ thống không thể duy trì đủ tiến trình để lưu trữ tất cả dịch vụ hiện đang chạy.

Nhìn chung, hãy tránh dùng các dịch vụ có tính liên tục do những dịch vụ này yêu cầu sử dụng bộ nhớ hiện có. Thay vào đó, bạn nên sử dụng phương thức triển khai khác, chẳng hạn như WorkManager. Để biết thêm thông tin về cách sử dụng WorkManager nhằm lên lịch cho tiến trình nền, hãy xem bài viết Công việc liên tục.

Dùng vùng chứa dữ liệu được tối ưu hóa

Một số lớp do ngôn ngữ lập trình cung cấp không được tối ưu hoá để sử dụng trên thiết bị di động. Chẳng hạn như cách triển khai HashMap chung có thể không hiệu quả vì bộ nhớ cần có một đối tượng mục riêng cho từng mục ánh xạ liên kết.

Khung Android bao gồm một số vùng chứa dữ liệu được tối ưu hoá, bao gồm cả SparseArray, SparseBooleanArrayLongSparseArray. Ví dụ: các lớp SparseArray hiệu quả hơn vì chúng tránh được việc hệ thống phải tự động đóng hộp khoá và đôi khi là cả giá trị (tạo ra một hoặc hai đối tượng khác cho mỗi mục nhập).

Nếu cần, bạn luôn có thể chuyển sang các mảng thô để có cấu trúc dữ liệu tinh gọn.

Hãy cẩn thận với việc trừu tượng hoá mã

Các nhà phát triển thường sử dụng các thành phần trừu tượng dưới dạng phương thức lập trình hiệu quả vì chúng có thể cải thiện tính linh hoạt và bảo trì mã. Tuy nhiên, các thành phần trừu tượng tốn kém hơn đáng kể vì chúng thường đòi hỏi nhiều mã hơn cần được thực thi, tốn nhiều thời gian và RAM hơn để liên kết mã đó vào bộ nhớ. Nếu các thành phần trừu tượng không mang lại lợi ích đáng kể, hãy tránh sử dụng chúng.

Dùng protobuf lite cho dữ liệu được tuần tự hoá

Vùng đệm giao thức (protobuf) là một cơ chế không phân biệt ngôn ngữ/nền tảng và có thể mở rộng do Google thiết kế để chuyển đổi tuần tự dữ liệu có cấu trúc. Cơ chế này tương tự với XML, nhưng nhỏ hơn, nhanh hơn và đơn giản hơn. Nếu bạn dùng protobuf cho dữ liệu của mình, hãy luôn sử dụng protobuf lite trong mã phía máy khách. Các protobuf thông thường tạo ra mã cực kỳ chi tiết, có thể gây nhiều kiểu sự cố trong ứng dụng của bạn, chẳng hạn như tăng mức sử dụng RAM, tăng đáng kể kích thước tệp APK và thực thi chậm hơn.

Để biết thêm thông tin, hãy xem phần protobuf readme.

Tránh tình trạng nhồi nhét bộ nhớ

Các sự kiện thu gom rác không ảnh hưởng đến hiệu suất của ứng dụng. Tuy nhiên, nhiều sự kiện thu gom rác xảy ra trong một khoảng thời gian ngắn có thể tiêu hao pin nhanh chóng cũng như tăng nhẹ thời gian thiết lập khung do bộ thu gom rác và các luồng ứng dụng cần tương tác với nhau. Hệ thống càng dành nhiều thời gian cho việc thu gom rác thì pin càng tiêu hao nhanh hơn.

Thường thì tình trạng nhồi nhét bộ nhớ có thể gây ra một lượng lớn các sự kiện thu thập rác. Trên thực tế, việc nhồi nhét bộ nhớ mô tả số lượng các đối tượng tạm thời được phân bổ xảy ra trong một khoảng thời gian nhất định.

Ví dụ: bạn có thể phân bổ nhiều đối tượng tạm thời trong một vòng lặp for. Hoặc bạn có thể tạo đối tượng Paint hoặc Bitmap mới bên trong hàm onDraw() của chế độ xem. Trong cả hai trường hợp, ứng dụng sẽ tạo nhanh nhiều đối tượng với khối lượng lớn. Các đối tượng này có thể nhanh chóng sử dụng hết bộ nhớ hiện có trong young generation (nhóm đối tượng được phân bổ gần đây), buộc sự kiện thu gom rác xảy ra.

Hãy dùng Trình phân tích bộ nhớ (Memory Profiler) để tìm những nơi trong mã nguồn của bạn có tình trạng nhồi nhét bộ nhớ cao trước khi bạn có thể khắc phục.

Sau khi xác định được những nơi có vấn đề trong mã nguồn của bạn, hãy cố giảm số lượng đối tượng phân bổ ở những khía cạnh quan trọng về hiệu suất. Bạn nên cân nhắc di chuyển mọi thứ ra khỏi vòng lặp nội bộ hoặc có thể đưa chúng vào cấu trúc phân bổ dựa trên factory.

Bạn cũng có thể đánh giá xem nhóm đối tượng có lợi cho trường hợp sử dụng này hay không. Với một nhóm đối tượng, thay vì bỏ qua một thực thể đối tượng, hãy giải phóng thực thể đối tượng đó vào một nhóm khi không cần dùng nữa. Lần tiếp theo cần dùng thực thể đối tượng thuộc loại đó, bạn có thể lấy thực thể đối tượng đó trong nhóm thay vì phân bổ thực thể đối tượng đó.

Hãy đánh giá kỹ lưỡng hiệu suất để xác định xem một nhóm đối tượng có phù hợp trong một tình huống cụ thể hay không. Có những trường hợp nhóm đối tượng có thể làm giảm hiệu suất. Mặc dù nhóm này giúp tránh tình trạng phân bổ lại, nhưng đồng thời cũng mang lại các hao tổn khác. Ví dụ: việc duy trì nhóm thực thể này thường liên quan đến quá trình đồng bộ hoá với chi phí phát sinh không nhỏ. Ngoài ra, việc xoá thực thể đối tượng gộp (để tránh rò rỉ bộ nhớ) trong quá trình giải phóng, sau đó khởi tạo thực thể trong quy trình thu nạp có thể gây ra chi phí.

Việc giữ lại nhiều thực thể đối tượng trong nhóm hơn mức cần thiết cũng sẽ tạo gánh nặng cho việc thu thập rác. Tuy làm giảm số lượng lệnh gọi thu thập rác, nhưng các nhóm đối tượng lại làm tăng khối lượng công việc cần thực hiện trên mỗi lần gọi vì khối lượng công việc tỷ lệ với số byte của bộ nhớ mà ứng dụng đang dùng (có thể tiếp cận).

Xoá thư viện và tài nguyên tiêu tốn nhiều bộ nhớ

Một số tài nguyên và thư viện trong mã nguồn của bạn có thể chiếm dụng bộ nhớ mà bạn không biết. Kích thước tổng thể của ứng dụng (bao gồm cả thư viện của bên thứ ba hoặc tài nguyên được nhúng) có thể ảnh hưởng đến mức tiêu thụ bộ nhớ của ứng dụng. Bạn có thể cải thiện mức tiêu thụ bộ nhớ của ứng dụng bằng cách xoá mọi thành phần, tài nguyên hoặc thư viện thừa, không cần thiết hoặc chiếm dụng bộ nhớ khỏi mã nguồn của mình.

Giảm kích thước tổng thể của tệp APK

Bạn có thể giảm đáng kể mức sử dụng bộ nhớ của ứng dụng bằng cách giảm kích thước tổng thể của nó. Các kích thước bit, tài nguyên, khung ảnh động và thư viện của bên thứ ba đều chiếm dung lượng ứng dụng của bạn. Android Studio và SDK Android có cung cấp nhiều công cụ giúp giảm kích thước tài nguyên và các phần phụ thuộc bên ngoài. Những công cụ này hỗ trợ các phương thức rút gọn mã hiện đại, chẳng hạn như công cụ biên dịch R8.

Để biết thêm thông tin về cách giảm kích thước tổng thể của ứng dụng, hãy xem bài viết Giảm kích thước ứng dụng.

Dùng Hilt hoặc Dagger 2 để chèn phần phụ thuộc

Khung chèn phần phụ thuộc có thể đơn giản hoá mã bạn viết và cung cấp một môi trường thích ứng hữu ích cho việc kiểm thử cũng như các thay đổi khác về cấu hình.

Nếu bạn định dùng khung chèn phần phụ thuộc trong ứng dụng của mình, hãy cân nhắc dùng Hilt hoặc Dagger. Hilt là thư viện chèn phần phụ thuộc cho Android chạy trên Dagger. Dagger không dùng chức năng phản chiếu để quét mã của ứng dụng. Bạn có thể dùng phương thức triển khai thời gian biên dịch tĩnh của Dagger trong ứng dụng Android mà không cần tốn thời gian chạy hay chiếm dụng bộ nhớ.

Các khung chèn phần phụ thuộc khác sử dụng tính năng phản chiếu sẽ khởi chạy các tiến trình bằng cách quét mã để tìm chú thích. Tiến trình này có thể yêu cầu nhiều chu kỳ và RAM hơn trong CPU, đồng thời có thể gây ra độ trễ đáng kể khi ứng dụng khởi chạy.

Cẩn thận khi dùng thư viện bên ngoài

Mã nguồn thư viện bên ngoài thường không được viết cho môi trường thiết bị di động và có thể không hiệu quả khi được sử dụng cho hoạt động trên ứng dụng khách dành cho thiết bị di động. Khi dùng thư viện bên ngoài, bạn có thể phải tối ưu hoá thư viện đó cho thiết bị di động. Hãy lên kế hoạch trước cho việc này và phân tích thư viện về kích thước mã cũng như dung lượng RAM trước khi sử dụng.

Ngay cả một số thư viện được tối ưu hoá cho thiết bị di động cũng có thể gây ra sự cố do các cách triển khai khác nhau. Ví dụ: một thư viện có thể sử dụng các giao thức protobuf lite trong khi một thư viện khác sử dụng các giao thức protobuf micro, dẫn đến hai cách triển khai protobuf khác nhau trong ứng dụng của bạn. Điều này có thể xảy ra khi bạn triển khai nhiều phương thức ghi nhật ký, phân tích, tải hình ảnh, lưu vào bộ nhớ đệm và nhiều tính năng khác mà bạn không mong đợi.

Mặc dù ProGuard có thể giúp loại bỏ các API và tài nguyên khi có hành động gắn cờ phù hợp, nhưng không thể xoá những phần phụ thuộc lớn bên trong của thư viện. Các tính năng bạn muốn trong các thư viện này có thể yêu cầu các phần phụ thuộc cấp thấp hơn. Điều này đặc biệt khó khăn khi bạn dùng lớp con Activity của một thư viện (có thể có nhiều phần phụ thuộc), khi các thư viện dùng tính năng phản chiếu (điều này phổ biến và đòi hỏi phải tinh chỉnh ProGuard theo cách thủ công để nó có thể hoạt động).

Bạn nên tránh sử dụng thư viện dùng chung chỉ cho một hoặc hai tính năng trong số hàng chục tính năng. Đừng lấy một lượng lớn mã và chịu chí phí hao tổn mà bạn không sử dụng. Khi bạn cân nhắc có nên sử dụng một thư viện hay không, hãy tìm một cách triển khai phù hợp nhất với nhu cầu của bạn. Nếu không, bạn có thể quyết định tạo cách triển khai của riêng mình.