การเพิ่มประสิทธิภาพหน่วยความจำของเกม Unity ใน Android

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

ปัญหาสำคัญที่เกิดจากการใช้งานหน่วยความจำมากเกินไป

  • การสิ้นสุดการทำงานเนื่องจากหน่วยความจำไม่เพียงพอ (LMK): ระบบปฏิบัติการจะปิดแอปในเบื้องหลัง หรือแม้แต่แอปที่ทำงานอยู่เบื้องหน้าโดยบังคับเพื่อแก้ไขปัญหาการขาดแคลนหน่วยความจำทั่วทั้งระบบ
  • เฟรมหลุดและภาพกระตุก (Jank): การเพิ่มขึ้นของการเก็บขยะ (GC) บ่อยครั้ง ใน Managed Heap หรือการสลับหน่วยความจำระดับ OS ทำให้เกิดคอขวดในการประมวลผล
  • การควบคุมปริมาณความร้อนและแบตเตอรี่หมดเร็ว: การจัดสรรหน่วยความจำ การยกเลิกการจัดสรร และการคอมมิตหน้าหน่วยความจำอย่างต่อเนื่องทำให้เกิดค่าใช้จ่ายเพิ่มเติมของ CPU อย่างมาก ซึ่งส่งผลให้ความร้อนเพิ่มขึ้นและแบตเตอรี่หมดเร็วขึ้น

การอัปเดตการจัดการหน่วยความจำของ Android

  • Android 17: การเปิดตัว MemoryLimiter: Android 17 เปิดตัว MemoryLimiter ซึ่งจะตรวจสอบการใช้หน่วยความจำของแอปพลิเคชันอย่างต่อเนื่องเทียบกับเกณฑ์เฉพาะของอุปกรณ์ ระบบจะปิดแอปที่ใช้หน่วยความจำเกิน ขีดจำกัดทันที เนื่องจากกลไกนี้เข้มงวดกว่า LMK แบบเดิม การจัดการการใช้งานหน่วยความจำสูงสุดจึงมีความสำคัญมากกว่าที่เคย

ลดการใช้งานหน่วยความจำใน Unity

ใน Unity เมื่อเอนจินขยายพูลหน่วยความจำภายใน (Native Block Allocators และ Managed Heap) เพื่อรองรับโหลดสูงสุดที่สูง เอนจินจะเก็บหน้าหน่วยความจำเหล่านั้นไว้แทนที่จะส่งคืนไปยังระบบปฏิบัติการทันที ด้วยเหตุนี้ แม้ว่าจะยกเลิกการโหลดเนื้อหาขนาดใหญ่แล้ว แต่หน่วยความจำที่ใช้พื้นฐานก็ยังคงสูงอยู่ ทำให้แอปพลิเคชันมีความเสี่ยงสูงต่อการสิ้นสุดกระบวนการของระบบปฏิบัติการ Android (เช่น LMK หรือ MemoryLimiter)

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

  • หมวดหมู่ ก: ลดการใช้งานหน่วยความจำสูงสุด
  • หมวดหมู่ ข: กำจัดร่องรอยของเนื้อหาและระบบที่ไม่จำเป็น
  • หมวดหมู่ ค. ขจัดหน่วยความจำที่จัดสรรที่ไม่จำเป็นสำหรับ GC

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

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

1. หลีกเลี่ยง AssetBundle ที่มีขนาดใหญ่เกินไป

เนื่องจากกลไกการโหลด Bundle ของ Unity ทำให้ AssetBundle ขนาดใหญ่ส่งผลให้เกิด การใช้หน่วยความจำมากเกินไปและการขัดข้องเนื่องจากหน่วยความจำไม่เพียงพอ จัดกลุ่มให้เป็นโมดูลเพื่อให้มั่นใจว่ามีการจัดการทรัพยากรอย่างมีประสิทธิภาพ

ปัญหาสำคัญ

  • ค่าใช้จ่ายด้านหน่วยความจำสูง: การขอชิ้นงานขนาดเล็กเพียงชิ้นเดียวจะบังคับให้ Unity โหลดทั้งไฟล์ Bundle (รวมถึงส่วนหัว ข้อมูลเมตา และบัฟเฟอร์การสตรีม) ลงใน RAM
  • การดักการเลิกโหลด: หากมีการใช้งานชิ้นงานใดๆ ในแพ็กเกจอยู่ ระบบจะเลิกโหลดแพ็กเกจทั้งหมดไม่ได้ ซึ่งจะทำให้ข้อมูลที่ไม่ได้ใช้ติดอยู่ใน RAM

