Giảm bộ nhớ

Sau khi bạn hiểu rõ về hoạt động quản lý bộ nhớ trên Android và đã thiết lập các công cụ để đo lường mức sử dụng bộ nhớ của trò chơi, bước tiếp theo là chủ động giảm và tối ưu hoá bộ nhớ. Việc tuân thủ các giới hạn nghiêm ngặt của Android giúp ngăn hệ thống đóng trò chơi, giảm thời gian khởi động dài và đảm bảo trò chơi của bạn chạy tốt trên mọi thiết bị.

Hướng dẫn này cung cấp các kỹ thuật thiết thực để giảm mức sử dụng bộ nhớ của trò chơi, đặc biệt tập trung vào việc tối ưu hoá cấp tài sản, các cấu hình dành riêng cho công cụ và các phương pháp hay nhất về quản lý bộ nhớ.

Giảm bộ nhớ trong Unity

Do thiết kế kiến trúc của Unity, sau khi các trình phân bổ khối gốc và heap được quản lý mở rộng, công cụ này có xu hướng giữ lại các trang bộ nhớ đó để sử dụng lại thay vì trả lại ngay cho hệ điều hành (OS), ngay cả sau khi các tài sản được phát hành. Cụ thể, không gian địa chỉ ảo (bộ nhớ dự phòng) vẫn được dự phòng trong suốt thời gian tồn tại của quy trình và bộ nhớ thực (RSS) không được thu hồi ngay lập tức cho đến khi một số chu kỳ thu gom rác (GC) và cắt bớt đã xảy ra. Do đó, mức sử dụng bộ nhớ tạm thời có thể khiến bộ nhớ thường trú tăng lên trong một thời gian dài ngay cả sau khi mức sử dụng thực tế giảm xuống. Hành vi này làm tăng nguy cơ xảy ra sự cố hết bộ nhớ (OOM) trên các thiết bị cấp thấp và làm giảm độ ổn định tổng thể của thời gian chạy.

Do đó, bạn phải tiếp cận việc tối ưu hoá bộ nhớ Unity thông qua 3 trụ cột cốt lõi phù hợp với những hành vi của công cụ này:

  • Kiểm soát mức sử dụng bộ nhớ đang hoạt động để ngăn chặn tình trạng tăng đột biến bộ nhớ ngay từ đầu.
  • Quản lý các định dạng hoạ tiết và biến thể chương trình đổ bóng để đảm bảo các tài sản và tài nguyên gốc không cần thiết không được khởi tạo.
  • Tái cấu trúc các cấu trúc mã thời gian chạy để loại bỏ các trường hợp phân bổ không cần thiết trên vùng nhớ heap được quản lý nhằm giảm thiểu tần suất GC và việc mở rộng vùng nhớ heap.

Để biết thêm thông tin, hãy xem bài viết Tối ưu hoá bộ nhớ trong Unity.

Giảm mức sử dụng bộ nhớ trong Unreal Engine

Trong Unreal Engine, các quy trình kết xuất có độ trung thực cao và các biểu đồ phần phụ thuộc đối tượng phức tạp có thể làm tăng đáng kể cả RSS ẩn danh và áp lực bộ nhớ dựa trên tệp. Cụ thể, việc dựa vào các tham chiếu cứng và hệ thống phân cấp kế thừa Blueprint sâu sẽ buộc các thành phần được kết nối không dùng đến phải được tải vào bộ nhớ. Hơn nữa, các hoán vị shader quá mức, các nhóm truyền trực tuyến hoạ tiết chưa được tối ưu hoá và các bảng di dời ELF chưa nén góp phần làm tăng mức sử dụng bộ nhớ cơ bản, làm tăng nguy cơ bị low memory killer (LMK) chấm dứt.

Do đó, bạn phải tiếp cận việc tối ưu hoá bộ nhớ của Unreal Engine thông qua 3 trụ cột cốt lõi phù hợp với những hành vi của công cụ này:

  • Tách dữ liệu và logic, thay thế các tham chiếu cứng hoặc tham chiếu mạnh bằng các tham chiếu mềm hoặc tham chiếu yếu.
  • Loại bỏ các tính năng chiếu sáng không dùng đến trên thiết bị di động và các lựa chọn hoán vị để giảm thiểu các đối tượng trạng thái quy trình (PSO) và mục tiêu kết xuất dư thừa, đồng thời áp dụng chế độ nén ASTC và tuỳ chỉnh các nhóm truyền phát kết cấu bằng hồ sơ thiết bị.
  • Bật tính năng Nén bảng di dời RELR và APS để giảm kích thước tệp nhị phân ELF và giảm mức sử dụng bộ nhớ vật lý trong thời gian chạy.

