
AssetBundle和热更新和语言基础移動端設備(iOS/Android)對單個 App 的總內存有限制。进程使用的内存释放后有可能会归还给操作系统1.Native 堆(贴图,模型,文件)非托管堆不受 GC 管辖: GC 扫描不到这里,所以即使你 C# 里的引用断了,如果 Native 层没卸载,内存依然被占用(这就是 Native 内存泄漏)。Native 堆會觸發 GC 嗎?不會。 Native 堆(紋理、網格)的內存是由 Unity 引擎底層(C++)手動管理的。如果 Native 內存不夠,系統會直接閃退(OOM),或者觸發引擎內部的 UnloadUnusedAssets(但這不是 GC,這是資源卸載)。Native 堆:按需申請,用完即還,主动销毁会还给作业系统,没有预分配,Native 堆效率高,能直接给GPU, 當你調用 material.SetTexture(“_MainTex”, myTex) 時,你只是在 C# 層傳遞了一個引用的 Wrapper(包裝對象)。Unity 底層(C++)會立刻找到這個 Wrapper 指向的 Native 堆 裡的真實二進制數據,然後把 Native 地址的对应的数据交給 GPU显存上。 GPU 無法直接使用託管堆對象,超过大小也会扩张,进程隔离:现代操作系统以进程(Process)为单位分配资源。当你关掉 App 或被杀掉时,OS 会收回该进程关联的所有虚拟内存页表和物理内存。Native 堆虽然能动态扩张,但过度申请会触发系统 LMK 导致 App 闪退。2.托管堆託管堆 C# 分配對象的地方不够的时候,Mono 或 IL2CPP 的內存管理器會去託管堆找一塊連續的空地。如果剩餘的連續空間不足以放下這個新對象,就會觸發 GC。GC 會掃描所有 C# 對象,看看誰沒人要了(沒有引用了),然後把它標記為「可回收」,釋放空間。如果 GC 完之後,空間還是不夠放新對象,託管堆會向操作系統申請擴張。 託管堆一旦擴張,極難縮回(它會一直佔著內存不還給系統)。這就是為什麼頻繁 new 對象會導致遊戲佔用內存越來越大的原因。託管堆的持續申請會造成閃退嗎,会,使用内存超过就会闪退,堆内存为什么不连续,因为gc回收之后会产生小洞,对象的回收周期不一致导致native堆 和托管堆的区别托管堆(Managed Heap)有 GC 拷贝和整理 的额外开销。4MB 的 Native 内存是纯粹的数据,而堆内存中每个对象都有 对象头(Object Header) 和 类型句柄,实际占用更大,且访问时多一层寻址。都是虚拟地址,但是native可以直接计算出来,托管要访问间接对象获得对象大小跳转3.AB包和资源(Texture,网格)的关系關係:AB 包是「母親」,資源是「孩子」。Unload(false):相當於「斷絕母子關係」。AB 包(母親)從內存消失了,但資源(孩子)還留在 Native 堆。冗餘產生過程:你加載了 AB 包 Character,加載了裡面的 TextureA。你調用了 Unload(false)。這時 AB 包沒了,但 TextureA 還在 Native 堆。你再次加載 AB 包 Character,然後又加載 TextureA。重點來了:因為之前的母子關係斷了,Unity 不知道內存裡已經有一個 TextureA 了,它會重新在 Native 堆再創建一個全新的 TextureA。AB 包的引用計數:到底是算「包」還是算「資源」?引用計數通常是針對「AB 包」。因為 AB 包是內存管理的最小單位。只要包裡還有一個資源(比如一張小貼圖)被引用,整個 AB 包的鏡像(SerializedFile)就必須留在 Native 堆。你無法卸載半個 AB 包。託管堆的包裝對象算一個引用嗎? 算4.LoadFromFile vs LoadFromMemoryLoadFromFile (文件句柄):Unity 只是在硬盤上「打開了書」,並沒有讀內容。內存裡只佔一個很小的索引(文件句柄)。只有當你 LoadAsset 時,才從硬盤讀對應那塊資源。極省內存。LoadFromMemory:你先把 AB 包的所有二進制數據讀進 C# 的 byte[] 數組(這在託管堆佔一份內存),然後 Unity 把它拷貝到 Native 堆(又佔一份內存)。內存直接翻倍,極其浪費。為什麼還要 LoadFromMemory? 用於加密。如果你不想讓別人在硬盤看到 AB 包內容,你會在內存中解密後再加載。LoadFromMemory 還有用嗎? 基本沒用了。現在只有那種「動態從網絡下載字節流且不存硬碟」的極端安全場景才會用。18k 方案首選偏移量加密,因為它省內存且快。LoadFromFile 随机访问,跳转到指定偏移量后随机同时使用lz4的压缩格式lz4和lzma的区别,lzma压缩率高,但是只能整包解压,解压的时候需要很大的buffer内存, 但是节省网络下载带宽,lz4压缩率低,但是可以分块解压切換場景 Unity 會自動調用 Resources.UnloadUnusedAssets 嗎?不會主動調用。Unity 切換場景(LoadScene)只會自動銷毀當前場景 Hierarchy 裡的 GameObject(託管堆實例)。那些由 AssetBundle.LoadAsset 加載出來的 Native 資源(紋理、網格),如果沒有主動卸載,會一直殘留在 Native 堆。你必須在切場景時手動調用 Resources.UnloadUnusedAssets(),或者更專業的做法是:使用自己寫的 引用計數系統,在切場景時對沒人用的 AB 包執行Resources.UnloadUnusedAssets() 會卸載什麼?會卸載實例化的東西嗎? 不會。它只會卸載 Native 堆 裡的資源(Asset),且前提是:這個資源在 託管堆(C#) 裡已經沒有任何包裝對象(Wrapper)引用它了。例子:如果你 Destroy(gameObject) 了,但沒調用 UnloadUnusedAssets,貼圖還在 Native 堆。調用了,它發現沒人用了,就會把貼圖從 Native 堆幹掉。ab包如果没有unload掉,ab包会hold住这个纹理,导致也无法卸载,ab包也是wrapper5.ab包的引用计数计数代表这个ab包的load次数,load出来一次计数就是1,不被其他ab包依赖的ab包的计数就是1,被其他ab包的依赖的ab包的计数是有多少个依赖他的ab包load了,现在还在它就有多少个计数如果你的代碼裡主動寫了 ABManager.Load(“Texture_C”),計數會變成多少?答案:3。組成:UI_AB (1) + UI_BC (1) + 你手動加載 (1) = 3。結論:你必須手動調用一次 Unload(“Texture_C”),否則即便兩個 UI 都關了,Texture_C 的計數還是 1,它會永久殘留在內存裡(內存洩漏)。循環引用:Unload(true) 能破局嗎?場景:A 依賴 B,B 依賴 A。答案:可以打破,但有順序風險。如果你主動對 A 調用 Unload(true):A 包的鏡像和 A 裡的所有資源會強制銷毀。此時,A 對 B 的引用計數會 -1。如果 B 此時計數歸 0,B 也會跟著銷毀。6.手绘AB包加载内存流向图实例化n份贴图,会造成n个托管堆的内存增加和n个native堆的内存增加Lua重要的方法:__newindex,__index,rawset,rawget__newindex的应用:只读变量,变化时候通知数据(红点系统)__index的应用:继承0.Lua的内存结构Lua中变量存在堆中,堆里面还有一块Lua栈空间,用来存放函数内局部变量只有 在函數內部定義的、生命週期隨函數結束而消失的 i 或 tempTable(指針本身),才可能出現在 Lua 棧 上。函數對象本體在哪?(永遠在堆上)在 Lua 中,函數是一等公民 (First-class Function)。當 Lua 編譯器看到 function()…end 時,它會創建一個 LClosure (Lua 閉包) 結構體。這個結構體包含:字節碼指針、環境指針、Upvalue 列表。真相:這個 LClosure 是一個大對象,它必須存放在 Lua 堆 (Heap) 中,受 GC 管理。local 關鍵字改變了什麼?(引用位置變了)local 決定的是「誰持有這個函數的地址」。全局函數 (function Update):Lua 會在 全局表 (_G)(這張表在堆上)裡存一個 Key 叫 “Update”,Value 指向堆裡的函數對象。局部函數 (local function Update):Lua 會在 Lua 棧 (Stack) 的當前 Slot(槽位)裡存一個指針,指向堆裡的函數對象。優勢:訪問這個 Update 時,虛擬機直接去棧上拿指針(指令是 GETLOCAL),不需要去全局哈希表裡算 Hash 查找(指令是 GETTABUP)。這就是為什麼 local 函數調用更快的根本原因。為什麼 local t = 23 不產生垃圾?结构:struct TValue {int tt; // 類型標記 (Type Tag)Value value; // 數據聯合體 (Union)};對於 Number (23):數據直接存放在 Value 聯合體內。也就是說,23 這個數字就住在 TValue 這個盒子內部。內存行為:當你寫 local t = 23,Lua 只是在棧(Stack)上的一個槽位裡填入了類型標記 LUA_TNUMBER 和數值 23。結論:它不需要在 Lua 堆 上額外申請內存,函數結束棧指針一跳,它就「消失」了。不經過 GC,所以不是垃圾。為什麼 Table 和 String 會產生垃圾?對於這些複雜類型,TValue 盒子裡裝不下它們的本體,只能裝一個「指針(地址)」。對於 Table:TValue 盒子裡存的是 0x12345(指向堆內存的地址)。對於 String:TValue 盒子裡存的是 0x67890(指向字符串駐留池的地址)。過程:Lua 執行 t = {}。Lua 必須在 Lua 堆 上動態申請一塊內存來存放 Table 的 Array 和 Hash 結構。這塊堆內存是獨立於棧之外的。array区和hash区都是存储的tvalue结构當棧上的 TValue 消失後,堆上這塊內存就沒人指著了,它就變成了白色對象(垃圾),必須靠 GC 來回收。1.Lua的GC和内存结构gc触发方式:主动触发(collectgarbage(“collect”)占用内存过高的时候被动触发增量式gc:分帧处理Lua上的变量结构:TValue结构产生gc的来源:字符串拼接: Lua 字符串的 不可變性(Immutability) 和 駐留機制(Interning)。\2.XLua和c#之间的交互Lua中变量存在堆中,堆里面还有一块Lua栈空间,用来存放函数内局部变量與 C# 交互的關聯:當你用 xLua 調用 LuaFunction.Call(obj) 時,xLua 會把 C# 的 obj 壓入 Lua 棧。重點:如果傳的是 Class,Lua 棧裡存的是一個指針;如果傳的是 Struct(如 Vector3),xLua 會產生大量的 Boxing(裝箱),在 Lua 堆上產生臨時對象,這是性能殺手栈交互:就是通过Lua的那个栈,桥接函数在lua调用c#函数的时候把参数c#压到栈上的函数名这一块有使用这个桥接函数桥接函数:wrap是lua调用c#函数的时候用到,静态函数,雙向都有橋接,c#调用lua,通过委托(csharpcalllua)特性,动态generate适配器,调用的时候会执行适配器// 第二步:獲取橋接(這裡會觸發 xLua 生成的適配器)luaAdd = luaEnv.Global.Get(“LuaAdd”);里面的桥接函数wrap,lua调用c#会在lua注册阶段(Initialization): 在游戏启动或加载脚本时,框架(如 xLua/tolua)会将 C# 的函数包装成一个 C 风格的函数指针,这里是指桥接函数的包装xLua 交互的內存拷貝真相Lua 調 C#(傳 Table):Lua 把 Table 指針壓入 Lua 棧。C# 通過 P/Invoke 拿到指針,並封裝成一個 LuaTable 對象。內存拷貝:沒有深拷貝 Table 內容。C# 只是持有了一個指向 Lua 堆的引用。但這會增加 C# 託管堆的對象數量。C# 調 Lua(傳 C# 對象):C# 把對象存入 ObjectPool。把該對象的 ID (IntPtr) 壓入 Lua 棧。Lua 側生成一個 Userdata。內存拷貝:沒有拷貝對象本身。Lua 只是存了一個指針 ID。Lua传Vector3数据到c# 里面去进行一个transform.position会发生什么Lua 传 Vector3 给 C#,会触发 P/Invoke。Vector3 是 值类型,在 Lua 侧通常是一个 Table 或 Userdata。过程:Lua 压栈 - C# 取值 - 产生一个 C# 的 Vector3 结构体。如果是大量的