แนวทางปฏิบัติแนะนำ

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

2. เพิ่มประสิทธิภาพการอ้างอิงชิ้นงานใน ScriptableObjects

UnityEngine.Object โดยตรงใน ScriptableObject จะสร้าง การอ้างอิงแบบฮาร์ดโดยตรง ซึ่งบังคับให้โหลดชิ้นงานที่อ้างอิงทั้งหมดลงใน RAM ทันทีที่ โหลดหรือสร้างอินสแตนซ์ของ ScriptableObject เอง

// BEFORE: Loading SceneRequiredAssets forces _worldAsset and _spawnSettings into RAM immediately
public class SceneRequiredAssets : ScriptableObject
{
    public string sceneName;
    public Object _worldAsset;
    public Object _spawnSettings;
}

// AFTER: Use AssetReference to enable asynchronous, on-demand loading using Addressables
public class SceneRequiredAssets : ScriptableObject
{
    public string sceneName;
    public AssetReference _worldAsset;
    public AssetReference _spawnSettings;
}

3. กำหนดค่าคลิปเสียงและช่องทาง

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

หมวดหมู่เสียง ประเภทการใส่ผ้า เหตุผล
BGM (เพลงประกอบ) สตรีมมิง สตรีมเสียงจากดิสก์ในบัฟเฟอร์ขนาดเล็กเพื่อลดการใช้หน่วยความจำที่พุ่งสูงขึ้น
SFX แบบยาว บีบอัดในหน่วยความจำ ช่วยให้โอเวอร์เฮดของ RAM ต่ำและคลายการบีบอัดเสียงได้ทันทีระหว่างการเล่น
SFX สั้นๆ และบ่อยครั้ง แตกไฟล์เมื่อโหลด คลายการบีบอัดเสียงลงใน RAM เมื่อโหลดเพื่อหลีกเลี่ยงโอเวอร์เฮดของ CPU ขณะรันไทม์ระหว่างการเล่น

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

  • เปิดใช้ "บังคับเป็นโมโนสำหรับเสียงแบบไม่มีทิศทาง": ชิ้นงานเสียงจำนวนมาก เช่น เอฟเฟกต์เสียง UI, เสียงบรรยาย และเอฟเฟกต์เสียงเชิงพื้นที่ 3 มิติ ไม่ จำเป็นต้องแยกช่องสัญญาณสเตอริโอ เปิดใช้บังคับเป็นโมโนในการ AudioClipตั้งค่าการนำเข้าสำหรับชิ้นงานเหล่านี้เพื่อลด หน่วยความจำที่ใช้และขนาดบัฟเฟอร์ลงครึ่งหนึ่ง (ลดลง 50%)
  • จำกัดช่องสัญญาณเสียงที่ใช้งานอยู่ตามระดับอุปกรณ์: เสียงที่ใช้งานอยู่แต่ละเสียงจะจัดสรรบัฟเฟอร์ตัวผสม DSP และการคลายการบีบอัดแบบเนทีฟ ในอุปกรณ์ที่มีหน่วยความจำต่ำ ให้ลดจำนวนช่องที่ใช้งานพร้อมกันสูงสุด เช่น โดยการ ลดเสียงจริงสูงสุด (เช่น จาก 32 เป็น 16) โดยใช้ AudioSettings.Reset ใน Unity หรือโดยการจำกัดช่องการเล่นซอฟต์แวร์ ในเครื่องมือเสียงของบุคคลที่สาม

4. ใช้กลยุทธ์การจัดกลุ่มและการเผยแพร่ออบเจ็กต์

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

  • การดำเนินการ: ล้างหรือตัดออบเจ็กต์ที่รวมไว้ซึ่งไม่ได้ใช้ออกเป็นระยะๆ ในระหว่างการเปลี่ยนฉากหรือช่วงที่มีกิจกรรมน้อย เพื่อให้ Unity แสดงหรือนำพื้นที่ที่สงวนไว้กลับมาใช้ใหม่สำหรับการจัดสรรอื่นๆ

