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

Sử dụng bộ sưu tập để sắp xếp ngăn nắp các trang Lưu và phân loại nội dung dựa trên lựa chọn ưu tiên của bạn.

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.

Trang này giải thích cách bạn có thể chủ động giảm mức sử dụng bộ nhớ trong ứng dụng của mình. Để 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ề tính năng Quản lý bộ nhớ trong Android.

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

Để có thể khắc phục các sự cố về mức sử dụng bộ nhớ trong ứng dụng, trước tiên, bạn cần tìm thấy các vấn đề đó. 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:

  1. 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.
  2. 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.
  3. Ghi lại quy trình phân bổ bộ nhớ của ứng dụng, sau đó 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ổ và 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

Như đã mô tả trong phần Tổng quan về Quản lý bộ nhớ trên Android, Android có thể thu hồi bộ nhớ từ ứng dụng của bạn theo nhiều cách hoặc loại bỏ hoàn toàn ứng dụng của bạn nếu cần thiết để giải phóng bộ nhớ cho các tác vụ quan trọng. Để 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 loại bỏ các quy 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. Nhờ được cung cấp phương pháp gọi lại onTrimMemory(), ứng dụng của bạn có thể theo dõi các sự kiện liên quan đến bộ nhớ khi ứng dụng này đang chạy ở nền trước hoặc trong nền, sau đó 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ớ.

Ví dụ: bạn có thể triển khai lệnh gọi lại onTrimMemory() để phản hồi các sự kiện liên quan đến bộ nhớ như 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 was raised.
     */
    override fun onTrimMemory(level: Int) {

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

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

                   The user interface has moved to the background.
                */
            }

            ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
                /*
                   Release any memory that 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 will
                   begin killing 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 will be one of
                   the first to be terminated.
                */
            }

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

                  The app received 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 was raised.
     */
    public void onTrimMemory(int level) {

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

            case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:

                /*
                   Release any UI objects that currently hold memory.

                   The user interface has moved 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 that 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 will
                   begin killing 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 will be one of
                   the first to be terminated.
                */

                break;

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

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

Lệnh gọi lại onTrimMemory() đã được thêm vào Android 4.0 (API cấp 14). Đối với các phiên bản cũ, bạn có thể dùng lệnh onLowMemory(), tương đương với sự kiện TRIM_MEMORY_COMPLETE.

Kiểm tra mức bộ nhớ bạn nên sử dụng

Để cho phép nhiều quy 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 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 loại bỏ các quy trình. Đối tượng ActivityManager.MemoryInfo cũng hiển thị một boolean đơn giản là lowMemory, cho biết liệu thiết bị sắp hết bộ nhớ hay chưa.

Đoạn mã sau đây cho thấy ví dụ về cách bạn có thể sử dụng phương thức getMemoryInfo() trong ứng dụng của mình.

Kotlin

fun doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check to see 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 to see 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;
}

Xây dựng một 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ã có xu hướng 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ã của mình.

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

Để một dịch vụ chạy khi không cần thiết là một trong những sai lầm nghiêm trọng nhất khi quản lý bộ nhớ 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ụ để thực hiện tác vụ trong nền, đừng tiếp tục chạy ứng dụng đó trừ khi nó cần thực hiện một tác vụ. Hãy nhớ dừng dịch vụ khi nó đã hoàn thành tác vụ. Nếu không, bạn có thể sơ ý 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 luôn duy trì cho quy trình đó hoạt động Hành vi này khiến các quy trình dịch vụ trở nên rất tốn kém vì RAM dịch vụ sử dụng vẫn không dùng được cho các quy trình khác. Điều này làm giảm số lượng các quy 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ì đủ quy trình để lưu trữ tất cả dịch vụ hiện đang chạy.

Nhìn chung, bạn nên tránh sử dụng các dịch vụ có tính liên tục do những dịch vụ này luôn phải 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ư JobScheduler. Để biết thêm thông tin chi tiết về cách sử dụng JobScheduler nhằm lên lịch cho các quy trình trong nền, vui lòng xem bài viết Tối ưu hoá ở chế độ nền.

Nếu bạn cần sử dụng một dịch vụ, cách tốt nhất để giới hạn thời gian sử dụng dịch vụ là dùng IntentService. Giá trị này sẽ tự kết thúc ngay sau khi xử lý xong ý định bắt đầu giá trị. Để biết thêm thông tin chi tiết, vui lòng đọc phần Chạy trong một dịch vụ nền.

