概述
項目簡介
由於是公司內做的項目,不方便開源,就只分享優化過程吧。
項目信息
逐日是一個移動端單機小游戲,使用Unity開發,目前已將項目使用的Unity升級到2019.4.14f1c1 (3e5991a5f6ba)版本。
項目內容
在進行優化前,項目資源目錄如下,可以看到,項目目錄命名雜亂,包含很多需求迭代產生的舊資源、無用場景、未壓縮的音視頻等內容。
由於這次主要是對於安裝包大小的一些嘗試,所以就不會特別關注游戲邏輯,整體能加載完成,不Crash就OK,沒有對游戲邏輯上花費過多精力,后面的過程中可能有小部分圖被壓糊了,有視頻無法加載等問題,不影響縮包的最終結果。


未優化的包體大小
如圖所示,使用新版UnityEditor直接打包之后,生成的游戲Apk大小為218MB,這對於一個只有兩個關卡的小游戲而言,明顯過於臃腫了,假如上架到GooglePlay之后,光下載內容所消耗的時間和流量就勸退了一部分用戶(更何況,現在GooglePlay限制了安裝包大小不能大於100M!)。所以,對於安裝包大小的優化是勢在必行的。
第一步:Assembly構建優化
使用IL2CPP代替Mono
- Mono使用即時(JIT)編譯,並在運行時按需編譯代碼。
- IL2CPP使用提前(AOT)編譯並在運行之前編譯整個應用程序。
使用IL2CPP進行構建有助於提高運行速度,並減少包體大小,IL2CPP的工作流程如下圖:
將ScriptingBackend設置為IL2CPP后,執行構建得到的安裝包大小為210MB,比使用Mono少了8MB(這個項目里游戲邏輯不多,腳本比較少,對於較大的項目來說,使用IL的優勢更大)
裁剪更多未使用的代碼(不是特別推薦)
Unity有提供一個Managed Stripping Level選項,可以在構建時裁減掉未使用的代碼,這個選項又是那個可選級別,IL2CPP不能關閉這個選項並且默認級別為Low。不同的裁剪級別對應的規則不同,裁減掉游泳的代碼可能性越高,並且有可能無法檢測到通過反射引用其他代碼的情況,導致程序崩潰。
設置Managed Stripping Level=High后,安裝包大小為209M。
關於腳本構建優化
對於Android Platform,我對於一個空的Unity3D項目進行過如下打包測試:
| 構建設置 | ApkSize |
|---|---|
| 默認(Mono_Release_DotNetStandard2.0) | 16.9 MB |
| ScriptBackend使用IL2CPP | 6.38 MB |
| StrippingLevel=High | 6.02 MB |
| CompressMethod=LZ4HC | 5.96 MB |
| GraphicApi只使用OpenGLES2(Android 2.2+都支持) | 5.73 MB |
再往下就很難減小了......
第二步:資源優化
分析資源大小的利器
在Editor中構建完成后,可以打開EditorLog,里面列出了在未壓縮的情況下,不同類型資源的占比以及每個資源體積從大到小的排列,我們可以直接根據Log查找哪些資源比較大,針對性地處理:
移除未使用的資源
為什么把這一步放在資源優化的第一位?因為先去掉無用的資源后面打包會快很多嘛。。。測試一個屬性就要重新打個包有時候甚至需要重新Import整個項目半小時的時間傷不起啊~
在GitHub上隨便找了個資源清理工具UnityAssetCleaner,對項目中的無用資源進行了清理:
這個步驟直接讓Assets目錄的大小從1.27GB減小到192MB,由於資源沒有放在Resource下,所以未引用的資源並沒有打包到Apk中,這個步驟之后僅僅讓包體大小減少到了208MB。
但是重新Import項目會快很多啊有木有~!這也看出來當時做的時候走了多少彎路。。。
去掉無用的場景
第一次用Unity做游戲時,關卡切換、各種UI面板都是使用獨立的場景做的,明明可以用Prefab的> _ <
看了下游戲里的場景,沒有需要刪掉的。。。如果有無用的場景,刪除掉也可以減少包體大小。
紋理尺寸壓縮、視頻音頻質量壓縮
在項目中,盡可能降低圖片和音視頻的質量,使用低質量高壓縮率的壓縮格式,不僅內存占用低、性能也更好;當出現質量不滿足要求時,再逐步的提升尺寸和壓縮格式來滿足需要。Link→官方文檔:不同平台下支持的壓縮格式
要在不修改源文件的情況下修改尺寸,可以直接在Unity里調整:
如上圖所示,游戲里的紋理都是按照最大尺寸來導入的,並且由於使用的是Sprite動畫,導致一個動畫就要使用很多紋理,所以首要的問題是把不同的圖片調整到適當的尺寸,比如場景地圖使用2048或1024,較大的Boss角色和樹木使用512,小怪、小火球等紋理使用256或更小。
音效以及視頻,也按照類似的方法,適當調低分辨率和碼率(視頻其實可以直接放CDN)。