หมวดหมู่ ข: ลดการใช้งานหน่วยความจำที่ไม่จำเป็น

การกำจัดเนื้อหากราฟิกที่ซ้ำซ้อนและการแสดงผลบัฟเฟอร์เป้าหมายโดยตรงจะช่วยลดหน่วยความจำที่ใช้พื้นฐาน

1. เพิ่มประสิทธิภาพพื้นผิวการแสดงผลและความลึกของกล้อง

  • นำบัฟเฟอร์ความลึก/สเตนซิลออก: ตั้งค่ารูปแบบสเตนซิลความลึกเป็นไม่มี สำหรับ Render Texture ที่ต้องการเฉพาะข้อมูลสี
  • ปิดใช้พื้นผิวความลึกของกล้อง UI: สำหรับกล้อง UI ที่ไม่จำเป็นต้องใช้ข้อมูลความลึก ให้ปิดใช้การสร้างพื้นผิวความลึกในการตั้งค่ากล้อง URP เพื่อกำจัด CopyDepth Pass และหน่วยความจำพื้นผิว GPU ที่เกี่ยวข้อง

2. เพิ่มประสิทธิภาพพื้นผิวและ Mesh

หมวดหมู่ หลักเกณฑ์การเพิ่มประสิทธิภาพ
การบีบอัดพื้นผิว ใช้รูปแบบการบีบอัดของแพลตฟอร์มเป้าหมายเสมอ (เช่น ASTC สำหรับ Android)
เปิดใช้การอ่าน/เขียน ปิดใช้ไว้จนกว่าจะจำเป็น การเปิดใช้ตัวเลือกนี้จะทำซ้ำหน่วยความจำของพื้นผิวใน RAM ของ CPU และ GPU
Mipmap ปิดใช้ Mipmap สำหรับพื้นผิว UI หรือออบเจ็กต์ที่ตรึงไว้ที่ระยะกล้องคงที่ ซึ่งจะช่วยประหยัดหน่วยความจำพื้นผิวได้ประมาณ 33%
ความซับซ้อนของ Mesh ลดจำนวนรูปหลายเหลี่ยมและสตรีมจุดยอดที่ไม่จำเป็นเพื่อลดหน่วยความจำที่ใช้ของ GPU และหน่วยความจำแบบเนทีฟ

3. แยกตัวแปรของ Shader และเพิ่มประสิทธิภาพหน่วยความจำ

Uber-shader (เช่น URP Lit Shader) จะรวมฟีเจอร์ต่างๆ ไว้ด้วยกันโดยใช้คีย์เวิร์ด #multi_compile และ shader_feature หากไม่มีการเพิ่มประสิทธิภาพ การระเบิดแบบผสมจะสร้างตัวแปรเฉดสีที่ไม่ซ้ำกันหลายหมื่นรายการ ซึ่งทำให้ขนาดบิลด์เพิ่มขึ้น การใช้หน่วยความจำแบบเนทีฟจำนวนมาก และการคอมไพล์ไดรเวอร์ GPU ขัดข้องระหว่างการเล่นเกม

ก. กลไกของโอเวอร์เฮดหน่วยความจำของตัวแปรเฉดสี

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

ข. ไปป์ไลน์การลบแบบหลายระดับในตัวของ Unity: Unity จะลบ ตัวแปรที่ไม่จำเป็นโดยอัตโนมัติในเวลาบิลด์ตามการตั้งค่ากราฟิกของแพลตฟอร์มเป้าหมาย และฟีเจอร์ของเอนจินที่ไม่ได้ใช้ (เช่น หมอก, ไลท์แมป และการตั้งค่า XR) นอกจากนี้ ระบบจะกรองตัวแปร shader_feature ออกโดยอัตโนมัติ หากไม่มีการใช้คีย์เวิร์ดของตัวแปรดังกล่าวในเนื้อหาใดๆ ในโปรเจ็กต์ ขณะที่ระบบจะรวมตัวแปร #multi_compile ไว้โดยไม่คำนึงถึงการใช้งาน อย่างไรก็ตาม หากฟีเจอร์การแสดงผลหรือการประมวลผลภายหลัง (เช่น ไฟเพิ่มเติม เงาแบบเรียลไทม์ หรือUberPostเอฟเฟกต์#multi_compileที่ไม่ได้ใช้) ยังคงเปิดใช้อยู่ ในการตั้งค่า URP หรือไปป์ไลน์ Unity จะไม่สามารถระบุได้ว่าเนื้อหาของคุณใช้ฟีเจอร์เหล่านั้นจริงหรือไม่ และจะสร้างตัวแปรทั้งหมดที่เกี่ยวข้อง ตรวจสอบและปิดใช้การตั้งค่าไปป์ไลน์ที่ไม่ได้ใช้เพื่อให้ตรงกับข้อกำหนดในการแสดงผลจริงและป้องกันการสร้างตัวแปรที่ไม่จำเป็น

