ลดหน่วยความจำ

เมื่อคุณเข้าใจการจัดการหน่วยความจำของ Android และได้ตั้งค่าเครื่องมือเพื่อวัดการใช้หน่วยความจำของเกมแล้ว ขั้นตอนถัดไปคือการลดและเพิ่มประสิทธิภาพการใช้หน่วยความจำอย่างจริงจัง การใช้หน่วยความจำให้อยู่ในขีดจำกัดที่เข้มงวดของ Android จะช่วยป้องกันไม่ให้ระบบปิดเกม หยุดเวลาเริ่มต้นที่นาน และทำให้เกมทำงานได้ดีในทุกอุปกรณ์

คู่มือนี้จะนำเสนอเทคนิคที่ใช้งานได้จริงเพื่อลดการใช้หน่วยความจำที่ใช้ของเกม โดยเน้นที่การเพิ่มประสิทธิภาพระดับชิ้นงาน การกำหนดค่าเฉพาะของเอนจิน และแนวทางปฏิบัติแนะนำในการจัดการหน่วยความจำ

ลดหน่วยความจำใน Unity

เนื่องจากการออกแบบสถาปัตยกรรมของ Unity เมื่อตัวจัดสรรบล็อกเนทีฟและฮีปที่มีการจัดการขยายออก เอนจินมักจะเก็บเพจหน่วยความจำเหล่านั้นไว้เพื่อนำกลับมาใช้ใหม่แทนที่จะส่งคืนไปยังระบบปฏิบัติการ (OS) ทันที แม้หลังจากที่ปล่อยชิ้นงานแล้วก็ตาม โดยเฉพาะอย่างยิ่ง พื้นที่ที่อยู่เสมือน (หน่วยความจำที่สงวนไว้) จะยังคงสงวนไว้ตลอดอายุการใช้งานของกระบวนการ และระบบจะไม่เรียกคืนหน่วยความจำจริง (RSS) ทันทีจนกว่าจะมีการเก็บขยะ (GC) และรอบการลดขนาดหลายรอบ ด้วยเหตุนี้ การใช้หน่วยความจำสูงสุดชั่วคราวอาจทำให้หน่วยความจำภายในยังคงสูงอยู่เป็นเวลานานแม้หลังจากที่การใช้งานจริงลดลงแล้วก็ตาม ลักษณะการทำงานนี้จะเพิ่มความเสี่ยงของการขัดข้องเนื่องจากหน่วยความจำไม่เพียงพอ (OOM) ในอุปกรณ์ระดับล่าง และลดเสถียรภาพโดยรวมของรันไทม์

ดังนั้น การเพิ่มประสิทธิภาพหน่วยความจำของ Unity จึงต้องดำเนินการผ่าน 3 เสาหลักที่ปรับให้เหมาะกับลักษณะการทำงานของเอนจินเหล่านี้

  • ควบคุมการใช้งานหน่วยความจำที่ใช้งานอยู่เพื่อป้องกันไม่ให้หน่วยความจำเพิ่มขึ้นสูงสุดตั้งแต่แรก
  • จัดการรูปแบบเท็กซ์เจอร์และตัวแปรเฉดสีเพื่อให้แน่ใจว่าจะไม่มีการสร้างชิ้นงานและทรัพยากรเนทีฟที่ไม่จำเป็น
  • ปรับโครงสร้างโค้ดรันไทม์ใหม่เพื่อขจัดการจัดสรรที่ไม่จำเป็นในฮีปที่มีการจัดการ เพื่อลดความถี่ของ GC และการขยายฮีป

ดูข้อมูลเพิ่มเติมได้ที่ การเพิ่มประสิทธิภาพหน่วยความจำของ Unity

ลดหน่วยความจำใน Unreal Engine

ใน Unreal Engine ไปป์ไลน์การแสดงผลที่มีรายละเอียดเสมือนจริงสูงและกราฟทรัพยากร Dependency ของออบเจ็กต์ที่ซับซ้อนอาจเพิ่มทั้ง RSS แบบไม่ระบุชื่อและแรงกดดันด้านหน่วยความจำที่สำรองด้วยไฟล์ได้อย่างมาก โดยเฉพาะอย่างยิ่ง การใช้การอ้างอิงแบบฮาร์ดและการสืบทอดแบบ Blueprint ที่ซับซ้อนจะบังคับให้โหลดชิ้นงานที่เชื่อมต่อซึ่งไม่ได้ใช้ลงในหน่วยความจำ นอกจากนี้ การเรียงสับเปลี่ยนตัวปรับแสงเงามากเกินไป พูลการสตรีมพื้นผิวที่ไม่ได้เพิ่มประสิทธิภาพ และตารางการย้ายตำแหน่ง ELF ที่ไม่ได้บีบอัดจะทำให้หน่วยความจำที่ใช้พื้นฐานสูงขึ้น ซึ่งจะเพิ่มความเสี่ยงของการสิ้นสุดการทำงานโดย Low Memory Killer (LMK)

