此篇文章准备了将近两周的时间,写了改,改了删。之前有朋友反馈,上一个文章太冗长了,影响阅读体验,这一讲就走个精简路线。所以只要不是很重要的内容就都删减掉了。
文章分两个部分,第一部分是原理,第二部分是实战。
原理部分,初学者尽量去理解就好,不用跟着敲,列出的代码都是示意代码。
实战部分是留给初学者的,希望敲完代码,这样有助于理解前边的原理。
当然原理不是很难。
第一部分:原理
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 获取第一时间更新通知及更多的免费内容。

被折叠的 条评论
为什么被折叠?