ค. สถาปัตยกรรมการตัดอัตโนมัติที่กำหนดเอง (IPreprocessShaders) เนื่องจากการวิเคราะห์แบบคงที่ไม่สามารถตรวจจับคีย์เวิร์ดที่แก้ไขแบบไดนามิกโดยใช้สคริปต์ C# รันไทม์ (Material.EnableKeyword) การตัดมาตรฐานจึงมักไม่เพียงพอ หากต้องการ ตรวจสอบว่ามีเฉพาะตัวแปรที่ใช้จริงเท่านั้น คุณสามารถรวบรวมตัวแปร ในระหว่างชุดโปรแกรมทดสอบ QA โดยใช้ Player.log (เปิดใช้ การบันทึกการคอมไพล์ Shader ในการตั้งค่า Editor) หรือ Profiler Traces (เครื่องหมาย Shader.CreateGPUProgram) จากนั้นใช้ IPreprocessShaders.OnProcessShader ในสคริปต์ Editor เพื่อกรอง ตัวแปรที่ไม่เคยดำเนินการในระหว่างรันไทม์ออก โดยเก็บเฉพาะตัวแปรที่จำเป็น ไว้ในบิลด์

ง. รวม Shader ที่ซ้ำกัน: Uber-Shader เช่น Lit และ Simple Lit มีคำสั่ง #multi_compile จำนวนมากและใช้หน่วยความจำ จำนวนมากเมื่อสร้างตัวแปรหลายรายการ

  • สร้างโปรไฟล์การใช้งานพร้อมกัน: สร้างโปรไฟล์การใช้งานหน่วยความจำเพื่อตรวจสอบว่า ทั้ง 2 ตัวปรับแสงเงาใช้หน่วยความจำจำนวนมากพร้อมกันหรือไม่ และตรวจสอบ ว่าโปรเจ็กต์ของคุณจำเป็นต้องใช้ทั้ง 2 ตัวปรับแสงเงาจริงหรือไม่
  • กำหนดมาตรฐานให้ใช้เฉดเดอร์เดียว: เมื่อไม่จำเป็นต้องใช้การแสดงผลตามหลักฟิสิกส์ (PBR) ให้กำหนดค่าชิ้นงานเพื่อกำจัดการใช้เฉดเดอร์ Lit ที่ซ้ำซ้อน และกำหนดมาตรฐานให้ใช้ Simple Lit (เช่น ตรวจสอบว่าโมเดล FBX ที่ส่งออกมีวัสดุ Lit เริ่มต้นที่ใช้โดยไม่ตั้งใจระหว่างการนำเข้าหรือไม่)

หมวดหมู่ ค. การหลีกเลี่ยงการจัดสรร GC ที่ไม่จำเป็น

การจัดสรรหน่วยความจำของตัวเก็บขยะ (GC) ในฮีปที่มีการจัดการจะทำให้เกิดการ กระจายหน่วยความจำ การขยายฮีปที่ไม่เคยลดขนาด และเฟรมตกอย่างรุนแรงในระหว่าง การหยุดชั่วคราวของ GC

1. ป้องกันการจัดสรร Lambda Closure

เมื่อนิพจน์ Lambda บันทึกตัวแปรภายในภายนอก C# จะสร้างคลาสการแสดงผลโดยนัยในฮีป การดำเนินการนี้ภายใน Update จะจัดสรรอินสแตนซ์ Closure ทุกเฟรม

// Bad: Capturing local variable 'targetId' allocates a new closure object on the Heap every frame
void Update()
{
    int targetId = 100;
    Monster target = monsterList.Find(m => m.Id == targetId);
}