ดังนั้น การเพิ่มประสิทธิภาพหน่วยความจำของ Unreal Engine จึงต้องดำเนินการผ่าน 3 เสาหลักที่ปรับให้เหมาะกับลักษณะการทำงานของเอนจินเหล่านี้

  • แยกข้อมูลและตรรกะออกจากกัน โดยแทนที่การอ้างอิงแบบฮาร์ดหรือแบบแข็งด้วยการอ้างอิงแบบซอฟต์หรือแบบอ่อน
  • นำฟีเจอร์แสงและตัวเลือกการเรียงสับเปลี่ยนสำหรับอุปกรณ์เคลื่อนที่ที่ไม่ได้ใช้ออกเพื่อลดออบเจ็กต์สถานะไปป์ไลน์ (PSO) และเป้าหมายการแสดงผลที่ซ้ำซ้อน ขณะเดียวกันก็ใช้การบีบอัด ASTC และปรับแต่งพูลการสตรีมเท็กซ์เจอร์ด้วยโปรไฟล์อุปกรณ์
  • เปิดใช้การบีบอัดตารางการย้ายตำแหน่ง RELR และ APS เพื่อลดขนาดไบนารี ELF และลดหน่วยความจำที่ใช้ของรันไทม์

ดูข้อมูลเพิ่มเติมได้ที่การเพิ่มประสิทธิภาพหน่วยความจำของ Unreal

การเพิ่มประสิทธิภาพแบบหลายกระบวนการ

ระบบจะไม่รวมการใช้หน่วยความจำของกระบวนการที่แคชไว้ในการคำนวณขีดจำกัดหน่วยความจำ เนื่องจากไม่มีผลต่อแอปที่ใช้งานอยู่ การเรียกใช้บริการในกระบวนการที่แยกต่างหากจะช่วยให้กระบวนการหลักเปลี่ยนไปอยู่ในสถานะแคชได้เร็วที่สุด ซึ่งจะช่วยปรับปรุงประสิทธิภาพของเกม

ดูข้อมูลเพิ่มเติมได้ที่วิธีติดตามสถานะและหน่วยความจำของกระบวนการ, วิธี แยกกระบวนการบริการด้วย Unity และวิธีแยกกระบวนการบริการ ด้วย Unreal

ลดการใช้งานหน่วยความจำในบริการที่ผู้ใช้รับรู้

เกมของคุณอาจต้องเรียกใช้ตรรกะในบริการที่ผู้ใช้รับรู้สำหรับ Use Case เช่น การดาวน์โหลดไฟล์ขนาดใหญ่ให้เสร็จสมบูรณ์ หรือสำหรับระบบแชทด้วยเสียงในเบื้องหลัง กลยุทธ์เหล่านี้จะช่วยคุณจัดการและลดการใช้งานหน่วยความจำในสถานการณ์ดังกล่าวได้

กลยุทธ์สำหรับการดาวน์โหลดไฟล์ขนาดใหญ่

กลยุทธ์เหล่านี้อาจใช้ได้กับการดาวน์โหลดไฟล์ขนาดใหญ่ที่คุณต้องการให้ดำเนินการต่อแม้หลังจากที่ผู้ใช้ย่อเกมลงแล้ว

1. แยกกระบวนการดาวน์โหลด