調整完之后,重新打了個包,emmm...只有52MB了(當然,有一小部分紋理被我壓糊了,適當調整之后最終結果其實相差不大)。
Sprite Atlas
多個NPTO的紋理拼合到一起組成一個POT型的圖集有利於Unity壓縮圖片,更能節省空間。創建方式如下(Unity2019+版本默認不使用給紋理打Tag的方式創建圖集,而是需要手動創建)
將游戲中大小、用途類似的紋理,添加到各自的圖集中,重新打包,包體大小33M。

AssetBundle打包
這可能是縮小Apk大小的最好方案了......
總體流程概述
如果前面的一系列縮包方法都不能達到想要的效果,就可以考慮把資源單獨打包拿出來,用戶啟動App時進行下載。
首先在Unity里給資源打標簽,把資源分到對應的Bundle中。

然后執行構建AssetBundle腳本(腳本官網文檔有提供),把AssetBundle包輸出到指定路徑。
詳細過程
把場景使用Prefab代替
在優化的過程中我發現,Unity通過BuildSetting中的場景列表來查找所有依賴的資源,所以為了打出來獨立的AssetBundle包並斷開AssetBundle內的資源與Apk之間的依賴關系,就需要對場景做優化。
將場景中的內容簡單粗暴地整合到一個GameObject內,制作成Prefab,用掛載Prefab的方式代替掛載場景,並刪掉原來的場景,清理BuildSetting中的場景列表。這樣就斷開了AssetBundle內的資源與Apk之間的依賴。
實現入口腳本
我的辦法是創建一個Init場景作為整個App的入口,里面只掛一個GameFramework腳本來執行一系列必要的AssetBundle加載過程、管理各個用於代替場景的Prefab。
這樣修改之后,我們構建出來的Apk就只會包含一個空的場景,以及C#腳本構建產生的dll,大小最小能有前面所說的5MB!!!
打AssetBundle包
前面我們把資源和Apk之間的依賴關系切斷了,但是我們還是要想辦法把它們動態加載回去的,這種方法就是AssetBundle包。
給資源打標簽的過程是非常繁瑣的,所以就使用腳本自動化處理了,會自動給ResForBundle下的直接子目錄按照目錄名遞歸添加標簽。
運行了BuildAssetBundle命令之后,我們就能在輸出目錄得到一系列相互依賴的AssetBundle文件了(也包括他們的manifest描述文件)。
動態加載AssetBundle
我們可以使用Unity提供的AssetBundle Api來動態把AssetBundle文件加載到內存里,並根據路徑讀取它們包含的資源,由於游戲內的資源比較少,大約20~30MB,所以我就直接放在StreamingAssets目錄下了(關於這個目錄,是和Resource一樣特殊的存在,感興趣的話可以直接查一下)
在GameFramework啟動時就加載所有的AssetBundle,並掛載Scene_Start.prefab到場景中,這樣我們的游戲流程又能正常走下去了。
當然這個過程中還有好多好多細節要處理:
比如需要用宏來判斷是在編輯器中還是在移動端,從而采用不同的自定義ResourceLoad來加載資源(如果是編輯器的話直接使用AssetDatabase Api通過路徑加載資源,移動端則采用上面提到的AssetBundle內的資源);
如何遞歸地給指定目錄下的內容添加標簽並自動維護它們;
優化結果
對場景進行精簡優化,只保留一個入口場景加載AssetBundle和加載入口,構建后,Apk大小只有7M。
打出的AssetBundle可以在App啟動時通過http拉取到本地,如果不嫌資源包大的話也可以直接放在StreamingAssets目錄打到apk內。
縮包歷程
直接上圖好了:

KeyWorkds:Unity安裝包體積優化,Unity減小安裝包體積,Unity構建更小的Apk,Unity減小構建的安裝包大小,Unity縮小安裝包
