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類

解決本問題有三個策略:

  1. 壓根不要解析文本格式,利用ScriptableObject二進位保存數據,可以保存一些很少變化的數據;
  2. 做更少的活,譬如將數據分成小塊,每次只解析需要的部分,解析完了之後保存到緩存中;
  3. 線程: 只能用於處理純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 兩周年慶活動第一彈!四場技術直播領跑六月充電季!

TAG:Unity游戏引擎 | 游戏开发 | 优化 |