สิ่งที่จะทำ: ตรวจสอบว่าระบบปฏิบัติการสามารถเรียกคืนหน่วยความจำที่แอปของคุณไม่ได้ใช้ได้ทันทีโดยทำการดาวน์โหลดในกระบวนการแยกต่างหาก เนื่องจากตัวจัดสรรหน่วยความจำอาจเก็บเพจพูลหน่วยความจำไว้และทำให้หน่วยความจำ RSS แบบไม่ระบุชื่อสูงขึ้นอย่างไม่เป็นจริงแม้หลังจากปล่อยอาร์เรย์แล้วก็ตาม เมื่อคุณสิ้นสุดหรือออกจากบริการและกระบวนการอย่างชัดเจน ระบบจะส่งคืนหน่วยความจำไปยังพูลของระบบปฏิบัติการ และกระบวนการหลักของคุณจะยังคงไม่ได้รับผลกระทบ

  • ใน Unity: ลดภาระการดาวน์โหลดไปยัง Service เนทีฟของ Android ที่ประกาศด้วยกระบวนการ เช่น android:process=":downloader" ในไฟล์ Manifest ที่กำหนดเอง และเรียกใช้ด้วย AndroidJavaClass JNI ของ Unity อย่าลืมสิ้นสุดกระบวนการเมื่อการดาวน์โหลดเสร็จสมบูรณ์ ดูคำแนะนำโดยละเอียดเพิ่มเติมได้ที่ เรียกใช้บริการที่รับรู้ได้ในกระบวนการแยกต่างหากด้วย Unity

  • ใน Unreal: ประกาศ Android ที่กำหนดเอง Service ด้วยกระบวนการ เช่น android:process=":downloader" โดยใช้ Unreal Plugin Language และทริกเกอร์ ด้วย C++ JNI อย่าลืมสิ้นสุดกระบวนการเมื่อการดาวน์โหลดเสร็จสมบูรณ์ ดูคำแนะนำโดยละเอียดเพิ่มเติมได้ที่ เรียกใช้บริการที่รับรู้ได้ในกระบวนการแยกต่างหากด้วย Unreal สำหรับ

  • สำหรับ Native Android: ประกาศ Service ใน AndroidManifest ด้วย กระบวนการ เช่น android:process=":downloader" เรียกใช้การดาวน์โหลดในกระบวนการที่แยกต่างหากนี้ และเรียกใช้ Process.killProcess(Process.myPid()) เมื่อการดาวน์โหลดเสร็จสมบูรณ์

ประโยชน์: ลดระยะเวลาที่หน่วยความจำถูกเก็บไว้ และอนุญาตให้การดาวน์โหลดดำเนินการต่อขณะที่ปล่อยหน่วยความจำที่กระบวนการหลักขนาดใหญ่กว่าใช้

2. สตรีมการดาวน์โหลดไปยังดิสก์โดยตรง

สิ่งที่จะทำ: สตรีมข้อมูลจากซ็อกเก็ตเครือข่ายไปยังดิสก์โดยตรงโดยใช้บัฟเฟอร์ขนาดเล็ก ขนาดคงที่ และนำกลับมาใช้ใหม่ได้ แทนที่จะสะสมการตอบกลับของเครือข่ายลงในอาร์เรย์ขนาดใหญ่ก่อนที่จะเขียน

  • ใน Unity: หลีกเลี่ยงการใช้ DownloadHandlerBuffer สำหรับชุดชิ้นงานหรือ ไฟล์ขนาดใหญ่ เนื่องจากจะจัดสรรบัฟเฟอร์หน่วยความจำเนทีฟที่มีขนาดเท่ากับขนาด ไฟล์ (หน่วยความจำ RSS แบบไม่ระบุชื่อ) แต่ให้ใช้ DownloadHandlerFile เพื่อสตรีม ไบต์ไปยังดิสก์แบบเนทีฟในเทรดเบื้องหลัง

  • ใน Unreal: ส่งต่อข้อมูลที่เข้ามาเป็นก้อนๆ ไปยัง FArchive (ที่เก็บถาวรที่สำรองด้วยไฟล์โดยใช้ File Manager ของ Unreal) โดยตรงด้วย SetResponseBodyReceiveStream() แทนที่จะผนวกเพย์โหลดจาก IHttpRequest ลงใน TArray<uint8>

  • สำหรับ Native Android: ส่งต่อ InputStream ไปยัง FileOutputStream โดยใช้บัฟเฟอร์แบบพูล แทนที่จะเรียกใช้ .readBytes() หรือ .string() ในการตอบกลับ HTTP

ประโยชน์: ลดการใช้หน่วยความจำสูงสุด

3. บีบอัดไฟล์ที่ดาวน์โหลดแบบสตรีม

สิ่งที่จะทำ: หากการดาวน์โหลดมีการบีบอัด ให้ห่อสตรีมอินพุตเครือข่ายด้วย ตัวบีบอัดแบบสตรีม เช่น ZipInputStream แทนที่จะดาวน์โหลด ไฟล์ โหลดลงใน RAM แล้วแยกไฟล์

ประโยชน์: ลดการใช้หน่วยความจำสูงสุด

4. มอบหมายงานให้ระบบปฏิบัติการ

