上个月花时间把XAsset这套资源框架的代码看了一遍且写了一遍。XAsset官网:https://xasset.github.io/#/compare-plans。我看的是它的7.0体验版本,所以功能上并不完整,不过尽管如此还是学到了很多的东西。
XAsset框架的代码量很少,架构很清晰,我认为作为一个学习的目的来看这个框架是非常不错的选择(老实说在这之前我去看Addressable的代码,是没能看下去),它也让我对这一部分知识的理解有了提升,并且在此基础上我回头再次看了一遍项目中使用的框架,又有了新的理解。
在看的同时,我画了它的UML图,本想放出来,但是已经找不到扔哪去了。。。另外,我把它与项目中正使用的框架从加载和卸载两个方面进行了仔细的对比,写了份文章,然而项目代码是不能随便放的,所以也只能存在我的有道云笔记里。至于热更部分,由于XAsset我看的只是一个体验版本,不仅是个Demo并且没有经历过实际项目的打磨,没什么好比较的。
在对比过程中,我对我认为不够好的一些地方做了修改:
加载部分
主要是GC问题,在加载流程中所使用到的new对象的操作,我一律做了池化处理,主要是解决框架本身带来的GC问题,这个问题在项目中肯定是很严重的。
另外,我增加了直接加载AssetBundle的操作,因为有一些资源是需要直接加载AB统一做处理的,比如shader、配置表。
卸载部分
原XAsset做了一个简单的引用计数。当你加载asset的时候,asset会计一个引用,同时它所在的AB以及依赖AB也均计一个引用。当你调用接口release此asset时,asset、AB、依赖AB的引用也都相应减1。而当这些东西引用为0时,会从缓存列表中移除并且AB会调用unload(true)来移除掉。这样的机制是没有问题的,问题出在假如业务层没有正确的去调用release接口,那么缓存的asset以及对应AB、依赖AB都没有办法卸载掉,会一直存在于内存中。这个机制无疑是有问题的,因为业务层的操作是无法保证的。
所以针对这个问题,我把它的引用计数改成了两种形式,一种是单纯的使用一个数字去计引用,而另一种则使用weakReference的机制来避免这个问题,首先贴一下代码:
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Framework.Res.Runtime
{
public class WeakRef : IReference
{
private List<WeakReference> _references;
public WeakRef()
{
_references = new List<WeakReference>();
}
public bool Unused
{
get { return GetReferenceCount() <= 0; }
}
public void Retain(object owner)
{
if (owner == null)
{
Logger.LogError("owner is null");
return;
}
int count = _references.Count;
for (int i = 0; i < count; i++)
{
if (owner.Equals(_references[i].Target))
{
return;
}
}
WeakReference weakReference = new WeakReference(owner);
_references.Add(weakReference);
}
public void Release(object owner)
{
if (owner == null)
{
Logger.LogError("owner is null");
return;
}
int count = _references.Count;
for (int i = 0; i < count; i++)
{
if (owner.Equals(_references[i].Target))
{
_references.RemoveAt(i);
break;
}
}
}
public int GetReferenceCount()
{
int count = 0;
for (int j = 0; j < _references.Count; j++)
{
if (_references[j].Target != null)
{
if (_references[j].Target.GetType() == typeof(GameObject))
{
GameObject go = (GameObject)_references[j].Target;
if (go != null)
{
count++;
}
else
{
//Target不为null,go为null,特殊处理
_references.RemoveAt(j);
j--;
}
}
else
{
count++;
}
}
}
return count;
}
public void Reset()
{
_references.Clear();
}
}
}
具体怎么使用呢?一般我们加载完资源以后,会提供一个接口供业务层获取这个资源,这时候我要求业务层传入一个持有者。比如说一个UI组件用到一张图片,那么你想得到这个图片的资源,就把这个组件对象传进来:
public Object GetAsset(object owner)
{
if (owner == null) return null;
reference.Retain(owner);
return asset;
}
而此同时,在内部用weakReference将这个引用记下来,并且在GetReferenceCount时通过WeakReference.Target便可知道这个UI组件还是否存在,若不存在,则也代表你不再需要这个资源了,便可不必再计这个引用。
这样一来,业务层便不再需要主动调用Release()接口去释放自己的引用,只需要销毁你这个持有者便可,最坏的情况,也会在切换场景时自动被销毁掉。不过使用这种方法,需要再update中定时去检测每个资源的引用情况,不过相比较于它原生的那种必须正确使用的方式,这样保证了那些不再被使用的资源可以正确的被释放掉。
另外,上面说到了直接加载AB的情况,针对这种情况,一般在拿到AB后会自己通过AB去加载AB内的资源然后缓存起来,所以我在接口回调之后添加了自动卸载AB的操作,让上层不必考虑卸载的问题。同时,这时候在AB没有引用时也不能再使用unload(true),因为这样会一并卸载掉上层缓存的东西,所以需要改成unload(false)。