// Good 1: Replace with a standard 'for' loop (Recommended: 0 B allocation)
void Update()
{
    int targetId = 100;
    Monster target = null;
    for (int i = 0; i < monsterList.Count; i++)
    {
        if (monsterList[i].Id == targetId)
        {
            target = monsterList[i];
            break;
        }
    }
}

// Good 2: Use a static lambda (C# 9.0+) if no outer variables are captured
Monster target = monsterList.Find(static m => m.Id == 100);

2. ใช้ stackalloc และ Span

หลีกเลี่ยงการจัดสรรฮีปสำหรับอาร์เรย์ชั่วคราวที่มีอายุสั้นโดยใช้หน่วยความจำสแต็ก

// Before: Allocates an array on the Heap every call (GC Target)
Vector2[] pos = new Vector2[4];

// After: Utilizes Stack memory using System.Span (0 B Heap Allocation)
System.Span<Vector2> pos = stackalloc Vector2[4];

3. เพิ่มประสิทธิภาพการทำซ้ำคอลเล็กชัน

หลีกเลี่ยงการจัดสรรฮีป Boxing และ Enumerator ที่เกิดจากส่วนขยาย LINQ หรือตัวช่วยเข้าถึง ReadOnlyCollection

// Before: LINQ Count() causes internal GetEnumerator() heap allocations
bool hasData = component != null && component.parameters.Count(parameter => parameter.overrideState) > 0;

// After: Replaced with indexer and direct loop iteration
bool hasData = HasDataOptimized(component);

private bool HasDataOptimized(TestComponent component)
{
    if (component == null) return false;

    var count = component.parameters.Count;
    for (var i = 0; i < count; ++i)
    {
        if (component.parameters[i].overrideState)
            return true;
    }
    return false;
}

4. กฎการป้องกันการจัดสรร GC เพิ่มเติม

  • หลีกเลี่ยงการใช้ Camera.allCameras เนื่องจากจะสร้างอาร์เรย์ Camera[] ใหม่ในฮีปทุกครั้งที่เรียกใช้ แต่ให้แคชอาร์เรย์กล้องและส่งไปยัง Camera.GetAllCameras(_allCameras) แทน
  • หลีกเลี่ยง foreach ใน IReadOnlyList<T>: การทำซ้ำในอินเทอร์เฟซทำให้เกิดการจัดกลุ่มตัวแจงนับโครงสร้าง ซึ่งจะสร้างการจัดสรร GC โปรดใช้ลูป for มาตรฐานแทน
  • แคชออบเจ็กต์โครูทีน: แคชอินสแตนซ์ WaitForSeconds แทนการสร้างอินสแตนซ์ yield return new WaitForSeconds(time); ซ้ำๆ
  • คีย์โครงสร้างที่กำหนดเองในพจนานุกรม: การใช้โครงสร้างที่กำหนดเองเป็นคีย์พจนานุกรมจะเรียกใช้ Equals เริ่มต้น ซึ่งจะทริกเกอร์การทำออบเจ็กต์บ็อกซิ่ง ใช้ IEqualityComparer<T> และส่งไปยังตัวสร้างพจนานุกรม
public struct TypeKey
{
    public int v1;
    public int v2;

    public class TypeKeyComparer : IEqualityComparer<TypeKey>
    {
        public bool Equals(TypeKey x, TypeKey y) => x.v1 == y.v1 && x.v2 == y.v2;
        public int GetHashCode(TypeKey obj) => obj.v1.GetHashCode() ^ obj.v2.GetHashCode();
    }
}

// Pass custom comparer during Dictionary initialization to prevent boxing
public readonly Dictionary<TypeKey, int> _typeKeyDictionary = new(new TypeKey.TypeKeyComparer());

5. ลดการใช้งานทั่วไปและการสะท้อน

