เมื่อคุณเข้าใจการจัดการหน่วยความจำของ 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 ที่กำหนดเอง และเรียกใช้ด้วยAndroidJavaClassJNI ของ 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()ก่อนที่จะใช้การสิ้นสุดกระบวนการที่ใช้หน่วยความจำสูง
ประโยชน์: ลดการใช้งานหน่วยความจำที่ไม่จำเป็นเมื่อเกมไม่ได้ทำงานในเบื้องหน้า