Unite 2016 針對移動設備端的Unity應用優化
今天參加Unite 2016聽下來最有收貨的一個talk,雖然一半以上都是老生常談…整理如下
ps. 我個人覺得比較有價值的在於資源審查這一部分,關於各類資源的常用方法都提出了一些很有實用價值的建議和規範
如何獲得足夠好的數據
iOS: Instruments
- XCode自帶的免費工具
- 對Unity IL2CPP編譯出的代碼使用起來完全沒問題
- 移動CPU性能優化的最佳工具
- 優化啟動時間的最佳工具
理解Instruments結果(遊戲循環中的重要函數):
- BaseBehaviourManager::CommonUpdate
- Update, FixedUpdate和LateUpdate的回調
- PhysicsManager::FixedUpdate
- PhysX模擬,OnCollision*和OnTrigger*的回調
- 如果使用了2D物理,還會有Physics2DManager::FixedUpdate
- DelayedCallManager::Update
- 恢復運行的協程
- PlayerRender
- 繪製命令
- 批次
- MonoBehaviour::OnWillRender
- 圖像後處理效果回調(我猜Camera.OnRenderImage)
- UI::CanvasManager::WillRenderCanvases
- 重新批次UI canvas
- 生成字體紋理等
- EnlightenRuntimeManager::Update
- Enlighten, 預計算實時GI,反射探針
當某些函數不是一次執行完,而是分散多次的時候(譬如協程),嘗試直接搜索方法名,例如:
- ::Box, Box(和_Box
- String_
Android
- VTune
- Snapdragon Profiler
Unity Editor: Timeline
Unity 5.3: Memory Profiler
- 代碼在Bitbucket
- 拖到Assets里任一Editor文件夾下
- 在編輯器Window-MemoryProfilerWindow打開
- 通過Profiler窗口連上Unity Profiler
- 點擊Take snapshot
如果發現兩個紋理名字相同,但是InstanceID不同,基本上就是紋理在內存里重複出現了…
常見的最佳實踐
資源審查
理由: 避免錯誤
- 開發者都是人類(大概)
- 是人就會犯錯
- 錯誤就會增加開發時間
用工具來規避常見但是代價大的錯誤顯然非常划算…
常見錯誤
- 瘋狂的紋理尺寸
- 資源壓縮
- 錯誤的Avatar/Rig設置
當然,就算在同一個項目里,不同部分的資源的標準要求是不一樣的~
HOWTO(如何實現)
參考AssetPostprocessor,根據項目需要修改assetImporter實例
常見規則
- 紋理
- 確認關閉Read/Write
- 儘可能禁用mipmap
- 儘可能使用壓縮紋理
- 確保紋理不要過大:UI來說用2048或者1024;模型紋理不超過512
- 模型
- 確認關閉Read/Write
- 非玩家模型就關掉rig
- 共用rig的模型就直接複製avatar
- 打開模型壓縮
- 音頻
- iOS使用mp3壓縮
- Android使用Vorbis壓縮
- 移動設備上Force Mono
- 儘可能降低比特率
常見的問題及解決方案
內存相關
Managed Memory: 堆(Heap)里包含了資源(Assets)和腳本(Scripts)里的東西(objects)。
當通過代碼申請的時候,會分配更多的內存,如int[] someNumbers = new int[2048]
垃圾回收會周期性的運行,刪除沒用的東西 GC.Collect()
需要注意:刪除掉後釋放的內存不一定能被再次使用,也就是所謂的內存碎片化。
現在問題來了:
- Unity中的heap只會增長,不會縮小
- iOS和Android中依然有保留頁(reserved pages)
以上兩點帶來的結果就是,堆里無用的區域(已經被回收器幹掉了)依然會被保留,但是又被清除出當前的保留頁~
- 臨時的內存申請非常不好
- 如果一個遊戲是60FPS,每幀申請1kb內存
- 也就是一秒60kb
- 如果每分鐘才運行一次垃圾回收(因為這事很影響幀率)
- 那麼總的需要3600kb內存…
優化內存使用
通過Unity Profiler里的GC Alloc一列,可以看到具體的內存申請。在用戶操作應用的時候,儘可能讓其接近0。(當然了,如果是載入資源就沒事)
- 儘可能重用集合(例如Lists, HashSets)
- 避免字元串拼接,可以考慮重用StringBuilder來完成
- 避免匿名函數和閉包
裝箱問題(Boxing)
當將值類型當做引用類型傳入時,會在堆頂臨時分配一個值來用
int x = 1;object y = new object();y.Equals(x); // Boxes "x" onto the heap
Foreach
當循環開始時會申請一個Enumerator,這也是廣為人知的Mono的鍋了…別這麼寫就行。
Unity API
- 如果引擎返回的是一個數組的話,它每次都會生成拷貝
- 每次被訪問的時都這樣,就算不修改裡面的值!
這個錯誤代碼藥丸,每次都申請非常多的Touch[]數組
for(int i = 0; i < Input.touches.Length; i++){ Touch touch = input.touches[i]; // ...}
正確的代碼就只有一份
Touch[] touches = Input.touches;for(int i = 0; i < touches.Length; i++){ Touch touch = touches[i]; // ...}
CPU性能
XML, JSON和其他文本格式
- 解析文本非常慢
- 避免基於反射的解析器——因為太TM慢了!
- 5.3開始可以用自帶的JsonUtility類
解決本問題有三個策略:
- 壓根不要解析文本格式,利用ScriptableObject二進位保存數據,可以保存一些很少變化的數據;
- 做更少的活,譬如將數據分成小塊,每次只解析需要的部分,解析完了之後保存到緩存中;
- 線程: 只能用於處理純C#邏輯,任何涉及Unity資源的都沒法做,而且寫的時候要非常非常小心…
Resources文件夾
在遊戲啟動的時候會載入Resources文件夾的目錄結構,這個是無法避免或者延後的~
解決方案:將Resources下的資源打包到Asset Bundle
Material/Animator/Shader屬性訪問
永遠不要直接通過名字去訪問,因為在引擎內部需要對字元串名字計算哈希得到一個整數id~
錯誤做法
material.SetColor("_Color", Color.white);animator.SetTrigger("attack");
正確做法是一開始啟動的時候計算一次哈希,然後緩存下來…
static readonly int material_Color = Shader.PropertyToID("_Color");static readonly int anim_Attack = Animator.StringToHash("attack");material.SetColor(material_Color, Color.white);animator.SetTrigger(anim_Attack);
裝箱 字元串操作
因為這些操作太慢了…所以不得不再強調下!
RegExps, String.StartsWith和String.EndsWith 都是非常非常慢的~ 在Instruments你可以搜索::Box _Box String_看下~
不過官方的人也說了,對於Boxing這個問題,開發者沒啥辦法能解決這個問題…
天滅Mono,快抱微軟.Net大腿吧!
推薦閱讀:
※從零開始學基於ARKit的Unity3d遊戲開發系列6
※比預想的更複雜——動態斷肢實現
※Lua性能優化(一):Lua內存優化
※Unity/C++混合編程全攻略!——基礎準備
※UWA 兩周年慶活動第一彈!四場技術直播領跑六月充電季!