Để biết thêm thông tin, hãy xem bài viết Tối ưu hoá bộ nhớ Unreal.

Tối ưu hoá nhiều quy trình

Mức sử dụng bộ nhớ của các quy trình được lưu vào bộ nhớ đệm sẽ không được tính vào giới hạn bộ nhớ vì mức sử dụng này không ảnh hưởng đến các ứng dụng đang hoạt động. Việc chạy dịch vụ trong một quy trình riêng biệt giúp quy trình chính chuyển sang trạng thái được lưu vào bộ nhớ đệm nhanh nhất có thể, nhờ đó cải thiện tình trạng của trò chơi.

Để biết thêm thông tin, hãy xem bài viết Cách theo dõi trạng thái và bộ nhớ của quy trình, Cách tách biệt quy trình dịch vụ bằng UnityCách tách biệt quy trình dịch vụ bằng Unreal.

Giảm mức sử dụng bộ nhớ trong các dịch vụ mà người dùng nhận thấy

Trò chơi của bạn có thể cần chạy logic trong một dịch vụ mà người dùng cảm nhận được cho các trường hợp sử dụng như hoàn tất quá trình tải xuống một tệp lớn hoặc cho các hệ thống trò chuyện thoại ở chế độ nền. Những chiến lược này có thể giúp bạn quản lý và giảm mức sử dụng bộ nhớ trong các trường hợp này.

Chiến lược tải tệp lớn xuống

Những chiến lược này có thể áp dụng cho các tệp tải xuống có kích thước lớn mà bạn muốn tiếp tục ngay cả sau khi người dùng đã thu nhỏ trò chơi.

1. Cô lập quy trình tải xuống

Nội dung: Đảm bảo rằng hệ điều hành có thể ngay lập tức thu hồi bộ nhớ mà ứng dụng của bạn không sử dụng bằng cách thực hiện quy trình tải xuống trong một quy trình riêng biệt, vì có thể bộ phân bổ bộ nhớ sẽ giữ lại các trang nhóm bộ nhớ và giữ cho bộ nhớ RSS ẩn danh ở mức cao giả tạo ngay cả sau khi giải phóng các mảng. Khi bạn chấm dứt hoặc thoát dịch vụ và xử lý một cách rõ ràng, bộ nhớ sẽ được trả về nhóm hệ điều hành và quy trình chính của bạn sẽ không bị ảnh hưởng.

  • Trong Unity: Giảm tải quá trình tải xuống cho một Service Android gốc được khai báo bằng một quy trình như android:process=":downloader" trong một Tệp kê khai tuỳ chỉnh và gọi quy trình đó bằng JNI AndroidJavaClass của Unity. Hãy nhớ kết thúc quy trình khi quá trình tải xuống hoàn tất. Hãy xem bài viết Chạy một dịch vụ có thể nhận biết trong một quy trình riêng biệt bằng Unity để biết hướng dẫn chi tiết hơn.

  • Trong Unreal: Khai báo một Service Android tuỳ chỉnh bằng một quy trình như android:process=":downloader" bằng cách sử dụng Ngôn ngữ trình bổ trợ Unreal và kích hoạt quy trình đó bằng C++ JNI. Hãy nhớ kết thúc quy trình khi quá trình tải xuống hoàn tất. Hãy xem bài viết Chạy một dịch vụ có thể nhận biết trong một quy trình riêng biệt bằng Unreal để biết hướng dẫn chi tiết hơn.

  • Đối với Android gốc: Khai báo một Service trong AndroidManifest bằng một quy trình như android:process=":downloader". Chạy quá trình tải xuống trong quy trình riêng biệt này và gọi Process.killProcess(Process.myPid()) khi quá trình tải xuống hoàn tất.

Lợi ích: Giảm thời lượng bộ nhớ được giữ lại và cho phép quá trình tải xuống tiếp tục trong khi giải phóng bộ nhớ mà quy trình chính lớn hơn sử dụng.

2. Truyền trực tiếp các tệp tải xuống vào ổ đĩa