แม้ว่าเมธอดทั่วไปจะช่วยให้ใช้โค้ดซ้ำและบำรุงรักษาโค้ดได้อย่างยอดเยี่ยม แต่การใช้เมธอดเหล่านี้มากเกินไปอาจส่งผลเสียต่อโปรเจ็กต์ในบริบทของแบ็กเอนด์ Unity IL2CPP (Intermediate Language to C++)

  • โค้ดที่มากเกินไปของ IL2CPP: สำหรับชุดค่าผสมประเภททั่วไปที่ไม่ซ้ำกันทุกชุด IL2CPP จะสร้างโค้ดเวอร์ชันเฉพาะ การใช้ Generics ที่ซับซ้อนมากเกินไปอาจทำให้โค้ด C++ ที่สร้างขึ้นเกิด "การระเบิดแบบคอมบิเนชัน" ซึ่งจะเพิ่มขนาดไบนารีของแอปพลิเคชันและร่องรอยการใช้หน่วยความจำแบบเนทีฟอย่างมาก
  • ค่าใช้จ่ายเพิ่มเติมในการสะท้อน: เมธอดที่ใช้การสะท้อน เช่น API System.Reflection จะทำงานช้าโดยธรรมชาติและมักทำให้เกิดการจัดสรรฮีประหว่างรันไทม์
  • แนวทางปฏิบัติแนะนำ: ใช้ Generics อย่างรอบคอบ โดยให้ความสำคัญกับความชัดเจนของสถาปัตยกรรมมากกว่าการใช้งานแบบกว้างๆ โดยไม่เลือก ในกรณีที่ ประสิทธิภาพมีความสําคัญ ให้ใช้ประเภทที่เฉพาะเจาะจงหรือการ พ้องรูปที่อิงตามอินเทอร์เฟซ สำหรับการสะท้อน ให้แคชผลลัพธ์ เช่น MethodInfo หรือ FieldInfo ระหว่างการเริ่มต้นแทนการค้นหาในลูปการอัปเดต

6. หลีกเลี่ยงการรั่วไหลของเชลล์ที่มีการจัดการ

ทุก UnityEngine.Object เช่น MonoBehaviour, Texture หรือ GameObject มี Wrapper "Managed Shell" ของ C# ที่สื่อสารกับเครื่องมือ C++ ดั้งเดิม

  • ปัญหา: หาก Managed Shell อยู่ในหน่วยความจำโดยการอ้างอิงแบบคงที่ การสมัครรับข้อมูลเหตุการณ์แบบถาวร หรือการปิดที่ไม่ได้ล้าง GC จะไม่สามารถเรียกคืนหน่วยความจำได้ แม้ว่าออบเจ็กต์เนทีฟจะถูกทำลาย แต่ Wrapper ที่มีการจัดการ จะยังคงอยู่ ซึ่งทำให้เกิดการรั่วไหลของหน่วยความจำ "ผี" ที่ทำให้ Managed Heap บวม
  • ความละเอียด: ใช้รูปแบบการล้างข้อมูลที่แข็งแกร่งเสมอ เมื่อทำลายออบเจ็กต์หรือเปลี่ยนฉาก ให้ยกเลิกการสมัครรับข้อมูลจากเหตุการณ์อย่างชัดเจนโดยใช้โอเปอเรเตอร์ -= และยกเลิกการอ้างอิงแบบคงที่ไปยังประเภท UnityEngine.Object การล้างข้อมูลนี้ช่วยให้ GC รวบรวม Wrapper ได้สำเร็จเมื่อเครื่องมือดั้งเดิมปล่อยแฮนเดิล

7. ใช้แคตตาล็อกไบนารีสำหรับ Addressables

ใน Unity 2022 และเวอร์ชันก่อนหน้า Addressables จะใช้แคตตาล็อก JSON เป็นค่าเริ่มต้นแทนแคตตาล็อกไบนารี (ซึ่งเป็นค่าเริ่มต้นใน Unity 2023 และเวอร์ชันที่ใหม่กว่า) หากโปรเจ็กต์ใช้แคตตาล็อก JSON การโหลดแคตตาล็อกขนาดใหญ่เมื่อเริ่มต้นระบบจะอ่านไฟล์ทั้งหมดเป็นสตริง C# ขนาดใหญ่รายการเดียว และจัดสรรสตริงที่มีการจัดการและอาร์เรย์ byte[] ชั่วคราวหลายพันรายการระหว่างการแยกวิเคราะห์ JSON และการถอดรหัส Base64 ซึ่งจะทำให้เกิดการจัดสรร GC ที่เพิ่มขึ้นอย่างมากซึ่งจะขยาย Managed Heap อย่างถาวร ดังนั้นให้เปลี่ยนไปใช้แคตตาล็อกไบนารีเพื่อสตรีมข้อมูลที่กะทัดรัดโดยตรง