此篇文章准備了將近兩周的時間,寫了改,改了刪。之前有朋友反饋,上一個文章太冗長了,影響閱讀體驗,這一講就走個精簡路線。所以只要不是很重要的內容就都刪減掉了。
文章分兩個部分,第一部分是原理,第二部分是實戰。
原理部分,初學者盡量去理解就好,不用跟着敲,列出的代碼都是示意代碼。
實戰部分是留給初學者的,希望敲完代碼,這樣有助於理解前邊的原理。
當然原理不是很難。
第一部分:原理
ResKit 中值得一說的 Feature 如下:
- 引用計數器
- 可以不傳 BundleName
當然也有很多不值得一提的 Feature。
-
比如統一的加載 API,一個 ResLaoder.LoadSync API 可以加載 Resources/AssetBundle/PersistentDataPath/StreamingAssets,甚至是網絡資源。這個很多開源方案都有實現。
-
Simulation Mode,這個功能最早源自於 Unity 官方推出的 AssetBundleManager,不過不維護了,被 AssetBundleBrowser 替代了。但是這個功能真的很方便就加入到 ResKit 里了。
-
對象池,是管理內存的一種手段,在 ResKit 中作用不是很大,但是是一個非常值得學習的優化工具,所以也列到本次 chat 里了。
接下來就按着以上順序聊聊這些 Feature。
首先先看下 ResKit 示例代碼。
// 在加載 AssetBundle 資源之前,要進行一次(只需一次) 初始化操作
ResMgr.Init();
// 申請一個加載器對象(從對象池中)
var loader = ResLoader.Allocate<ResLoader>();
// 加載 prefab
var smObjPrefab = loader.LoadSync<GameObject>("smObj");
// 加載 Bg
var bgTexture = loader.LoadSync<Texture2D>("Bg");
// 通過 AssetBundleName 加載 logo
var logoTexture = loader.LoadSync<Texture2D>("hometextures","logo");
// 當 GameOject Destroy 時候進行調用,這里進行一個資源回收操作,會將每個引用計數器減一。
loader.Recycle2Cache();
loader = null;
最先登場的是 ResMgr。
ResMgr.Init() 進行了讀取配置操作。在游戲啟動或者是熱更新完成之后可以調用一次。
第二登場的是 ResLoader。字如其意,就是資源加載器。用戶大部分時間都是和它打交道。
ResLoader.LoadSync 默認加載 AssetBundle 中的資源。
如果想加載 Resources 目錄下的資源,代碼如下所示:
var someObjPrefab = loader.LoadSync<GameObject>("resources://someObj");
當然也可以擴展成支持加載網絡資源等。
這里關鍵的一點是 resources://
這個前綴。和 https://
或 http://
類似,是一個路徑,和 www
的 file://
的原理是一樣的。
實現比較簡單。核心代碼如下:
public static IRes Create(string assetName)
{
short assetType = 0;
if (assetName.StartsWith("resources://"))
{
assetType = ResType.Internal;
}
else if (assetName.StartsWith("NetImage:"))
{
assetType = ResType.NetImageRes;
}
else if (assetName.StartsWith("LocalImage:"))
{
assetType = ResType.LocalImageRes;
}
else
{
var data = ResDatas.Instance.GetAssetData(assetName);
if (data == null)
{
Log.E("Failed to Create Res. Not Find AssetData:" + assetName);
return null;
}
assetType = data.AssetType;
}
return Create(assetName, assetType);
}
這里除了支持 resources://
還支持網絡圖片 NetImage:
和本地圖片 LocalImage:
的加載。
同樣也可以支持自定義格式,通過特殊的前綴去決定資源如何加載、從哪里加載、異步還是同步等等,一個靈活的系提供應該如此。這里不多說,文章的下半部分的實戰環節仔細講解。
接下來說一個比較重要的概念,引用計數。
雖然在以上的代碼中看不出來引用計數的影子,但是他是整個 ResKit 的核心。
先進行一個簡單的入門。貼上筆者的專欄:Unity 游戲框架搭建 (二十二) 簡易引用計數器。文章不長,足夠入門。
在講引用計數在 ResKit 中的應用之前,還要再重新介紹一次 ResMgr。
在開頭介紹了 ResMgr.Init 進行了讀取配置操作。配置文件中則記錄的是 AssetBundle 和 Asset 信息。
那 ResMgr 的職責是什么呢?
字如其意就是管理資源。
以筆者的習慣,稱為 XXXMgr 或者 YYYManager 的都是一個容器,叫容器是因為里邊維護了 List 或 Dictionary 等,用來對外提供查詢 API 和進行一些簡單的邏輯處理。
這里的 ResMgr 就是 Res 的容器。Res 是一個類,它持有一個真正的資源對象(UnityEngine.Object)。有的 Res 是從 Resources 里加載進來的,在 ResKit 中叫做 InternalRes。有的 Res 從 AssetBundle 里加載進來的,這里叫做 AssetRes/AssetBundleRes。它們都繼承一個共同的父類 Res,之間的區別只是加載的方式不同。
代碼如下:InternalRes.cs
public class InternalRes : Res
{
...
public void LoadSync()
{
mAsset = Resources.Load(AssetName);
}
...
}
AssetRes.cs
public class AssetRes : Res
{
...
public void LoadSync()
{
mAsset = mAssetBundle.LoadAsset(AssetName);
}
...
}
這里每個 Res 都實現了引用計數器。那么在什么時候進行引用計數+1(Retain)操作呢?
ResMgr 對 ResLoader 提供一個 GetRes API,當 ResLoader 調用它的時候會對獲取的 Res 進行 Retain 操作。
這里了解下 ResLoader 加載一個資源的步驟就知道了。
var smObjPrefab = loader.LoadSync<GameObject>("smObj");
以上這步操作做了一下事情:
- 判斷 loader 里的
List<Res>
中有沒有加載過 Name 為 "smObj" 的 Res,如果加載過直接返回這個資源。 - 接着通過 GetRes 這個 API 從 ResMgr 獲取這個資源,獲取之后對這個 Res 進行 Retain 操作,之后將 Res 保存到 loader 的
List<Res>
中,之后再返回給用戶。 - 在 ResMgr.GetRes 中,先判判斷容器中有沒有該 Res,如果沒有就創建一個出來加入到容器中,再返回給 ResLoader。
理解起來不難,代碼如下:
ResLoader.cs
public class ResLoader
{
/// <summary>
/// 持有的
/// </summary>
private List<Res> mResList = new List<Res>();
public T Load<T>(string assetName, string assetBundleName = null) where T : Object
{
var loadedRes = mResList.Find(loadedAsset => loadedAsset.Name == assetName);
if (loadedRes != null)
{
return loadedRes as T;
}
loadedRes = ResMgr.GetRes(assetName, assetBundleName);
mResList.Add(loadedRes);
return loadedRes.Asset as T;
}
...
}
ResMgr.cs
public class ResMgr
{
/// <summary>
/// 共享的
/// </summary>
public static List<Res> SharedLoadedReses = new List<Res>();
public static Res GetRes(string resName, string assetBundleName)
{
var retRes = SharedLoadedReses.Find(loadedAsset => loadedAsset.Name == resName);
if (retRes != null)
{
retRes.Retain();
return retRes;
}
if (resName.StartsWith("resources://"))
{
retRes = new ResourcesRes(resName);
}
else
{
retRes = new AssetRes(resName, assetBundleName);
}
retRes.Load();
SharedLoadedReses.Add(retRes);
retRes.Retain();
return retRes;
}
...
}
到這里,總結下:
- ResLoader 中有個 ResList,用來緩存從 ResMgr 中獲取的 Res。也就是用戶加載過的資源都會在 ResLoader 中緩存一次,這里緩存的當然只是這個對象,並不是真正的資源。用戶可以同 Res 對象訪問真實的資源。
- ResMgr 中有個 SharedLoadedReses,用來存儲共享的 Res。
- 所以 ResMgr 中的 Res 會共享給,每個 ResLoader。
- ResLoader 是 ResMgr 的一個分身,只不過每個分身從 ResMgr 獲取資源,都會對資源進行 Retain 操作。
什么時候進行資源的 Release 操作呢?
就是當 ResLoader 調用 Recycle2Cache 時:
public class ResLoader
{
...
public void Recycle2Cache()
{
foreach (var asset in mResList)
{
asset.Release();
}
mResList.Clear();
mResList = null;
}
}
代碼比較簡單了。就是遍歷 List<Res>
,對每個 Res 進行一個 Release 操作。當 每個資源引用計數 Release 到 0 的時候會調用對應的 Unload 操作。各個 Res 子類之間的 Unload 的區別 LoadSync 一樣。這里不介紹了。
下面介紹下這種設計的優點。
ResKit 中提倡每個需要動態加載的 MonoBehaviour 或者單元都申請一個 ResLoader 對象。當該 MonoBehaviour 進行 OnDestroy 時,調用 Recycle2Cache。
比較理想的使用方式如下:
public class UIHomePanel : MonoBehaivour
{
private ResLoader mResLoader = ResLoader.Allocate();
void Start()
{
var someObjPrefab = mResLoader.LoadSync<GameOBject>("someObj")
// 加載 Bg
var bgTexture = mResLoader.LoadSync<Texture2D>("Bg");
// 通過 AssetBundleName 加載 logo
var logoTexture = mResLoader.LoadSync<Texture2D>("hometextures","logo");
}
void OnDestroy()
{
mResLoader.Recycle2Cache();
mResLoader = null
}
}
在該 Panel Destroy 時,進行資源的引用計數減一操作。這里加載過的資源並不一定真正進行卸載,而是當引用計數減為 0 時才進行真正的卸載操作。這個好處非常明顯了,就是減少大腦負荷,不用記住某個資源在那里加載過,需要在哪里進行真正的卸載。主要保證每個 ResLoader 的申請,都對應一個 Recycle2Cache 就好。
這就是引用計數器的力量。引用計數的應用先說到這里。
下面說說,不用傳入 AssetBundleName 就能加載資源這個功能。
目前市面上大部分開源資源管理模塊都不支持這個功能,當然筆者不是原創。原創是筆者的前同事,給出原創 git 鏈接:Qarth
我們一般的方案, 每個從 AssetBundle 加載的資源,必須傳入 AssetBundleName。
例如:
ResourcesManager.LoadSync<Texture2D>("images","bg");
AssetBundleManager.LoadSync<Texture2D>("images","bg");
這是常見的解決方案。實現起來和原理也比較簡單。而且它已經足夠解決大部分問題了。
但是用的時候會有一點限制。
在一個項目的初期,美術的資源還沒有全部給出。項目中美術資源相關的目錄結構還不是最終版。這時候加載方式全部像上邊一樣寫。
在整個項目周期中,目錄結構發生了幾次變化。每次變化資源就會被打進不同的 AssetBundle 中,導致都要更改一次加載資源相關的代碼,可能改成如下:
ResourcesManager.LoadSync<Texture2D>("homes","bg");
AssetBundleManager.LoadSync<Texture2D>("homes","bg");
而字符串的更改往往比較麻煩,加載錯誤不會被編譯器識別,只有當項目運行之后才可能發現。這就會造成大量人力浪費。
而 ResKit 初期就處於這個階段。
當時的解決方案是代碼生成。生成 Bundle 名字常量和各個資源常量。這樣當一發生目錄結構的改變,就會報出來很多編譯錯誤。這樣生成的編譯錯誤一個一個地改就好了。
生成代碼如下:
namespace QAssetBundle
{
public class Assetobj_prefab
{
public const string BundleName = "ASSETOBJ_PREFAB";
public const string ASSETOBJ = "ASSETOBJ";
}
public class Gamelogic
{
public const string BundleName = "GAMELOGIC";
public const string BGLAYER = "BGLAYER";
public const string DEATHZONE = "DEATHZONE";
public const string LANDEFFECT = "LANDEFFECT";
public const string MAGNETITELAYER = "MAGNETITELAYER";
public const string PLAYER = "PLAYER";
public const string RAINPREFAB2D = "RAINPREFAB2D";
public const string STAGELAYER = "STAGELAYER";
public const string SUN = "SUN";
}
public class Gameplay_prefab
{
public const string BundleName = "GAMEPLAY_PREFAB";
public const string GAMEPLAY = "GAMEPLAY";
}
...
}
初期 ResKit 以這個方案減少了很多工作量,和項目風險。
關於只傳入 AssetName 不傳 AssetBundleName 支持的構想在很早就有了,只不過不敢確定到底可不可行,會不會造成性能上的瓶頸什么的,直到后來發現了 Qarth,確定是可行的方案。
實現非常簡單,只要生成一個配置表就夠了。
配置文件如下:
AssetTable.json
{
"AssetBundleInfos": [
{
"AssetInfos": [
{
"OwnerAssetBundleName": "images",
"AssetName": "Square"
},
{
"OwnerAssetBundleName": "images",
"AssetName": "SquareA"
},
{
"OwnerAssetBundleName": "images",
"AssetName": "SquareB"
}
],
"AssetBundleName": "images"
}
]
}
有了這個文件,就可以根據 AssetName 查詢對應的 AssetBundleName 就好了。
這樣直到項目優化階段會很少去進行代碼的改動,畢竟項目開發的每一分鍾都很寶貴。
使用這種方式會有一些限制,就是資源不能同名。同名時就會加載第一個查詢到的資源和對應的 AssetBundle。
當發生不同 AssetBundle 之間有相同資源時,只要傳入 AssetBundleName 就可以解決。這種操作在日常開發中占極少數。多語言包可能是一種常見的需求。
還有一種是不同類型的資源同名,比如 prefab 類型 和 TextureD 類型的資源使用了同一個名字,這里筆者想了幾種解決方案:
1. 可傳入文件擴展名。根據擴展名決定加載哪個資源,比如:
AssetBundleManager.LoadSync<Texture2D>("bg.jpg");
AssetBundleManager.LoadSync<GameObject>("bg.prefab");
實現起來也是比較容易的事情。
2. 根據傳入泛型類型去判斷,比如:
mResLoader.LoadSync<Texture2D>("bg"); // 加載 紋理類型的,名為 bg 的資源。
mResLoader.LoadSync<GameObject>("bg"); // 加載 GameObject 類型的,名為 bg 的資源。
實現也是比較簡單。
以上兩種都需要在生成配置階段,對每個資源生成更多的信息。
還有一種是 Qarth 的方案。
就是在對每個 AssetBundle,都加入了激活和未激活兩個狀態。
示意代碼如下。
ResMgr.ActivateAssetBundle("images");
mResLoader.LoadSync<Texture2D>("bg");
ResMgr.DectivateAssetBundle("images");
ResMgr.ActivateAssetBundle("home");
mResLoader.LoadSync<GameObject>("bg");
ResMgr.ActivateAssetBundle("home");
這也是一個非常不錯的方案。
目前以上三種 ResKit 都沒有支持,筆者也在糾結當中,也許三種都支持,也許只支持 1 ~ 2 種方式。可能會有更好的,隨着 QFramework 發展慢慢來吧。
值得一說的 Features 都聊完了。
接下來開始實踐部分,如果對原理部分有問題歡迎在文章下邊留言探討,或者私聊我也行,聯系方式在 QFramework 主頁上能找到。
第二部分:實踐
讓我們先一切歸零。下面筆者展示資源管理模塊的演化過程。
v0.0.1 Resources API 入門
public class UIHomePanel: MonoBehaviour
{
private Texture2D mBgTexture = null;
void Start()
{
mBgTexture = Resources.Load<Texture2D>("bg");
// do something
}
void OnDestroy()
{
Resources.UnloadAsset(mBgTexture);
mBgTexture = null;
}
}
代碼很容易理解,就是打開 UIHomePanel 時動態加載背景資源,當關閉時進行資源的卸載操作。
如果隨着需求增多,這個頁面可能需要動態加載的資源也會增多。
public class UIHomePanel: MonoBehaviour
{
private Texture2D mBgTexture = null;
private Texture2D mLogoTexture = null;
private Texture2D mBgEnTexture = null;
void Start()
{
mBgTexture = Resources.Load<Texture2D>("bg");
// do something
mLogoTexture = Resources.Load<Texture2D>("logo");
// do something
mBgEnTexture = Resources.Load<Texture2D>("bg_en");
// do something
}
void OnDestroy()
{
Resources.UnloadAsset(mBgTexture);
mBgTexture = null;
Resources.UnloadAsset(mLogoTexture);
mBgTexture = null;
Resources.UnloadAsset(mBgEnTexture);
mBgTexture = null;
}
}
這樣代碼量就增多了,成員變量的代碼和需要卸載的代碼隨着資源加載數量正比例增長。這里要盡量避免這種類型的增長。因為聲明那么多成員變量不是很好的事情。解決方案很簡單,引入一個 List,使用 List 來記錄本頁面加載過的資源。
v0.0.1 引入 List<UnityEngine.Object>
public class UIHomePanel: MonoBehaviour
{
private List<UnityEngine.Object> mLoadedAssets = new List<UnityEngine.Object>();
void Start()
{
var bgTexture = Resources.Load<Texture2D>("bg");
mLoadedAssets.Add(bgTexture);
// do something
var logoTexture = Resources.Load<Texture2D>("logo");
mLoadedAssets.Add(logoTexture);
// do something
var bgEnTexture = Resources.Load<Texture2D>("bg_en");
mLoadedAssets.Add(bgEnTexture);
// do something
}
void OnDestroy()
{
mLoadedAssets.ForEach(loadedAsset => {
Resources.UnloadAsset(loadedAsset);
});
mLoadedAssets.Clear();
mLoadedAssets = null;
}
}
這樣加載和卸載相關的代碼就固定了。
這時候又有一個問題,資源的重復加載和卸載。可能代碼如下:
public class UIHomePanel: MonoBehaviour
{
private List<UnityEngine.Object> mLoadedAssets = new List<UnityEngine.Object>();
void Start()
{
var bgTexture = Resources.Load<Texture2D>("bg");
mLoadedAssets.Add(bgTexture);
// do something
var logoTexture = Resources.Load<Texture2D>("logo");
mLoadedAssets.Add(logoTexture);
// do something
var bgEnTexture = Resources.Load<Texture2D>("bg_en");
mLoadedAssets.Add(bgEnTexture);
// do something
OtherFunction();
}
void OtherFunction()
{
// 重復加載了,也會導致重復卸載。
var logoTexture = Resources.Load<Texture2D>("logo");
mLoadedAssets.Add(logoTexture);
// do something
}
void OnDestroy()
{
mLoadedAssets.ForEach(loadedAsset => {
Resources.UnloadAsset(loadedAsset);
});
mLoadedAssets.Clear();
mLoadedAssets = null;
}
}
對於 Resources 這個 API 來說問題不大,但是 AssetBundle 就可能比較危險了,重復加載 AssetBundle 會導致閃退。所以比較危險了,還是要避免。
v0.0.2 添加重復加載判斷
public class UIHomePanel: MonoBehaviour
{
private List<UnityEngine.Object> mLoadedAssets = new List<UnityEngine.Object>();
void Start()
{
var bgTexture = LoadAsset<Texture2D>("bg");
// do something
var logoTexture = LoadAsset<Texture2D>("logo");
// do something
var bgEnTexture = LoadAsset<Texture2D>("bg_en");
// do something
OtherFunction();
}
void OtherFunction()
{
// 重復加載了,也會導致重復卸載。
var logoTexture = LoadAsset<Texture2D>("logo");
// do something
}
T LoadAsset<T>(string assetName) where T: UnityEngine.Object
{
var retAsset = mLoadedAssets.Find(loadedAsset=>loadedAsset.name == assetName);
if (resAsset)
{
return resAsset as T;
}
retAsset = Resources.Load<T>(assetName);
mLoadedAssets.Add(retAsset);
return retAsset;
}
void OnDestroy()
{
mLoadedAssets.ForEach(loadedAsset => {
Resources.UnloadAsset(loadedAsset);
});
mLoadedAssets.Clear();
mLoadedAssets = null;
}
}
添加 LoadAset 方法,帶來了一個意外的好處,就是加載資源部分的代碼也變得精簡了。
這樣就可以避免重復加載和卸載了,當然重復加載和卸載僅限於在 UIHomePanel 內。還是沒法避免多個頁面之間的對統一個資源的重復加載和卸載的。要解決這個問題會相對麻煩,我們分幾個版本慢慢迭代。
第一個要做的就是,先把這套加載卸載策略封裝好,總不能讓每個加載資源的頁面或者腳本都寫一遍這套策略。
v0.0.3 ResLoader
策略的復用有很多種,繼承、封裝成服務類對象等等。
封裝成一個服務類對象會好搞一些,就是 ResLoader。
public class ResLoader
{
private List<UnityEngine.Object> mLoadedAssets = new List<UnityEngine.Object>();
public T LoadAsset<T>(string assetName) where T: UnityEngine.Object
{
var retAsset = mLoadedAssets.Find(loadedAsset=>loadedAsset.name == assetName);
if (resAsset)
{
return resAsset as T;
}
retAsset = Resources.Load<T>(assetName);
mLoadedAssets.Add(retAsset);
return retAsset;
}
public void UnloadAll()
{
mLoadedAssets.ForEach(loadedAsset => {
Resources.UnloadAsset(loadedAsset);
});
mLoadedAssets.Clear();
mLoadedAssets = null;
}
}
而 UIHomePanel 則會變成如下:
public class UIHomePanel: MonoBehaviour
{
ResLoader mResLoader = new ResLoader();
void Start()
{
var bgTexture = mResLoader.LoadAsset<Texture2D>("bg");
// do something
var logoTexture = mResLoader.LoadAsset<Texture2D>("logo");
// do something
var bgEnTexture = mResLoader.LoadAsset<Texture2D>("bg_en");
// do something
OtherFunction();
}
void OtherFunction()
{
// 重復加載了,也會導致重復卸載。
var logoTexture = mResLoader.LoadAsset<Texture2D>("logo");
// do something
}
void OnDestroy()
{
mResLoader.UnloadAll();
mResLoader = null;
}
}
使用代碼精簡了很多。
ResLoader 只是記錄下當前頁面或者腳本加載過的資源,這樣不夠用,還需要一個記錄全局加載過資源的容器。
v0.0.4 SharedLoadedAssets & Res
實現很簡單,給 ResLoader 創建一個靜態 List<UnityEngine.Object>
。
ResLoader.cs
public class ResLoader
{
private List<UnityEngine.Object> mLoadedAssets = new List<UnityEngine.Object>();
private static List<UnityEngine.Object> mSharedLoadedAssets = new List<UnityEngine.Object>();
public T LoadAsset<T>(string assetName) where T: UnityEngine.Object
{
var retAsset = mLoadedAssets.Find(loadedAsset=>loadedAsset.name == assetName);
if (resAsset)
{
return resAsset as T;
}
retAsset = Resources.Load<T>(assetName);
mLoadedAssets.Add(retAsset);
return retAsset;
}
public void UnloadAll()
{
mLoadedAssets.ForEach(loadedAsset => {
Resources.UnloadAsset(loadedAsset);
});
mLoadedAssets.Clear();
mLoadedAssets = null;
}
}
這個 mSharedLoadedAssets 有什么作用呢 ?
它相當於一個全局資源池,而 ResLoader 獲取資源,都要從資源池中獲取,何時進行加載和卸載取決於某個資源是否是第一次加載、某個資源卸載時是不是不被所有 ResLoader 引用。
所以資源的加載和卸載,應該取決於被引用的次數,第一次被引用就加載該資源,最后一次引用釋放則進行卸載。
但是 UnityEngine.Object 沒有提供引用計數功能。
所以需要在 UnityEngine.Object 基礎上抽象一個類 Res:
這個 Res,要實現一個引用計數的功能。
而且為了未來分出來不同類型的資源(例如 ResourcesRes 和 AssetBundleRes/AssetRes 等),Res 也要管理自己的加載卸載操作。
代碼如下:
Res.cs
public class Res
{
public string Name
{
get { return mAsset.name; }
}
public Res(Object asset)
{
mAsset = asset;
}
private Object mAsset;
private int mReferenceCount = 0;
public void Retain()
{
mReferenceCount++;
}
public void Release()
{
mReferenceCount--;
if (mReferenceCount == 0)
{
Resources.UnloadAsset(mAsset);
ResLoader.SharedLoadedReses.Remove(this);
mAsset = null;
}
}
}
對應的 ResLoader 變為如下:
public class ResLoader
{
/// <summary>
/// 共享的
/// </summary>
public static List<Res> SharedLoadedReses = new List<Res>();
/// <summary>
/// 持有的
/// </summary>
private List<Res> mResList = new List<Res>();
public T LoadAsset<T>(string assetName) where T : Object
{
var loadedRes = mResList.Find(loadedAsset=>loadedAsset.Name == assetName);
if (loadedRes != null)
{
return loadedRes as T;
}
loadedRes = SharedLoadedReses.Find(loadedAsset => loadedAsset.Name == assetName);
if (loadedRes != null)
{
loadedRes.Retain();
mResList.Add(loadedRes);
return loadedRes as T;
}
var asset = Resources.Load<T>(assetName);
loadedRes = new Res(asset);
SharedLoadedReses.Add(loadedRes);
loadedRes.Retain();
mResList.Add(loadedRes);
return asset;
}
public void UnloadAll()
{
foreach (var asset in mResList)
{
asset.Release();
}
mResList.Clear();
mResList = null;
}
}
ResLoader 的代碼,希望大家仔細研讀下,尤其是 LoadAsset 方法。加載步驟在原理部分有簡單講過,這里不多說了。而 UnloadAll,則是不是進行真正的卸載,而是對資源進行一次釋放。
使用代碼 UIHomePanel.cs 不變:
public class UIHomePanel: MonoBehaviour
{
ResLoader mResLoader = new ResLoader();
void Start()
{
var bgTexture = mResLoader.LoadAsset<Texture2D>("bg");
// do something
var logoTexture = mResLoader.LoadAsset<Texture2D>("logo");
// do something
var bgEnTexture = mResLoader.LoadAsset<Texture2D>("bg_en");
// do something
OtherFunction();
}
void OtherFunction()
{
// 重復加載了,也會導致重復卸載。
var logoTexture = mResLoader.LoadAsset<Texture2D>("logo");
// do something
}
void OnDestroy()
{
mResLoader.UnloadAll();
mResLoader = null;
}
}
這里在寫其他的示例代碼,加載相同的資源:
public class UIOtherPanel: MonoBehaviour
{
ResLoader mResLoader = new ResLoader();
void Start()
{
var bgTexture = mResLoader.LoadAsset<Texture2D>("bg");
// do something
var logoTexture = mResLoader.LoadAsset<Texture2D>("logo");
// do something
var bgEnTexture = mResLoader.LoadAsset<Texture2D>("bg_en");
// do something
OtherFunction();
}
void OtherFunction()
{
// 重復加載了,也會導致重復卸載。
var logoTexture = mResLoader.LoadAsset<Texture2D>("logo");
// do something
}
void OnDestroy()
{
mResLoader.UnloadAll();
mResLoader = null;
}
}
這樣就算 UIHomePanel 和 UIOtherPanel 同時打開,也不會發生資源的重復加載或卸載了。
到這里一個基本的管理模型完成了,可以將版本號升級為 v0.1.1,算是一個最小可執行版本(MVP)。不管在使用還是結構上都是一個非常好的方案。而 ResKit 就是以這個管理模型為基礎,慢慢完善功能的,至今為止支持了非常多的項目。
本片的文章就到這里
那么剩下的不值得一提的功能歡迎研讀本 chat 的示例工程和 QFramework 中的 ResKit 模塊。
轉載請注明地址:涼鞋的筆記:liangxiegame.com
更多內容
-
QFramework 地址:https://github.com/liangxiegame/QFramework
-
QQ 交流群:623597263
-
Unity 進階小班:
- 主要訓練內容:
- 框架搭建訓練(第一年)
- 跟着案例學 Shader(第一年)
- 副業的孵化(第二年、第三年)
- 權益、授課形式等具體詳情請查看《小班產品手冊》:https://liangxiegame.com/master/intro
- 主要訓練內容:
-
關注公眾號:liangxiegame 獲取第一時間更新通知及更多的免費內容。