Nội dung: Truyền trực tiếp dữ liệu từ ổ cắm mạng vào ổ đĩa bằng cách sử dụng một vùng đệm nhỏ, có kích thước cố định và có thể dùng lại thay vì tích luỹ các phản hồi mạng vào một mảng lớn trước khi ghi.

  • Trong Unity: Tránh sử dụng DownloadHandlerBuffer cho các gói tài sản hoặc tệp lớn, vì thao tác này sẽ phân bổ một vùng đệm bộ nhớ gốc tương đương với kích thước tệp (Bộ nhớ RSS ẩn danh). Thay vào đó, hãy dùng DownloadHandlerFile để truyền trực tuyến các byte một cách tự nhiên vào ổ đĩa trên một luồng nền.

  • Trong Unreal: Truyền trực tiếp các khối dữ liệu đến vào một FArchive (tệp lưu trữ được sao lưu bằng Trình quản lý tệp của Unreal) bằng SetResponseBodyReceiveStream() thay vì nối các tải trọng từ một IHttpRequest vào một TArray<uint8>.

  • Đối với Android gốc: Truyền InputStream đến FileOutputStream bằng cách sử dụng bộ đệm được gộp thay vì gọi .readBytes() hoặc .string() trên phản hồi HTTP.

Lợi ích: Giảm mức sử dụng bộ nhớ cao nhất

3. Truyền trực tuyến quá trình giải nén tệp đã tải xuống

Nội dung: Nếu tệp tải xuống của bạn được nén, hãy gói luồng đầu vào mạng của bạn trong một trình giải nén truyền phát trực tiếp như ZipInputStream thay vì tải tệp xuống, tải tệp vào RAM rồi trích xuất tệp.

Lợi ích: Giảm mức sử dụng bộ nhớ cao nhất

4. Uỷ quyền cho hệ điều hành

Mục đích: Để tránh quản lý bộ nhớ nền hoàn toàn, hãy uỷ quyền cho các API gốc của Android.

  • WorkManager là trình bao bọc hiện đại, được đề xuất xung quanh JobScheduler ở cấp hệ điều hành. Đối với Android 14 trở lên, WorkManager sẽ tự động xử lý các lượt tải xuống do người dùng kích hoạt dưới dạng một lệnh chuyển dữ liệu do người dùng yêu cầu (UIDT). Thao tác này chạy trong quy trình của ứng dụng, vì vậy, bạn vẫn phải truyền trực tiếp đến ổ đĩa để giảm thiểu mức sử dụng bộ nhớ. UIDT bảo vệ ứng dụng của bạn khỏi sự cố do thiếu bộ nhớ bằng cách cho phép hệ điều hành tạm dừng và tiếp tục quá trình tải xuống một cách suôn sẻ nếu tài nguyên hệ thống bị hạn chế.

  • DownloadManager chạy trong một quy trình hệ thống riêng biệt và không phân bổ mức sử dụng bộ nhớ cho quá trình tải xuống vào ứng dụng của bạn. Ứng dụng của bạn sẽ nhận được thông báo truyền tin khi tệp được tải xuống và sẵn sàng.

Lợi ích: WorkManager giúp xử lý các trường hợp bộ nhớ thấp và DownloadManager giúp giảm mức sử dụng bộ nhớ của ứng dụng.

Chiến lược cho các dịch vụ phụ trợ

Những chiến lược này có thể áp dụng cho các dịch vụ phụ mà một trò chơi chạy song song với quy trình chính của trò chơi, chẳng hạn như trò chuyện bằng giọng nói ở chế độ nền.

1. Cô lập quy trình

Việc cần làm: Tách rời tính năng này (ví dụ: giải pháp trò chuyện bằng giọng nói) khỏi công cụ phát triển trò chơi chính. Ví dụ: bạn có thể chạy tính năng ghi âm bằng micrô và truyền phát trực tuyến qua mạng trong một dịch vụ nền trước của Android được chỉ định cho một quy trình riêng biệt (được khai báo trong Tệp kê khai, chẳng hạn như bằng android:process=":voice").

Lợi ích: Khi ứng dụng được thu nhỏ, quy trình chính của trò chơi có thể chuyển sang trạng thái được lưu vào bộ nhớ đệm có mức độ ưu tiên thấp hơn trong khi dịch vụ phụ có trọng lượng nhẹ hơn tiếp tục ở trạng thái dịch vụ mà người dùng nhận thấy.

2. Cắt bớt bộ nhớ không dùng đến trong quy trình

Nội dung: Nếu dịch vụ phụ trợ được tích hợp quá sâu vào công cụ trò chơi để tách biệt, hãy cố gắng giảm tải càng nhiều càng tốt cho quy trình ngay khi trò chơi được chuyển xuống nền/thu nhỏ. Hãy cân nhắc việc xả dữ liệu bộ nhớ đệm hoạ tiết, huỷ tải các cảnh không cần thiết, giảm tốc độ kim đánh dấu nhịp độ khung hình và tốc độ hiển thị của công cụ xuống 0, đồng thời gọi rõ ràng quy trình thu gom rác.

Lợi ích: Giảm mức sử dụng bộ nhớ không cần thiết khi trò chơi không ở nền trước.