ResourcesComponent学习笔记
请大家关注我的微博:@NormanLin_BadPixel坏像素
看这篇文章前,我希望大家先去了解一下Unity的AssetBundle。找最近的看,Unity近几个版本对其做了不少更新。
并且,这里必定有热更新相关的资源读取,大家也一起学习一下吧。
大家可以先看一下ABInfo
ABInfo
我这里就直接注释源代码好了。
private int refCount;//用来标识这个资源被引用的次数
public string Name { get; }//用来识别Assetbundle
public AssetBundle AssetBundle { get; }//Assetbundle的具体资源
而且会在Dispose方法当中对存放的已经加载的AssetBundle资源进行释放。
ResourcesComponent
//用来存放已经加载了的Object
private readonly Dictionary<string, UnityEngine.Object> resourceCache = new Dictionary<string, UnityEngine.Object>();
//用来存放已经加载了的AssetBundle信息ABInfo
private readonly Dictionary<string, ABInfo> bundles = new Dictionary<string, ABInfo>();
// lru缓存队列。当想卸载一个包的时候,会先存放在这里而不是马上卸载
private readonly QueueDictionary<string, ABInfo> cacheDictionary = new QueueDictionary<string, ABInfo>();
public K GetAsset<K>(string bundleName, string prefab) where K : class
{
string path = $"{bundleName}/{prefab}".ToLower();
UnityEngine.Object resource = null;
if (!this.resourceCache.TryGetValue(path, out resource))
{
throw new Exception($"not found asset: {path}");
}
K k = resource as K;
if (k == null)
{
throw new Exception($"asset type error, type: {k.GetType().Name}, path: {path}");
}
return k;
}
因为导出AssetBundle的时候硬性设置全部都是小写,所以我们为了以防万一,在获取path的时候,把string给小写化。
这段代码就是通过AssetBundle的名字和想要获取的资源的名称获取到资源。
/// <summary>
/// 同步加载assetbundle
/// </summary>
/// <param name="assetBundleName"></param>
/// <returns></returns>
public void LoadBundle(string assetBundleName)
我们知道,一个AssetBundle可能会有多个依赖,我们在加载时候,要把这些依赖包一并加载进来。ResourcesHelper就是用来帮助我们获取依赖包的名称的。
ResourcesHelper
从这里开始,我们就要提出另一个问题了。当我们在开发的时候,不可能每次更新资源的时候都重新倒一遍AssetBundle,特别是当资源多了之后。所以我们得写两套资源加载的方案,一种用在我们开发的时候,直接从本地获取资源。另一种是我们在发布后,通过AssetBundle加载资源。而Unity的宏定义,就很方便我们使用。Define这个类,也是方便我们编辑的。因为如果只用宏定义,你会发现,当我们在编辑未定义的代码时,就是在编辑一段文本,享受不到脚本编辑器提供给我们强大的功能。当然不全是哈。
public static string[] GetDependencies(string assetBundleName)
这个方法就是通过上面的方案来获取到指定AssetBundle的依赖。
public static string[] GetSortedDependencies(string assetBundleName)
再此基础上,作者还提供了对依赖排序的方法。我们来看看是怎么做到的。
public static void CollectDependencies(List<string> parents, string assetBundleName, Dictionary<string, int> info)
{
parents.Add(assetBundleName);
string[] deps = GetDependencies(assetBundleName);
foreach (string parent in parents)
{
if (!info.ContainsKey(parent))
{
info[parent] = 0;
}
info[parent] += deps.Length;
}
foreach (string dep in deps)
{
if (parents.Contains(dep))
{
throw new Exception($"包有循环依赖,请重新标记: {assetBundleName} {dep}");
}
CollectDependencies(parents, dep, info);
}
parents.RemoveAt(parents.Count - 1);
}
我们看到,这里有一个递归的用法。会不断的向下查找依赖,直到找到最底层。递归一般比较难理解,这里希望大家仔细看。第一次递归,如果一个AB有2个依赖AB1,AB2,则会登记AB有2个依赖,AB1 = 0,AB2 = 0。然后,再深入AB1,第二次递归,如果AB1有3个依赖AB1-1,AB1-2,AB1-3,然后更新信息,AB = 2 + 3 = 5,AB1 =0 + 3 = 3,AB2 = 0,AB1-1 = 0…然后深入AB2,然后。。。以此递归,直到需要深入的Assetbundle没有依赖,跳出递归。需要注意的是,用这种方法要求包内没有循环依赖。也就是不能AB1依赖AB2,AB2也依赖AB1。这样会进入死循环。
string[] ss = info.OrderBy(x => x.Value).Select(x => x.Key).ToArray();
运行到这里,我们已经知道,info里面已经存了所有依赖的AssetBundle,并且保存了每个AssetBundle所需要的依赖包个数为字典的值。info.OrderBy(x => x.Value) 就是通过字典的值进行从小到大的排序。.Select(x => x.Key).ToArray(); 更是提取出了排序后的键值,也就是依赖的AssetBundle名。这样,我们便可以让没有依赖的在最前面被加载,依赖项越少的排在越前面,后面加载的AssetBundle就不会出现依赖项没有被加载的情况了。这个逻辑大家仔细想想就可以明了了。很神奇的一步。
ResourcesComponent
public void LoadOneBundle(string assetBundleName)
这个方法就是直接加载指定的AssetBundle,一般调用这个方法的时候,都已经对其的依赖项加载过的。
如果可以在已经加载的bundles里面找到,则对其的引用信息加+1。如果在cacheDictionary里面,则把cacheDictionary里面的这个bundle取出来存到bundles内。引用+1。如果是第一次加载,就得加载AssetBunndle里面的资源了。如果我们是在编辑器模式下,也就是开发的时候,我们直接通过AssetBundle里面存放的资源地址加载资源。并把加载了的资源存放到resourceCache里面。之后,存放这个AssetBundle的信息进入bundles字典里。因为我们没有加载AssetBundle,所以这个ABInfo的AssetBundle是空的。
同样的,如果我们是通过AssetBundle获取资源,则先通过AssetBundle的名称加载到这个AssetBundle。
AssetBundle assetBundle = AssetBundle.LoadFromFile(Path.Combine(PathHelper.AppHotfixResPath, assetBundleName));
这里通过PathHelper来决定路径。PathHelper没啥技术含量,固定写吧。
然后,把加载到的资源根据包名+资源名存入resourceCache。存放这个AssetBundle的信息进入bundles字典里。
需要注意的是,上面讲的是同步加载Asset资源的,作者还提供了异步的方法,没什么区别,只是换了加载资源的模式。
assets = await assetsLoaderAsync.LoadAllAssetsAsync();
经过了之前的.Net异步编程学习笔记,相信大家对此很好理解的吧。
讲完加载,我们再来看看卸载。卸载就不分同步异步啦。当然,卸载的同时也要把没有其他作用的依赖包一并卸载了。不过,这里我有一个疑惑,为什么作者这里要用
string[] dependencies = ResourcesHelper.GetSortedDependencies(assetBundleName);
获取排序好的依赖包信息呢?如果真要按排序,卸载的时候不是应该跟加载的时候相反,从依赖项多的开始卸载吗?如果不需要排序,为什么要获取排序好的呢?
我们先继续看下去吧。
private void UnloadOneBundle(string assetBundleName)
我们可以看到,当我们尝试卸载一个bundle的时候,会先把这个bundle的引用-1,如果这个bundle还有其他的引用,则不会卸载。如果没有引用了,则会先把它从bundles中移除,缓存进cacheDictionary。就像作者注释的,先缓存十个包。如果超过10个了,则把最先缓存的包真正卸载。
为什么要缓存呢?因为如果有一些包大量反复加载卸载的时候,会很耗性能。用这个机制就可以解决这个问题。
结束语
以前就想研究一下AssetBundle,看了一堆的资料,却还是不知道从哪里下手。看了作者的源代码后,豁然开朗。不过,还得研究一套规范的AssetBundle命名方法。
果然,看源代码才是最有用的。——————-Norman_林