最近閑來無事,記記unity3D相關的一些知識點吧,也當作筆記存儲。轉載請标明出處:http://www.cnblogs.com/zblade/
1、unity是如何調用Start/Awake等相關函數的?
在unity中,一個常見的問題是awake, start, update等相關函數的執行順序,這個就不在這兒贅述了,一個比較深入的問題,是如何調用這些函數的。如果是虛函數的重載,那麼我們為什麼沒有override關鍵字?我查閱了一下,知乎上有一個相關問題,大概是2個方向的意見。一個是源代碼顯示,unity的mono支援對函數名進行字元串擷取,在擷取到這些函數名後,再進行反射調用。其實說的更淺白一點,就是繼承自mono的類,unity都會對其中的函數進行引用統計,在每幀調用的時候,對于非空的函數,都會執行一次周遊反射回調。很多人覺得反射對于性能影響比較大,其實可以用緩存的方式,在第一次反射執行後,緩存下來,下一次執行的之後可以直接從緩存中擷取,直接執行,是以很多人測試發現反射的性能接近于函數調用,就是這個原理。
2、圖集的原理及使用圖集的原因?
圖集的本質,其實就是一張大圖,将各個圖集中的小圖合并到一張大圖,然後還有一份儲存各個小圖的尺寸、位置、偏移等資訊的資料檔案,是以一般一個圖集會對應2個檔案,當然如果把資料檔案也打包進去,就會隻有一個資料檔案。
使用圖集的原因,有這麼幾點:首先,使用圖集可以友善的管理圖檔資源;其次,在每次繪制一張圖的時候,都會在GPU階段送入一張圖檔進去,這樣的一次操作就會觸發一次DrawCall,如果有幾十上百個圖,那麼每次在轉換的時候,都會觸發多次DrawCall,使用圖集,可以隻觸發一次DrawCall,把所有相關的圖檔都塞入進去,進而大大降低DrawCall;最後使用圖集也友善圖檔資源的加載和解除安裝。
3、lua相關的一些問題
1)lua中table實作的原理
lua對于table的設計,是基于數組和hash共同相容的,對于數組,其主要存儲連續的同類型資料,hash則通過key-value的方式存儲。對于hash和數組,預設大小都是0,然後是1,2,4等等基于2的幂次遞增,由于每次遞增的時候,都會進行一次rehash,是以性能都消耗在rehash上,是以在建立table的時候,盡量避免這樣rehash的操作,比如:
local t1 = {} t["x"] =1, t["y" ] =2 , t["z"] = 3, 這樣三次操作,就會觸發三次rehash,想想原理即可明白
local t2 = {"x" = 1, "y" = 2, "z" = 3}, 這樣隻會觸發一次rehash,對比節省3次性能。
2)請回答lua中對于key值的查找過程
lua對于key值的查找,首先會去數組和hash中查找對應的key值,如果存在,則傳回;如果不存在,則查找該table是否有元表metatable,如果沒有,則傳回nil;如果有,則檢視metatable中是否有__index方法,如果沒有,則傳回nil;如果有,則執行__index[key]查找,傳回對應的值。
3)lua如何執行GC,以及對應的原理和API設定
lua GC的原理,推薦大家閱讀一下雲風的blog中對于GC的詳細過程的闡述,結合源代碼,有較為詳細的講解過程:Lua GC的源碼剖析
這兒做一個讀後筆記記錄吧:
(1) Lua GC對象
lua中一共有9種資料類型,分别為nil, boolean, lightuserdata, number, string, table, function, userdata 和 thread。其中, string, table, function, thread會被GC處理,此外還有proto和upvalue需要被GC處理。
(2) Lua 資料定義方式 union + type
所有的GCObject都有一個相同的資料頭,CommonHeader,其定義為:
這樣所有的GCObject都會被同一個單向連結清單串接起來,每個對象基于tt識别,marked用來标記清除的工作
(3) Lua 對不同類型的清除操作分類
Lua在每次GC清除的的時候,分為多種類型:
對于GCObject,通過若幹根節點開始,逐個直接或者間接的将其上的所有節點左上标記,完成标記後,周遊連結清單,對未被标記的節點執行删除操作;
對于string類型,由于所有的string都放在一張大的hash表中,這樣是為了確定整個lua中同一個string不會被建立兩份,是以其是被單獨管理的,不會被串在GCObject的連結清單中
對于upvalue類型資料,也是一個特殊處理過程,這是由于GC可能分布掃描,由于upvalue是對已有的對象的間接引用,在建立的時候不屬于建立新資料,在mark的過程中需要添加luaC_barrier
對于userdata, 由于userdata都有gc方法,是以會在最後單獨處理逐一周遊所有的userdata來執行其中的gc方法,會有一些特殊的處理
(4) Lua執行GC的幾個流程
Lua執行GC的幾個流程,可以分為5步: GCSpause\ GCSpropagate\GCSsweepstring\GCSsweep\GCSfinalize,從lua5.1 開始就執行分布GC,每次執行可能會有多個狀态切換
GCSpause 為GC階段的啟動流程,标記系統的根節點即可
GCSpropagate 這是标記流程,對尚未标記的對象(灰色連結清單)疊代标記(反複調用propagatemark),否則在atomic函數中執行一次标記
GCSsweepstring 這就是前面提起的對string類型的資料,進行特殊的處理,在這個狀态中,每步都會清除string的hash表中的一列
GCSsweep 和上一個狀态類是,不過這步操作的對象是GCObject
GCSfinalize 在這兒主要對userdata執行,如果需要調用其gc,則執行gc操作,由于userdata的對象和關聯資料不會在之前的清除階段被清除,是以其實際清除會在下一次的GC清除中執行或者在lua_close中被清除:lua_close的工作就是簡單的處理所有userdata的gc元方法,以及釋放其所用到的記憶體。
(5) Lua GC的标記流程
Lua對于所有的GCObject都設定一個顔色,最開始是白色,建立的節點也是白色,然後在标記階段,可見的節點被設定為黑色,如果某些節點關聯其他節點,在沒有處理完其關聯節點前,都被标記為灰色,對于顔色的标記,其存儲在CommonHeader的8位的marked域中,對于白色有兩個白色的标記位,采用一種乒乓開關,避免在标記完成後,清理沒有完成前,對象間關系發生變化的時候,某些不需要被清理的節點,就可以從一種類型的白色轉換到另一種類型的白色中,比如目前删除0型白色,那麼轉換到1型白色,這樣1型白色就會被保護起來不會被删除,反之亦然。具體對于8個位的定義和使用,可以看雲風的原文,有一定講解。
(6) Lua GC的操作
常用的幾個API: luaC_fullgc\ luaC_step\luaC_checkGC
luaC_fullgc: 執行一次完整的gc動作,對于可能執行一般的流程,在走完一次流程後,會阻塞狀态再次執行一遍gc,對于已經執行的前半程gc,其實不需要做清除操作,隻需要做狀态回複
luaC_step: 其核心在與調用singlestep函數,通過設定gcstepmul值,可以設定步長,進而影響gcthreshold,其實步進量的設定,是一個經驗值
luaC_checkGC: 自動GC的接口,在大部分導緻記憶體增長的api中會調用該方法,自動GC,可能會在某一個周期性中将衆多臨時對象也mark了,造成系統的峰值記憶體占用比實際需求大,可以在這種周期性調用中采用gcstep的方法,同時設定較大的data量,使得有限周期做一個完整的gc。
(7) Lua GC的mark操作
對于Lua的mark操作,主要操作的API: markroot\ reallymarkobject\remarkupvals\atomic\iscleared
(8) Lua GC的write barrier操作
主要的API: luaC_barrier\luaC_barriert\luaC_objbarrier\luaC_objbarriert
(9) Lua GC的剩餘操作 sweep/finalize
sweep的操作分為 GCSsweepstring 和GCSseep, 貼2個源碼:
對于seeplist,其源代碼為:
基本看源代碼就可以了解,對于dead的freeobj,沒有dead的則執行makewhite,最後一個流程就是GCS finalize,通過GCTM函數執行,每次調用一個需要回收的userdata的gc元方法:
在回收的時候,設定較大的GCthreshold來避免GC的重入
4、C#的GC原理和機制
對于上面的Lua的GC的原理,在閱讀了源碼後,可以進一步的衍生到C#的GC機制和原理,找到一篇超級贊的博文:c#技術漫談之垃圾回收機制(GC),拜讀大牛對于GC的詳細漫談,對GC也有一個初步的學習和了解,這兒記下來,用作後續的閱讀。對于C#的托管和非托管資源,可以一圖解釋:

1) 使用記憶體托管的原因
(1) 提高軟體的開發速度(無需陷入記憶體管理中);(2) 降低子產品耦合,使得接口更清晰;(3) 提高記憶體管理的效率;
2) GC 的定義
garbage collection , 以應用程式的root為基礎,周遊應用程式在堆上動态配置設定的所有對象,識别其是否被引用來确定其是否死亡還是被引用,對于不再引用的對象或者整個root,都标記為垃圾,然後執行回收。主要的算法有 Reference Counting\Mark Sweep\ Copy Collection, 目前主流的.NET CLR, JAVA VM都是采用Mark Sweep的算法
3) Mark Sweep-Compact 算法
階段1:Mark sweep标記清除階段,先假設heap中所有對象都可以回收,然後找出不能回收的對象,給這些對象打上标記,然後heap中沒有被打标記的對象都是可以被回收的;
階段2:compact階段,對象回收之後heap記憶體空間變得不連續,在heap中移動這些對象,使得其從heap的基位址開始連續排列,類似于磁盤空間的碎片整理,然後将heap的指針指向壓縮後的起始位置,便于下次記憶體配置設定;
操作流程: 線程挂起-> 确定roots->建立reachable objects graph-> 對象回收->heap 壓縮->指針修複
roots: 就是CLR在heap之外可以找到的各個入口點,一般在全局變量、靜态變量、局部對象、函數調用參數、目前CPU寄存器中的對象指針、finalization queue中,可以分為已經初始化了的靜态變量、線程仍在使用的對象;
指針修複:由于heap的壓縮,對象的位址發生變化,需要修複所有引用指針,包括stack\CPU register中的指針\heap中其他對象的指針,copy原文中的圖檔:
4)Generational 分代算法
分代算法,将對象按照生命周期分成新的、老的,根據統計分布規律所反應的結果,對新老區域采用不同的回收政策和算法,加快回收速度,其基本假設為:
(1) 新建立的對象生命周期都較短,較老的對象生命周期會更長;
(2) 對部分記憶體回收會比全記憶體回收更快;
(3) 新建立的對象之間關聯較強,記憶體配置設定是連續的,其基本操作如原文中圖:
heap分為三個代,對應三種GC方式:#Gen 0 collection #Gen 1 collection #Gen 2 collection, 對應的頻率可以設定為1:10:100
5) Finalization Queue \Freachable Queue
這兩個隊列會用來存儲對象的指針,當程式中new一個對象建立在heap上,在GC的時候會對對象進行分析,如果其中含有Finalize方法,則會在Finalization Queue中添加執行該對象的指針,在GC的時候,會将這個對象從垃圾中分離出來,然後将其從Finalization Queue中移到Freachable Queue中,這個過程就是對象的複生。當被添加到Freachable Queue中後,就會觸發對象執行Finalize方法,然後将指針從隊列中移除,這時候整個對象可以安靜的go die了。
System.GC類提供兩個控制Finalize的方法,ReRegisterForFinalize和SuppressFinalize,前者請求系統完成對象的Finalize方法,後者請求系統不要完成對象的Finalize方法。
對于非托管的資源,主要采用Dispose方法來進行主動釋放其中的托管和非托管資源,最後摘抄一下作者的總結:
GC注意事項:
1) 隻管理記憶體,非托管資源,如檔案句柄,GDI資源,資料庫連接配接等還需要使用者去管理。
2) 循環引用,網狀結構等的實作會變得簡單。GC的标志-壓縮算法能有效的檢測這些關系,并将不再被引用的網狀結構整體删除。
3) GC通過從程式的根對象開始周遊來檢測一個對象是否可被其他對象通路,而不是用類似于COM中的引用計數方法。
4) GC在一個獨立的線程中運作來删除不再被引用的記憶體。
5) GC每次運作時會壓縮托管堆。
6) 你必須對非托管資源的釋放負責。可以通過在類型中定義Finalizer來保證資源得到釋放。
7) 對象的Finalizer被執行的時間是在對象不再被引用後的某個不确定的時間。注意并非和C++中一樣在對象超出聲明周期時立即執行析構函數
8) Finalizer的使用有性能上的代價。需要Finalization的對象不會立即被清除,而需要先執行Finalizer.Finalizer,不是在GC執行的線程被調用。GC把每一個需要執行Finalizer的對象放到一個隊列中去,然後啟動另一個線程來執行所有這些Finalizer,而GC線程繼續去删除其他待回收的對象。在下一個GC周期,這些執行完Finalizer的對象的記憶體才會被回收。
9) .NET GC使用"代"(generations)的概念來優化性能。代幫助GC更迅速的識别那些最可能成為垃圾的對象。在上次執行完垃圾回收後新建立的對象為第0代對象。經曆了一次GC周期的對象為第1代對象。經曆了兩次或更多的GC周期的對象為第2代對象。代的作用是為了區分局部變量和需要在應用程式生存周期中一直存活的對象。大部分第0代對象是局部變量。成員變量和全局變量很快變成第1代對象并最終成為第2代對象。
10) GC對不同代的對象執行不同的檢查政策以優化性能。每個GC周期都會檢查第0代對象。大約1/10的GC周期檢查第0代和第1代對象。大約1/100的GC周期檢查所有的對象。重新思考Finalization的代價:需要Finalization的對象可能比不需要Finalization在記憶體中停留額外9個GC周期。如果此時它還沒有被Finalize,就變成第2代對象,進而在記憶體中停留更長時間。
5、unity中協程的了解
協程的本質是一個分部執行函數,在unity的mainThread中執行,unity在每幀的更新中,都會執行各個協程調用,分别在FixedUpdate和LateUpdate之後的一些協程調用上,其本質就是一個疊代器,當遇到條件不滿足的時候會被挂起,條件滿足的時候,會被喚醒來繼續執行。舉個在其他地方看到的例子吧,這樣便于講解過程:
會輸出什麼的順序:a1, b1, b2, a2
執行的順序是先輸出a1,然後執行Test2,輸出b1,這時候遇到yield return null ,被挂起,在下一幀,被喚醒,繼續執行,輸出b2, 接着執行輸出a2
如果對這個過程了解了,那麼協程基本就沒問題了
6、unity中meta檔案的作用
有兩個作用,第一是包含了目前資源(代碼或者prefab, 圖檔等)在目前工程中唯一的guid, unity擷取資源是依據guid來擷取的,是以每個資源都會附帶生成一份meta檔案;
第二個,包含了目前資源的導入資訊,比如圖檔資源,會包含一下bump等相關的資訊
7、unity UGUI的自适應方案設計
這是很多遊戲中都會需要處理的一個問題,現在晚上的解決方案有這樣的示例:UGUI的自适應方案,這裡面簡單的講解了一下Canvas和Canvas Scaler的設定,其中對于Canvas設定為Scree Space-Camera的render mode,對于canvas scaler,設定UI Scale Mode為Scale With Screen Size, 然後填寫對應的resolution的width 和height, Screen Match Mode 示例中設定為Match Width or Height, 對于這種比對方式,其Match值為0的時候是依據Width來比對,Match值為1的時候,是依據Height來比對。不過一般的工程中,都是采用Expand的比對方式,具體的流程我寫一份僞代碼來闡述:
1)首先判斷目前系統的平台,主要分為ios/android/PC三種主流平台;
2)針對不同的平台,讀取不同的SystemInfo的參數來設定目前應該設定的renderLevel,具體的參數讀取和判斷,每個項目可能設定的不一樣;
3)根據擷取的renderLevel,再來設定目前Screen的width和height, renderLevel主要分為高,中,低三個檔次
4)對于高中低三個檔次,分别不同的處理,
高,直接将目前螢幕的width和height設定為最終的width/height;
中,則根據目前讀取的螢幕的height來做不同的設定
低,則将目前螢幕的width/height減半;
對于中低兩個檔次的設定,最後還需要執行一次adjust,避免width低于最低width,然後對比初始width/height和計算後的width/height的比值大小,做對應的width或者height的調整;
最後,都調用Screen.SetResolution(width, height, Screen.fullScreen)這個接口來實作自适應的比對
8、c#的虛函數的調用
在查找這個問題的時候,找到一篇非常贊的博文:c#之虛函數
摘用作者的幾句話,詳盡的解釋了虛函數的特點和執行過程:
虛函數的特點:
虛函數前不允許有static\abstract\override等修飾字,不能私有(private不能有)
虛函數的執行:
一般函數在編譯時期就靜态的編譯到執行檔案中,其相對位址在程式運作期間是不會發生變化的;
虛函數在編譯期間不被靜态編譯,其相對位址不确定,而是根據運作時期對象執行個體來動态判斷要調用的函數。
1)當調用一個對象的函數時,首先檢測該對象的申明類,看該方法是否為虛函數;
2)如果不為虛函數,則直接執行該函數,如果為虛函數,則檢測該對象的執行個體類;
3)檢測執行個體類中是否有實作該虛函數或者重新實作該虛函數(override),如果有,則執行該虛函數,否則繼續查找該執行個體的父類,知道找到第一個重載或者實作了該虛函數的地方,執行該虛函數。
未完待續,持續更新ing