Sử 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 á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 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à 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 thực sự tinh gọn.

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

Các nhà phát triển thường đơn thuần sử dụng các thành phần trừu tượng như một cách thức lập trình tốt, vì các thành phần trừu tượng giúp 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 cũng khá tốn kém: chúng thường yêu cầu một lượng lớn mã cần được thực thi hơn, đòi hỏi nhiều thời gian và nhiều RAM hơn để mã đó được liên kết vào bộ nhớ. Do đó, nếu chế độ trừu tượng không mang lại lợi ích đáng kể, thì bạn nên tránh sử dụng.

Sử dụng giao thức lite protobuf cho dữ liệu được tuần tự hóa

Vùng đệm giao thức là một cơ chế không phân biệt ngôn ngữ và nền tảng, 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 quyết định sử dụng protobuf cho dữ liệu của mình, bạn phải luôn sử dụng giao thức lite protobuf cho các mã này ở 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 APK và thực thi chậm hơn.

Để biết thêm thông tin chi tiết, vui lòng xem phần "Phiên bản thu gọn" trong phần protobuf readme.

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

Như đã đề cập trước đó, 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 phải 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 cạn kiệt nhanh hơn.

Thông thường, việc 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. Trong 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.

Tất nhiên là bạn cần phải tìm những vị trí trong mã nơi có tỷ lệ nhồi nhét bộ nhớ cao thì mới có thể khắc phục vấn đề. Để làm được việc đó, bạn nên sử dụng Trình phân tích bộ nhớ trong Android Studio.

Khi bạn đã xác định được các vị trí xảy ra sự cố trong mã của mình, hãy cố giảm số lượng đối tượng phân bổ ở những khu vực 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 (Nhà máy).

Có một khả năng khác là đá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. Khi cần dùng thực thể đối tượng thuộc loại đó cho lần tiếp theo, bạn có thể lấy lại trong nhóm thay vì phân bổ lại thực thể đối tượng đó.

Việc đánh giá hiệu suất kỹ lưỡng là điều cần thiết để xác định xem liệu một nhóm đối tượng có phù hợp cho 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 lấy lại có thể có chi phí khác 0. Sau cùng, việc giữ lại nhiều thực thể đối tượng trong nhóm hơn so với mong muốn cũng sẽ gây gánh nặng cho GC. Tuy làm giảm số lượng lệnh gọi GC, 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á các 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ã 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ã của bạn.

Giảm kích thước APK tổng thể

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 sẽ cung cấp nhiều công cụ giúp bạn 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. (Android Studio phiên bản 3.3 trở xuống sử dụng công cụ ProGuard thay vì 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 hướng dẫn về cách giảm kích thước ứng dụng.

Sử dụng 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 có ý định sử dụng một khung chèn phần phụ thuộc trong ứng dụng của mình, vui lòng cân nhắc sử dụng Dagger 2. Dagger không sử dụng chức năng phản chiếu để quét mã của ứng dụng. Phương thức triển khai tĩnh và tại thời gian biên dịch của Dagger có nghĩa là nó có thể dùng được trong các ứng dụng Android mà không cần tốn thời gian chạy hoặc sử 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 có xu hướng khởi chạy các quy trình bằng cách quét mã để tìm chú thích. Quy trình này có thể yêu cầu nhiều chu kỳ và RAM hơn trong CPU, và có thể gây ra độ trễ đáng kể khi ứng dụng khởi chạy.

Vui lòng cẩn thận khi sử dụng các thư viện bên ngoài

Mã 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 quyết định sử dụng một thư viện bên ngoài, bạn có thể cần tối ưu hoá thư viện đó cho thiết bị di động. Hãy lên kế hoạch trước cho vấn đề đó và phân tích thư viện về kích thước mã cũng như dung lượng RAM trước khi quyết định sử dụng.

Ngay cả một số thư viện được tối ưu hóa 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 lite protobuf trong khi một thư viện khác sử dụng các giao thức protobuf vi mô, dẫn đến 2 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ù công cụ 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 mà bạn muốn dùng trong những thư viện này có thể cần có 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 (thường có nhiều phần phụ thuộc), khi các thư viện sử dụng chức năng phản chiếu (điều này phổ biến và có nghĩa là bạn cần phải dành nhiều thời gian để tinh chỉnh công cụ ProGuard theo cách thủ công để nó có thể hoạt động), v.v.

Bạn cũng 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. Bạn sẽ không muốn lấy một lượng lớn mã và chịu chí phí hao tổn mà bạn thậm chí 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.