สิ่งที่จะทำ: มอบหมายงานให้ API เนทีฟของ Android เพื่อหลีกเลี่ยงการจัดการหน่วยความจำเบื้องหลังทั้งหมด

  • WorkManager เป็น Wrapper ที่ทันสมัยและแนะนำให้ใช้แทน JobScheduler ระดับระบบปฏิบัติการ สำหรับ Android 14 ขึ้นไป WorkManager จะจัดการการดาวน์โหลดที่ผู้ใช้ทริกเกอร์โดยอัตโนมัติเป็นงานการโอนข้อมูลที่เริ่มต้นโดยผู้ใช้ (UIDT) ซึ่งจะทำงานภายในกระบวนการของแอป ดังนั้นคุณจึงยังต้องสตรีมไปยังดิสก์โดยตรงเพื่อลดการใช้หน่วยความจำ UIDT จะปกป้องแอปของคุณจากการขัดข้องเนื่องจากหน่วยความจำเหลือน้อยโดยอนุญาตให้ระบบปฏิบัติการหยุดชั่วคราวและดำเนินการดาวน์โหลดต่ออย่างราบรื่นหากทรัพยากรระบบมีจำกัด

  • DownloadManager จะทำงานในกระบวนการระบบแยกต่างหากและจะไม่ระบุการใช้งานหน่วยความจำสำหรับการดาวน์โหลดให้กับแอปของคุณ แอปของคุณจะได้รับการแจ้งเตือนแบบออกอากาศเมื่อดาวน์โหลดไฟล์และไฟล์พร้อมใช้งาน

ประโยชน์: WorkManager ช่วยจัดการกับสถานการณ์ที่หน่วยความจำเหลือน้อย และ DownloadManager จะลดการใช้งานหน่วยความจำของแอป

กลยุทธ์สำหรับบริการเสริม

กลยุทธ์เหล่านี้อาจใช้ได้กับบริการเสริมที่เกมเรียกใช้แบบขนานกับกระบวนการหลักของเกม เช่น การแชทด้วยเสียงในเบื้องหลัง

1. แยกกระบวนการ

สิ่งที่จะทำ: แยกฟีเจอร์ เช่น โซลูชันการแชทด้วยเสียง ออกจากเกมเอนจินหลัก ตัวอย่างเช่น คุณสามารถเรียกใช้การจับภาพจากไมโครโฟนและ การสตรีมเครือข่ายภายในบริการที่ทำงานอยู่เบื้องหน้าของ Android ที่กำหนดให้กับกระบวนการแยกต่างหาก (ประกาศในไฟล์ Manifest เช่น ด้วย android:process=":voice") ได้

ประโยชน์: เมื่อย่อแอปลง กระบวนการหลักของเกมที่ใช้ทรัพยากรมากจะเปลี่ยนไปอยู่ในสถานะแคชที่มีลำดับความสำคัญต่ำกว่า ขณะที่บริการเสริมที่มีน้ำหนักเบากว่าจะยังคงอยู่ในสถานะบริการที่ผู้ใช้รับรู้

2. ลดหน่วยความจำในกระบวนการที่ไม่ได้ใช้

สิ่งที่จะทำ: หากบริการเสริมผสานรวมกับเอนจินของเกมมากเกินไปจนแยกออกไม่ได้ ให้พยายามลดการใช้ทรัพยากรของกระบวนการให้มากที่สุดทันทีที่เกมเปลี่ยนไปทำงานในเบื้องหลัง/ย่อลง พิจารณาล้างแคชพื้นผิว ยกเลิกการโหลดฉากที่ไม่จำเป็น ลดอัตราการประมวลผลของเอนจินและอัตราการแสดงผลลงเป็น 0 และเรียกใช้ระบบจัดการหน่วยความจำที่ไม่ใช้แล้วอย่างชัดเจน

  • ใน Unity: ทำการลดนี้เมื่อ OnApplicationPause() ทริกเกอร์ Resources.UnloadUnusedAssets() อาจมีประโยชน์

  • ใน Unreal: ผูกตรรกะการลดขนาดกับ Delegate ApplicationWillEnterBackgroundDelegate

  • สำหรับ Native Android: ทำการลดขนาดใน onPause() หรือ onStop() ตามความเหมาะสม ระบบปฏิบัติการอาจพยายามเรียกใช้การติดตั้งใช้งานของ onTrimMemory()ก่อนที่จะใช้การสิ้นสุดกระบวนการที่ใช้หน่วยความจำสูง

ประโยชน์: ลดการใช้งานหน่วยความจำที่ไม่จำเป็นเมื่อเกมไม่ได้ทำงานในเบื้องหน้า